iOS架構師之路:慎用繼承

  最近在看大神Casa的文章《跳出面向對象思想(一) 繼承》,腦洞大開。文章給咱們展現了一個隨着產品需求不斷變化的例子,該例子中經過繼承實現不一樣頁面的搜索視圖和搜索邏輯的代碼複用,隨着產品需求的演變,最後致使繼承的搜索功能層級愈來愈深,相互依賴愈來愈嚴重,最後致使拔出蘿蔔帶出泥,又隨着個性化需求的發展,最後代碼變得愈來愈混亂。相信有經驗的開發人員都經歷過這方面的痛苦。繼承對代碼複用來講很是好用,但同時繼承複用的思路隨着產品經理的需求變化會致使項目緊耦合,牽一髮而動全身。繼承作面向對象的三大特性之一,固然宅正確的時候使用它能發揮巨大價值,但若是不加思索的使用也會帶來代碼維護和擴展上的災難。那咱們應該在什麼狀況下使用繼承,什麼狀況下又須要避免濫用繼承,並可以有其餘方案替代繼承實現代碼複用的目的呢,這是做爲架構師須要掌握的內功,最終達到十八般武藝,樣樣精通,須要時信手拈來的境界。html

 

適用繼承的場合緩存

      大神Chris Eidhof的文章《subclassing》提到須要自定義UITableViewCell等View視圖的時候咱們可使用繼承來建立自定義View,這些代碼放入子類更合理,不光代碼獲得更好的封裝,也能獲得一個能夠在工程中重用的組件。Chris Edihof還提到model能夠繼承來實現了 isEqual: 、hash 、 copyWithZone: 和 description 等方法的類。架構

Chris Eidhof說的只是繼承的幾個應用場景,他沒有說使用繼承的界限。 Casa還提到是否使用繼承須要考慮三個點:框架

1. 父類只是給子類提供服務,並不涉及子類的業務邏輯less

2. 層級關係明顯,功能劃分清晰,父類和子類各作各的。ide

3. 父類的全部變化,都須要在子類中體現,也就是說此時耦合已經成爲需求函數

  在我看來一個很重要的原則就是咱們不能脫離cocoa框架開發,因此咱們能夠繼承cocoa的類,以達到快速開發的目的,可是若是沒有特殊緣由咱們寫的代碼要控制在繼承鏈不增長兩層。ui

 

不適合繼承的場景this

我比較贊成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. 在我工做的許多項目中看到過一些深度繼承的項目。當我也這麼幹的時候,總會感到內疚。除非繼承的層次很是淺,不然你會很快發現它的侷限性。atom

 

替代繼承解決複用需求的解決方案

      1.協議(protocols)

  我常用繼承來使得對象可以響應某個方法,假設一個APP有播放器(player)對象,它擁有播放(play)方法播放視頻,若是APP但願支持YouTube,須要相同幾個播放(player)接口,可是方法的實現不一樣,經過繼承實現的代碼以下:

@interface Player : NSObject

- (void)play;

- (void)pause;
 
@end

@interface YouTubePlayer : Player


@end

  這兩個類並無太多共用的代碼,它們只不過具備相同的接口。若是這樣的話,使用協議可能會是更好的方案。能夠這樣用協議來寫你的代碼。

@protocol VideoPlayer <NSObject>

- (void)play;
- (void)pause;

@end


@interface Player : NSObject <VideoPlayer>

@end


@interface YouTubePlayer : NSObject <VideoPlayer>

@end

  這樣,YouTubePlayer 類就沒必要知道 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》

相關文章
相關標籤/搜索