iOS架構師之路:慎用繼承
最近在看大神Casa的文章《跳出面向對象思想(一) 繼承》,腦洞大開。文章給我們展示了一個隨著產品需求不斷變化的例子,該例子中通過繼承實現不同頁面的搜索視圖和搜索邏輯的代碼復用,隨著產品需求的演變,最后導致繼承的搜索功能層級越來越深,相互依賴越來越嚴重,最后導致拔出蘿卜帶出泥,又隨著個性化需求的發展,最后代碼變得越來越混亂。相信有經驗的開發人員都經歷過這方面的痛苦。繼承對代碼復用來說非常好用,但同時繼承復用的思路隨著產品經理的需求變化會導致項目緊耦合,牽一發而動全身。繼承做面向對象的三大特性之一,當然宅正確的時候使用它能發揮巨大價值,但如果不加思索的使用也會帶來代碼維護和擴展上的災難。那我們應該在什么情況下使用繼承,什么情況下又需要避免濫用繼承,并能夠有其他方案替代繼承實現代碼復用的目的呢,這是作為架構師需要掌握的內功,最終達到十八般武藝,樣樣精通,需要時信手拈來的境界。
適用繼承的場合
大神Chris Eidhof的文章《subclassing》提到需要自定義UITableViewCell等View視圖的時候我們可以使用繼承來創建自定義View,這些代碼放入子類更合理,不光代碼得到更好的封裝,也能得到一個可以在工程中重用的組件。Chris Edihof還提到model可以繼承來實現了 isEqual: 、hash 、 copyWithZone: 和 description 等方法的類。
Chris Eidhof說的只是繼承的幾個應用場景,他沒有說使用繼承的界限。 Casa還提到是否使用繼承需要考慮三個點:
- 父類只是給子類提供服務,并不涉及子類的業務邏輯
- 層級關系明顯,功能劃分清晰,父類和子類各做各的。
- 父類的所有變化,都需要在子類中體現,也就是說此時耦合已經成為需求
在我看來一個很重要的原則就是我們不能脫離cocoa框架開發,所以我們可以繼承cocoa的類,以達到快速開發的目的,但是如果沒有特殊原因我們寫的代碼要控制在繼承鏈不增加兩層。
不適合繼承的場景
我比較同意Casa的觀點。當你發現你的繼承超過2層的時候,你就要好好考慮是否這個繼承的方案了,第三層繼承正是濫用的開端。確定有必要之后,再進行更多層次的繼承。Chris Eidhof也有類似的觀點:
In a lot of projects that I’ve worked on, I’ve seen deep hierarchies of subclasses. I am guilty of doing this as well. Unless the hierarchies are very shallow, you very quickly tend to hit limits.
( 在我工作的許多項目中看到過一些深度繼承的項目。當我也這么干的時候,總會感到內疚。除非繼承的層次非常淺,否則你會很快發現它的局限性。)
替代繼承解決復用需求的解決方案
1.協議(protocols)
我經常使用繼承來使得對象能夠響應某個方法,假設一個APP有播放器(player)對象,它擁有播放(play)方法播放視頻,如果APP希望支持油Tube,需要相同幾個播放(player)接口,但是方法的實現不同,通過繼承實現的代碼如下:
@interface Player : NSObject
- (void)play;
- (void)pause;
@end
@interface 油TubePlayer : Player
@end
這兩個類并沒有太多共用的代碼,它們只不過具有相同的接口。如果這樣的話,使用協議可能會是更好的方案。可以這樣用協議來寫你的代碼。
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player : NSObject <VideoPlayer>
@end
@interface 油TubePlayer : NSObject <VideoPlayer>
@end
這樣,油TubePlayer類就不必知道 Player類的內部實現了。
2.代理(delegation)
再以上面的例子為例,player對象希望在播放的時候執行一些自定義的行為,使用繼承也可以輕易的實現:創建個player對象的子類,然后重寫play方法,再調用[super play],再跟著希望執行的行為。但是我們也可以通過的代理的方式更有優雅的實現這個需求:
@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方法里,我們可以通過代理屬性調用 playerDidStartPlaying:方法,任何使用Player類的對象,可以遵守代理協議,就可以實現自定義的playerDidStartPlaying:方法了,player類依然保持它的通用性和獨立性,方便為對外提供服務。代理是非常強大技巧,蘋果本身就經常使用。像 UITextField這樣的類,有時候你還會想把幾個不同的方法分組到幾個單獨的協議里,比如UITableView—— 它不僅有一個代理(delegate),還有一個數據源(dataSource)。
3.類別(category)
我們有時候會給對象添加方法,通過集成的方式當然可以實現,但是不如category的方式來的方便和容易使用,不增加新的類,可復用的價值也更高。 比如我們需要給NSArray添加一個arrayByRemovingFirstObject方法,通過category的方式我們就可以這么做:
@interface NSArray (OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
在用類別擴展一個不是你自己的類的時候,在方法前添加前綴是個比較好的習慣做法。如果不這么做,有可能別人也用類別對此類添加了相同名字的函數。那時候程序的行為可能跟你想要的并不一樣,未預期的事情可能會發生。
使用類別還有另外一個風險,那就是,到最后你可能會使用一大堆的類別,連你自己都會失去對代碼全局的認識。假如那樣的話,創建自定義的類可能更簡單一些。
4.組合(composition)
Casa提到我們盡可能用組合替代繼承。組合是最強大的替代組合的選項。如果你想復用已經存在的代碼,并且不想共享同樣的接口,組合是最佳選擇。舉個例子,假設你要設計一個緩存類:
@interface OBJCache : NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
一個簡單的做法就通過聚成NSDictionary并且通過調用字典的方法來實現這上面兩個緩存方法。
@interface OBJCache : NSDictionary
但是這樣做會帶來一些問題。它本來是應該被詳細實現的,但只是通過字典來實現。現在,在任何需要一個 NSDictionary 參數的時候,你可以直接提供一個 OBJCache 值。但如果你想把它轉為其它完全不同的東西(例如你自己的庫),你就可能需要重構很多代碼了。
更好的方式就是組合了。創建一個緩存類,并將添加一個字典的私有屬性,對外還是暴露著兩個接口,實現的時候就可以通過調用字典屬性的方法實現我們使用字典的方法了,這樣做可以靈活改變其涉嫌,而該類的使用者恩不用進行重構。
總結
代碼復用,盡管他們都可以通過繼承實現,但是我們為了在沒有耦合需求的時候盡量不要使用繼承,而是根據不同場景采用不同復用代碼的方式。如果只是共享接口,我們可以使用協議;如果是希望共用一個方法的部分實現,但希望根據需要執行不同的其他行為,我們可以使用代理;如果是添加方法,我們可以優先使用類別(category);如果是為了使用一個類的很多方法,我們可以使用組合來實現。,如果當初只是出于代碼復用的目的而不區分類別和場景,就采用繼承是不恰當的。當你發現你的繼承超過2層的時候,你就要好好考慮是否這個繼承的方案了,第三層繼承正是濫用的開端。確定有必要之后,再進行更多層次的繼承。我認同Casa的看法:萬不得已不要用繼承,優先考慮組合等方式。
參考文章:《跳出面向對象思想(一) 繼承》、《subclassing》
文/jackiehoo(簡書)