這篇文章跟我以往的文章有點(diǎn)不一樣。它主要是一些思想與模式的匯集,而不是一篇指南。下面我所寫的模式幾乎全都來之不易,都是我犯了錯(cuò)之后才學(xué)到的。我并不認(rèn)為自己是子類方面的權(quán)威,但我確實(shí)想把我學(xué)到的一些東西分享出來。別把本文當(dāng)做權(quán)威指南,它只是一些例子的匯集。
在被問到 OOP(面向?qū)ο缶幊蹋┑臅r(shí)候,Alan Kay(OOP 的發(fā)明人)寫到:它跟類無關(guān),但跟消息有關(guān)。^1然而,很多人的關(guān)注點(diǎn)仍然還在類層次上。在本文中,我們會(huì)看幾個(gè)我們可能會(huì)把注意力放在創(chuàng)建復(fù)雜的類結(jié)構(gòu)上的例子,并給出更有用的替代方案。根據(jù)經(jīng)驗(yàn),這樣會(huì)讓代碼更簡(jiǎn)單,更易維護(hù)。關(guān)于這個(gè)話題,在 Clean Code(中文版:代碼整潔之道)和 Code Complete(中文版:代碼大全)中已經(jīng)有大量討論。推薦你閱讀這兩本書。
首先,我們討論幾種使用子類比較合適的場(chǎng)景。如果你要寫一個(gè)自定義布局的 UITableViewCell ,那就創(chuàng)建一個(gè)子類。這同樣適用于幾乎每個(gè)視圖。一旦你開始布局,把這塊代碼放入子類就更合理一些,不光代碼得到了更好的封裝,你也能得到一個(gè)可在工程之間重用的組件。
假設(shè)你的代碼是針對(duì)多平臺(tái)多版本的,并且你需要針對(duì)每個(gè)平臺(tái)每個(gè)版本寫一些代碼。這時(shí)候更合理的做法可能是創(chuàng)建一個(gè) OBJDevice 類,讓一些子類如 OBJIPhoneDevice 和 OBJIPadDevice ,甚至更深層的子類如 OBJIPhone5Device 來繼承,并讓這些子類重寫特定的方法。例如,你的 OBJDevice 類可能包含了函數(shù) applyRoundedCornersToView:withRadius ,它有一個(gè)默認(rèn)的實(shí)現(xiàn),但是也能被特定的子類重寫。
另一個(gè)子類化可能很有用的場(chǎng)景是模型對(duì)象(model object)。絕大多數(shù)情況下,我的模型對(duì)象繼承自一個(gè)實(shí)現(xiàn)了 isEqual: 、 hash 、 copyWithZone: 和 description 等方法的類。這些方法只被實(shí)現(xiàn)一次,并且迭代循環(huán)遍歷所有屬性,所以極不容易出錯(cuò)。(如果你也想找一個(gè)這樣的基類,可以考慮使用 Mantle ,它就是這么做的,并且做得更多。)
在以往工作過的很多工程中,我見到過很多繼承層次很深的子類。當(dāng)我也這么干的時(shí)候,總會(huì)感到內(nèi)疚。除非繼承的層次非常淺,否則你會(huì)很快發(fā)現(xiàn)它的局限性。^2
幸運(yùn)的是,如果你發(fā)現(xiàn)自己正在使用深層次的繼承,還有很多替代方案可選。在下面的章節(jié)中,我們會(huì)逐個(gè)進(jìn)行更詳細(xì)地描述。如果你的子類只是使用相同的接口,協(xié)議會(huì)是個(gè)非常好的替代方案。如果你知道某個(gè)對(duì)象需要大量的修改,你可能會(huì)使用代理來動(dòng)態(tài)改變和配置它。當(dāng)你想給已有對(duì)象增加一些簡(jiǎn)單功能時(shí),類別可能是個(gè)選擇。當(dāng)你有一堆重寫了相同方法的子類時(shí),你可以使用配置對(duì)象(configuration object)來代替。最后,當(dāng)你想重用某些功能時(shí),組合多個(gè)對(duì)象而不是擴(kuò)展它們可能會(huì)更好。
很多時(shí)候,使用子類的原因是你想保證某個(gè)對(duì)象可以響應(yīng)某些消息。假設(shè)在 app 里你有一個(gè)播放器對(duì)象,它可以播放視頻?,F(xiàn)在你想添加對(duì) YouTube 的支持,使用相同的接口,但是具體實(shí)現(xiàn)不同。你可以使像這樣用子類來實(shí)現(xiàn):
@class Player : NSObject
- (void)play;
- (void)pause;
@end
@class YouTubePlayer : Player
@end
事實(shí)上可能這兩個(gè)類并沒有太多共用的代碼,它們只不過具有相同的接口。如果這樣的話,使用協(xié)議可能會(huì)是更好的方案。可以這樣用協(xié)議來寫你的代碼:
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@class Player : NSObject <VideoPlayer>
@end
@class YouTubePlayer : NSObject <VideoPlayer>
@end
這樣,YouTubePlayer 類就不必知道 Player 類的內(nèi)部實(shí)現(xiàn)了。
再一次假設(shè)你有一個(gè)像上面例子中的 Player 類?,F(xiàn)在,你想在開始播放的時(shí)候在某個(gè)地方執(zhí)行一個(gè)自定義的函數(shù)。這么做相對(duì)容易一些:創(chuàng)建一個(gè)自定義的子類,重寫 play 方法,調(diào)用 [super play ],然后開始做你自定義的工作。這么做是一種方法。另外一種方法是,改動(dòng)你的 Player 對(duì)象,然后給它設(shè)置一個(gè)代理。如下:
@class Player;
@protocol PlayerDelegate
- (void)playerDidStartPlaying:(Player *)player;
@end
@class Player : NSObject
@property (nonatomic,weak) id<PlayerDelegate> delegate;
- (void)play;
- (void)pause;
@end
現(xiàn)在,在播放器的 play 方法里,就可以給代理發(fā)送 playerDidStartPlaying: 消息了。這個(gè) Player 類的任何使用者都可以僅僅實(shí)現(xiàn)這個(gè)代理協(xié)議,而不用繼承該該類, Player 類也能夠保持通用性。這是個(gè)強(qiáng)大有效的技術(shù),蘋果在自己的框架里大量地使用它。你想想像 UITextField 這樣的類,還有 NSLayoutManager。有時(shí)候你還會(huì)想把幾個(gè)不同的方法打包分組到幾個(gè)單獨(dú)的協(xié)議里,比如 UITableView —— 它不僅有一個(gè)代理(delegate),還有一個(gè)數(shù)據(jù)源(dataSource)。
有時(shí)候,你可能會(huì)想給一個(gè)對(duì)象增加一點(diǎn)點(diǎn)額外的功能。比如你想給 NSArray 增加一個(gè)方法 arrayByRemovingFirstObject。不用子類,你可以把這個(gè)函數(shù)放到一個(gè)類別里。像這樣:
@interface NSArray (OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
在用類別擴(kuò)展一個(gè)不是你自己的類的時(shí)候,在方法前添加前綴是個(gè)比較好的習(xí)慣做法。如果不這么做,有可能別人也用類別對(duì)此類添加了相同名字的函數(shù)。那時(shí)候程序的行為可能跟你想要的并不一樣,未預(yù)期的事情可能會(huì)發(fā)生。
使用類別還有另外一個(gè)風(fēng)險(xiǎn),那就是,到最后你可能會(huì)使用一大堆的類別,連你自己都會(huì)失去對(duì)代碼全局的認(rèn)識(shí)。假如那樣的話,創(chuàng)建自定義的類可能更簡(jiǎn)單一些。
在我經(jīng)常會(huì)犯的錯(cuò)誤中(現(xiàn)在很快就能發(fā)現(xiàn)了),其中有一條是:使用一個(gè)含有幾個(gè)抽象方法的類并讓很多子類來重寫某個(gè)方法。例如,在一個(gè)幻燈片應(yīng)用里,你有一個(gè)主題類 Theme ,它有幾個(gè)屬性,比如 backgroundColor 和 font ,還有一些在一張幻燈片上如何布局的邏輯函數(shù)。
然后,對(duì)每種主題,你都創(chuàng)建一個(gè) Theme 的子類,重寫某個(gè)函數(shù)(例如 setup )并配置其屬性。直接使用父類對(duì)此做不了什么事。在這種情況下,你可以使用配置對(duì)象來讓代碼更簡(jiǎn)單些。你可以把共有的邏輯(比如幻燈片布局)放在 Theme 類中,把屬性的配置放到較簡(jiǎn)單的對(duì)象中,這些對(duì)象中只含有這些屬性。
例如,類 ThemeConfiguration 具有 backgroundColor 和 font 屬性,而類 Theme 在其初始化函數(shù)中獲取一個(gè)配置類 ThemeConfiguration 的值。
組合是代替子類化的最強(qiáng)大有效的方案。如果你想重用已有代碼而不想共享同樣的接口,組合就是你的首選武器。例如,假設(shè)你要設(shè)計(jì)一個(gè)緩存類:
@interface OBJCache : NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
簡(jiǎn)單點(diǎn)的做法是直接繼承 NSDictionary,通過調(diào)用字典的函數(shù)來實(shí)現(xiàn)上面的兩個(gè)方法。
@interface OBJCache : NSDictionary
但是這么做有幾個(gè)弊端。它本來是應(yīng)該被詳細(xì)實(shí)現(xiàn)的,但只是通過字典來實(shí)現(xiàn)。現(xiàn)在,在任何需要一個(gè) NSDictionary 參數(shù)的時(shí)候,你可以直接提供一個(gè) OBJCache 值。但如果你想把它轉(zhuǎn)為其它完全不同的東西(例如你自己的庫),你就可能需要重構(gòu)很多代碼了。
更好的方式是,將這個(gè)字典存在一個(gè)私有屬性(或者實(shí)例變量)中,對(duì)外僅僅暴露這兩個(gè) cache 方法。現(xiàn)在,當(dāng)你有了更深入想法的時(shí)候,你可以在靈活地修改其實(shí)現(xiàn),而該類的使用者們不用進(jìn)行重構(gòu)。