繼承
繼承從代碼復(fù)用的角度來說,特別好用,也特別容易被濫用和被錯(cuò)用。不恰當(dāng)?shù)厥褂美^承導(dǎo)致的最大的一個(gè)缺陷特征就是高耦合。
在這里我要補(bǔ)充一點(diǎn),耦合是一個(gè)特征,雖然大部分情況是缺陷的特征,但是當(dāng)耦合成為需求的時(shí)候,耦合就不是缺陷了。
適用繼承的場(chǎng)合
大神Chris Eidhof的文章《subclassing》提到需要自定義UITableViewCell等View視圖的時(shí)候我們可以使用繼承來創(chuàng)建自定義View,這些代碼放入子類更合理,不光代碼得到更好的封裝,也能得到一個(gè)可以在工程中重用的組件。Chris Edihof還提到model可以繼承來實(shí)現(xiàn)了 isEqual: 、hash 、 copyWithZone: 和 description 等方法的類。
當(dāng)我們使用繼承的時(shí)候我們需要考慮
- 父類只是給子類提供服務(wù),并不涉及子類的業(yè)務(wù)邏輯
- 層級(jí)關(guān)系明顯,功能劃分清晰,父類和子類各做各的。
- 父類的所有變化,都需要在子類中體現(xiàn),也就是說此時(shí)耦合已經(jīng)成為需求
- 我們不能脫離cocoa框架開發(fā),所以我們可以繼承cocoa的類,以達(dá)到快速開發(fā)的目的,但是如果沒有特殊原因我們寫的代碼要控制在繼承鏈不增加兩層。
不適合繼承的場(chǎng)景
- 當(dāng)你發(fā)現(xiàn)你的繼承超過2層的時(shí)候,你就要好好考慮是否這個(gè)繼承的方案了
- 不滿足上面一些條件時(shí)候
替代繼承解決復(fù)用需求的解決方案
對(duì)于這樣的問題,業(yè)界其實(shí)早就給出了解決方案:用組合替代繼承,通過定義好的接口進(jìn)行交互,一般來說可以選擇Delegate模式來交互。
協(xié)議
兩個(gè)類并沒有太多共用的代碼,它們只不過具有相同的接口。如果這樣的話,使用協(xié)議可能會(huì)是更好的方案
舉個(gè)例子~
假設(shè)一個(gè)APP有播放器(player)對(duì)象,它擁有播放(play)方法播放視頻,如果APP希望支持YouTube,需要相同幾個(gè)播放(player)接口,
使用繼承實(shí)現(xiàn)代碼如下
<pre>
@interface Player : NSObject
- (void)play;
- (void)pause;
@end
@interface YouTubePlayer : Player
@end
</pre>
使用協(xié)議方法代碼如下
<pre>
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player : NSObject <VideoPlayer>
@end
@interface YouTubePlayer : NSObject <VideoPlayer>
@end
</pre>
代理
如果當(dāng)我們類中的方法需要些自定義的行為時(shí)候,我們可以使用代理優(yōu)雅解決這個(gè)實(shí)現(xiàn)
舉個(gè)例子~
再以上面的例子為例,player對(duì)象希望在播放的時(shí)候執(zhí)行一些自定義的行為,使用繼承也可以輕易的實(shí)現(xiàn):創(chuàng)建個(gè)player對(duì)象的子類,然后重寫play方法,再調(diào)用[super play],再跟著希望執(zhí)行的行為。但是我們也可以通過的代理的方式更有優(yōu)雅的實(shí)現(xiàn)這個(gè)需求
<pre>
@class Player;
@protocol PlayerDelegate
- (void)playerDidStartPlaying:(Player *)player;
@end
@interface Player : NSObject
@property (nonatomic,weak) id<PlayerDelegate> delegate;
- (void)play;
- (void)pause;
@end
</pre>
現(xiàn)在在player對(duì)象的play方法里,我們可以通過代理屬性調(diào)用 playerDidStartPlaying:方法,任何使用Player類的對(duì)象,可以遵守代理協(xié)議,就可以實(shí)現(xiàn)自定義的playerDidStartPlaying:方法了,player類依然保持它的通用性和獨(dú)立性,方便為對(duì)外提供服務(wù)。代理是非常強(qiáng)大技巧,蘋果本身就經(jīng)常使用。像 UITextField這樣的類,有時(shí)候你還會(huì)想把幾個(gè)不同的方法分組到幾個(gè)單獨(dú)的協(xié)議里,比如UITableView—— 它不僅有一個(gè)代理(delegate),還有一個(gè)數(shù)據(jù)源(dataSource)。
類別
我們有時(shí)候會(huì)給對(duì)象添加方法,通過繼承的方式當(dāng)然可以實(shí)現(xiàn),但是不如category的方式來的方便和容易使用,不增加新的類,可復(fù)用的價(jià)值也更高
注意:
1> 在用類別擴(kuò)展一個(gè)不是你自己的類的時(shí)候,在方法前添加前綴是個(gè)比較好的習(xí)慣做法。如果不這么做,有可能別人也用類別對(duì)此類添加了相同名字的函數(shù)。那時(shí)候程序的行為可能跟你想要的并不一樣,未預(yù)期的事情可能會(huì)發(fā)生。
2> 使用類別還有另外一個(gè)風(fēng)險(xiǎn),那就是,到最后你可能會(huì)使用一大堆的類別,連你自己都會(huì)失去對(duì)代碼全局的認(rèn)識(shí)。假如那樣的話,創(chuàng)建自定義的類可能更簡(jiǎn)單一些。
組合
Casa提到我們盡可能用組合替代繼承。組合是最強(qiáng)大的替代繼承的選項(xiàng)。如果你想復(fù)用已經(jīng)存在的代碼,并且不想共享同樣的接口,組合是最佳選擇
舉個(gè)例子~
假設(shè)你要設(shè)計(jì)一個(gè)緩存類
<pre>
@interface OBJCache : NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
</pre>
一個(gè)簡(jiǎn)單的做法就通過繼承NSDictionary并且通過調(diào)用字典的方法來實(shí)現(xiàn)這上面兩個(gè)緩存方法。
<pre>
@interface OBJCache : NSDictionary
</pre>
但是這樣做會(huì)帶來一些問題。它本來是應(yīng)該被詳細(xì)實(shí)現(xiàn)的,但只是通過字典來實(shí)現(xiàn)?,F(xiàn)在,在任何需要一個(gè) NSDictionary 參數(shù)的時(shí)候,你可以直接提供一個(gè) OBJCache 值。但如果你想把它轉(zhuǎn)為其它完全不同的東西(例如你自己的庫(kù)),你就可能需要重構(gòu)很多代碼了。
更好的方式就是組合了。創(chuàng)建一個(gè)緩存類,并將添加一個(gè)字典的私有屬性,對(duì)外還是暴露著兩個(gè)接口,實(shí)現(xiàn)的時(shí)候就可以通過調(diào)用字典屬性的方法實(shí)現(xiàn)我們使用字典的方法了,這樣做可以靈活改變其涉嫌,而該類的使用者就不用進(jìn)行重構(gòu)。
總結(jié)
代碼復(fù)用,盡管他們都可以通過繼承實(shí)現(xiàn),但是我們?yōu)榱嗽跊]有耦合需求的時(shí)候盡量不要使用繼承,而是根據(jù)不同場(chǎng)景采用不同復(fù)用代碼的方式。如果只是共享接口,我們可以使用協(xié)議;如果是希望共用一個(gè)方法的部分實(shí)現(xiàn),但希望根據(jù)需要執(zhí)行不同的其他行為,我們可以使用代理;如果是添加方法,我們可以優(yōu)先使用類別(category);如果是為了使用一個(gè)類的很多方法,我們可以使用組合來實(shí)現(xiàn)。,如果當(dāng)初只是出于代碼復(fù)用的目的而不區(qū)分類別和場(chǎng)景,就采用繼承是不恰當(dāng)?shù)?。?dāng)你發(fā)現(xiàn)你的繼承超過2層的時(shí)候,你就要好好考慮是否這個(gè)繼承的方案了,第三層繼承正是濫用的開端。確定有必要之后,再進(jìn)行更多層次的繼承。我認(rèn)同Casa的看法:萬不得已不要用繼承,優(yōu)先考慮組合等方式。