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