[翻譯]本文翻譯自objc.io創(chuàng)始人、iOS大神Chris Eidhof的文章层释, 原文鏈接可查看Subclassing。
——————————————我是分割線——————————————
這篇文章和我平時寫的文章有點(diǎn)不同座舍,它不是一份標(biāo)準(zhǔn)指南港令,它更像一些想法和模式的匯總。以下我將要描述的幾乎所有模式的弊端都是等到自己犯了錯誤才發(fā)現(xiàn)的秕磷。我絕不認(rèn)為自己是使用繼承的權(quán)威诵闭,但我確實想分享我學(xué)到的一些東西。
當(dāng)被問及OOP(Object Oriented Programing澎嚣,面向?qū)ο缶幊蹋r艾倫凱(發(fā)明者)表示疏尿,OOP不是關(guān)于類而是關(guān)于消息傳遞的技術(shù)。盡管如此币叹,很多人還是過分關(guān)注創(chuàng)建類的層次結(jié)構(gòu)润歉。在本文中,我們將看看繼承的一些有用情景颈抚,但我們主要關(guān)注復(fù)雜繼承結(jié)構(gòu)的替代方案踩衩。根據(jù)我們的經(jīng)驗,這會導(dǎo)致代碼更簡單贩汉,更易于維護(hù)驱富。關(guān)于這個主題已經(jīng)有人寫了很多東西,你可以在Clean Code和Code Complete這樣的書中找到匹舞,這兩本書也推薦大家閱讀褐鸥。
什么時候用繼承
首先,我們來談?wù)剟?chuàng)建子類的一些有利情景赐稽。如果您正在構(gòu)建自定義布局的UITableViewCell
叫榕,幾乎所有的觀點(diǎn)都一樣——請創(chuàng)建一個子類浑侥。將布局代碼移入子類是有意義的,這樣您不僅可以很好地聚合代碼晰绎,而且還可以在項目隨處復(fù)用這個對象寓落。
假設(shè)你的代碼是針對多個平臺和版本的,你需要以某種方式為每個平臺和版本編寫一些自定義的細(xì)節(jié)荞下。此時創(chuàng)建一個OBJDevice
類是有意義的伶选,它可以有OBJIPhoneDevice
和OBJIPadDevice
這樣的子類,甚至可以包含像OBJIPhone5Device
這樣更深層次的子類尖昏,這些子類覆蓋了特定的方法仰税。例如,你的OBJDevice
可能包含applyRoundedCornersToView:withRadius:
方法抽诉。它雖然被默認(rèn)實現(xiàn)陨簇,但也可以被特定子類覆蓋并重寫。
另一種有利情況是用在實體類對象上掸鹅。大多數(shù)情況下塞帐,我的實體對象會繼承于一個基類,這個基類實現(xiàn)了isEqual
巍沙,hash
葵姥,copyWithZone:
和description
等方法。這些方法通過迭代屬性實現(xiàn)一次句携,使得代碼更不容易出錯榔幸。 (如果你正在尋找這樣的基類,你可以考慮使用Mantle)
何時不要用繼承
在我曾工作過的很多項目中矮嫉,我見過許多深層的子類結(jié)構(gòu)削咆。我自己也犯過這樣的錯誤。除非繼承結(jié)構(gòu)很淺蠢笋,否則你很快看到它的局限性拨齐。
幸運(yùn)的是,如果你發(fā)現(xiàn)自己處于這樣的深層次繼承結(jié)構(gòu)中昨寞,那么你有很多其他選擇瞻惋。在下文我們將更詳細(xì)地介紹每種替代方案。如果你的子類們僅僅共用相同方法名援岩,協(xié)議可能是一個很好的選擇歼狼。如果你知道一個對象需要經(jīng)常改動,你可能需要使用協(xié)議來動態(tài)更改和配置它享怀。當(dāng)你想給現(xiàn)有對象拓展功能時羽峰,類拓展可能是個更好的選擇。當(dāng)你有很多子類需要重寫相同的父類方法時,你可以使用配置對象梅屉。最后值纱,當(dāng)你想重用某些功能時,最好是組合多個對象而不是擴(kuò)展它們履植。
代替方案
使用協(xié)議代替繼承
通常计雌,當(dāng)你想確保一個對象會響應(yīng)某些消息時你會使用繼承。假設(shè)你有一個可以播放視頻的播放器對象∶钓現(xiàn)在你想讓它支持YouTube,此時你會用到相同的方法名妈橄,但內(nèi)部實現(xiàn)不同庶近。你可以用繼承實現(xiàn)這一需求:
@interface Player:NSObject
- (void)play;
- (void) pause;
@end
@interface YouTubePlayer:Player
- (void)play;
- (void) pause;
@end
這兩個類很可能不共用很多代碼,但它們有相同的方法名眷蚓。在這種情況下鼻种,使用協(xié)議可能是一種更好的解決方案。使用協(xié)議沙热,您可以像這樣編寫代碼:
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player:NSObject <VideoPlayer>
@end
@interface YouTubePlayer:NSObject <VideoPlayer>
@end
這樣叉钥,YouTubePlayer
不需要知道Player
的內(nèi)部實現(xiàn)。
使用代理代替繼承
再次假設(shè)你有一個像上例中的Player
類「菝常現(xiàn)在投队,你可能要在play
方法中執(zhí)行自定義操作。一種相對容易的做法是創(chuàng)建一個自定義的子類重寫play
方法爵川,先調(diào)用[super play]
敷鸦,然后執(zhí)行自定義操作。這是一種方法寝贡,另一種方法則是給你的Player
對象設(shè)置一個代理扒披。例如:
@class Player;
@protocol PlayerDelegate
- (void)playerDidStartPlaying:(Player *)player;
@end
@interface Player:NSObject
@property(nonatomic,weak)id <PlayerDelegate>delegate;
- (void)play;
- (void)pause;
@end
在Player
對象的play
方法中圃泡,delegate
可以接收playerDidStartPlaying:
消息碟案。 比起使用繼承,現(xiàn)在這個類的任何調(diào)用者可以通過代理協(xié)議方法實現(xiàn)自定義需求颇蜡,而且還可以保證Player
類非常簡潔价说。 這是一項非常強(qiáng)大的技術(shù),蘋果在其自己的框架中也大量使用了它澡匪,例如UITextField
熔任、NSLayoutManager
。 有時候你還想用不同的協(xié)議將不同的方法組合在一起唁情,它不僅有一個delegate
疑苔,而且還有一個dataSource
,這時你就可以參照UITableView
甸鸟。
使用類拓展代替繼承
有時候惦费,你可能想給一個對象拓展一點(diǎn)點(diǎn)額外的功能兵迅。假設(shè)你想通過添加一個arrayByRemovingFirstObject
方法來擴(kuò)展NSArray
。這時您可以將其歸入一個類拓展薪贫,而不是使用繼承恍箭。它是這樣實現(xiàn)的:
@interface NSArray(OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
使用Categories技術(shù)擴(kuò)展一個不屬于你自己的類時,給方法名添加前綴是一個好習(xí)慣瞧省。你不這樣做扯夭,有可能有人會不小心實現(xiàn)相同的方法,這時很容易引發(fā)意外錯誤鞍匾。
使用類拓展的局限之一是交洗,您最終可能會因為大量的類拓展失去你對這個類的全局觀。在這種情況下橡淑,創(chuàng)建自定義子類可能更合理构拳。
使用配置對象代替繼承
我長期犯的錯誤之一是使用一個抽象類,它有很多子類會重寫一個特定方法梁棠。例如置森,在幻燈片類型的App中,您可能會有一個類叫Theme
符糊,它擁有一些屬性凫海,如backgroundColor
和font
,以及在幻燈片上用于布局的一些邏輯濒蒋。
接著我為每個主題創(chuàng)建Theme
的子類盐碱,并重寫一個父類方法(例如setup
)來配置屬性。這里直接使用父類是沒有意義的沪伙。在這種情況下瓮顽,你可以使用配置對象使代碼更簡單一些。您可以保留Theme類中的共用邏輯(例如幻燈片布局)围橡,并將配置邏輯移動到只擁有屬性的簡單對象中暖混。
例如,創(chuàng)建一個配置類ThemeConfiguration
翁授,它擁有backgroundColorand
拣播、font
等屬性,并且Theme
類可以在初始化時獲取這個類收擦。
使用組合代替繼承
繼承最強(qiáng)大的替代方法是組合贮配。如果你想復(fù)用現(xiàn)有代碼,但這些代碼的方法名不同塞赂,組合將是一項強(qiáng)力手段泪勒。例如,假設(shè)你正在設(shè)計一個緩存類:
@interface OBJCache:NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
一個簡單的做法是繼承于NSDictionary
并通過調(diào)用字典的方法來實現(xiàn)這兩種方法:
@interface OBJCache : NSDictionary
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
@implementation OBJCache
- (void)cacheValue:(id)value forKey:(NSString *)key{
[self setValue:value forKey:key];
}
@end
但是這樣做會有一些問題。它用字典實現(xiàn)的事實應(yīng)該是一個實現(xiàn)細(xì)節(jié)≡泊妫現(xiàn)在當(dāng)你在任何地方想傳一個NSDictionary
參數(shù)時叼旋,你都有可能傳了OBJCache
值。當(dāng)你想把OBJCache
類切換到完全不同的東西(例如你自己的庫)沦辙,你可能需要重構(gòu)很多代碼夫植。
更好的方法是在私有屬性(或?qū)嵗兞浚┲谐钟幸粋€字典,并僅公開這兩個緩存方法∮脱叮現(xiàn)在详民,您可以靈活地改變其實現(xiàn)以滿足更多需求,同時這個類的調(diào)用者也不需要重構(gòu)相關(guān)代碼撞羽。