iOS中子類(lèi)的理解

這篇文章跟我以往的文章有點(diǎn)不一樣。它主要是一些思想與模式的匯集,而不是一篇指南。下面我所寫(xiě)的模式幾乎全都來(lái)之不易,都是我犯了錯(cuò)之后才學(xué)到的。我并不認(rèn)為自己是子類(lèi)方面的權(quán)威,但我確實(shí)想把我學(xué)到的一些東西分享出來(lái)。別把本文當(dāng)做權(quán)威指南,它只是一些例子的匯集。

在被問(wèn)到 OOP(面向?qū)ο缶幊蹋┑臅r(shí)候,Alan Kay(OOP 的發(fā)明人)寫(xiě)到:

它跟類(lèi)無(wú)關(guān),但跟消息有關(guān)。

然而,很多人的關(guān)注點(diǎn)仍然還在類(lèi)層次上。在本文中,我們會(huì)看幾個(gè)我們可能會(huì)把注意力放在創(chuàng)建復(fù)雜的類(lèi)結(jié)構(gòu)上的例子,并給出更有用的替代方案。根據(jù)經(jīng)驗(yàn),這樣會(huì)讓代碼更簡(jiǎn)單,更易維護(hù)。

何時(shí)用子類(lèi)

首先,我們討論幾種使用子類(lèi)比較合適的場(chǎng)景。

  • 如果你要寫(xiě)一個(gè)自定義布局的 UITableViewCell ,那就創(chuàng)建一個(gè)子類(lèi)。這同樣適用于幾乎每個(gè)視圖。一旦你開(kāi)始布局,把這塊代碼放入子類(lèi)就更合理一些,不光代碼得到了更好的封裝,你也能得到一個(gè)可在工程之間重用的組件。

  • 假設(shè)你的代碼是針對(duì)多平臺(tái)多版本的,并且你需要針對(duì)每個(gè)平臺(tái)每個(gè)版本寫(xiě)一些代碼。這時(shí)候更合理的做法可能是創(chuàng)建一個(gè) OBJDevice 類(lèi),讓一些子類(lèi)如 OBJIPhoneDevice 和 OBJIPadDevice ,甚至更深層的子類(lèi)如 OBJIPhone5Device 來(lái)繼承,并讓這些子類(lèi)重寫(xiě)特定的方法。例如,你的 OBJDevice 類(lèi)可能包含了函數(shù) applyRoundedCornersToView:withRadius ,它有一個(gè)默認(rèn)的實(shí)現(xiàn),但是也能被特定的子類(lèi)重寫(xiě)。

  • 另一個(gè)子類(lèi)化可能很有用的場(chǎng)景是模型對(duì)象(model object)。絕大多數(shù)情況下,我的模型對(duì)象繼承自一個(gè)實(shí)現(xiàn)了 isEqual: 、 hash 、 copyWithZone: 和 description 等方法的類(lèi)。這些方法只被實(shí)現(xiàn)一次,并且迭代循環(huán)遍歷所有屬性,所以極不容易出錯(cuò)。(如果你也想找一個(gè)這樣的基類(lèi),可以考慮使用 Mantle ,它就是這么做的,并且做得更多。)

何時(shí)不使用子類(lèi)

在以往工作過(guò)的很多工程中,我見(jiàn)到過(guò)很多繼承層次很深的子類(lèi)。當(dāng)我也這么干的時(shí)候,總會(huì)感到內(nèi)疚。除非繼承的層次非常淺,否則你會(huì)很快發(fā)現(xiàn)它的局限性。

幸運(yùn)的是,如果你發(fā)現(xiàn)自己正在使用深層次的繼承,還有很多替代方案可選。在下面的章節(jié)中,我們會(huì)逐個(gè)進(jìn)行更詳細(xì)地描述。如果你的子類(lèi)只是使用相同的接口,協(xié)議會(huì)是個(gè)非常好的替代方案。如果你知道某個(gè)對(duì)象需要大量的修改,你可能會(huì)使用代理來(lái)動(dòng)態(tài)改變和配置它。當(dāng)你想給已有對(duì)象增加一些簡(jiǎn)單功能時(shí),類(lèi)別可能是個(gè)選擇。當(dāng)你有一堆重寫(xiě)了相同方法的子類(lèi)時(shí),你可以使用配置對(duì)象(configuration object)來(lái)代替。最后,當(dāng)你想重用某些功能時(shí),組合多個(gè)對(duì)象而不是擴(kuò)展它們可能會(huì)更好。

替代方案:協(xié)議(Protocols)

很多時(shí)候,使用子類(lèi)的原因是你想保證某個(gè)對(duì)象可以響應(yīng)某些消息。假設(shè)在 app 里你有一個(gè)播放器對(duì)象,它可以播放視頻?,F(xiàn)在你想添加對(duì) YouTube 的支持,使用相同的接口,但是具體實(shí)現(xiàn)不同。你可以使像這樣用子類(lèi)來(lái)實(shí)現(xiàn):

@class Player : NSObject

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

@end


@class YouTubePlayer : Player

@end

事實(shí)上可能這兩個(gè)類(lèi)并沒(méi)有太多共用的代碼,它們只不過(guò)具有相同的接口。如果這樣的話(huà),使用協(xié)議可能會(huì)是更好的方案。可以這樣用協(xié)議來(lái)寫(xiě)你的代碼:

@protocol VideoPlayer <NSObject>

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

@end


@class Player : NSObject <VideoPlayer>

@end


@class YouTubePlayer : NSObject <VideoPlayer>

@end

這樣,YouTubePlayer 類(lèi)就不必知道 Player 類(lèi)的內(nèi)部實(shí)現(xiàn)了。

替代方案:代理(Delegation)

再一次假設(shè)你有一個(gè)像上面例子中的 Player 類(lèi)?,F(xiàn)在,你想在開(kāi)始播放的時(shí)候在某個(gè)地方執(zhí)行一個(gè)自定義的函數(shù)。這么做相對(duì)容易一些:創(chuàng)建一個(gè)自定義的子類(lèi),重寫(xiě) play 方法,調(diào)用 [super play ],然后開(kāi)始做你自定義的工作。這么做是一種方法。另外一種方法是,改動(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 類(lèi)的任何使用者都可以?xún)H僅實(shí)現(xiàn)這個(gè)代理協(xié)議,而不用繼承該該類(lèi), Player 類(lèi)也能夠保持通用性。這是個(gè)強(qiáng)大有效的技術(shù),蘋(píng)果在自己的框架里大量地使用它。你想想像 UITextField 這樣的類(lèi),還有 NSLayoutManager。有時(shí)候你還會(huì)想把幾個(gè)不同的方法打包分組到幾個(gè)單獨(dú)的協(xié)議里,比如 UITableView—— 它不僅有一個(gè)代理(delegate),還有一個(gè)數(shù)據(jù)源(dataSource)。

替代方案:類(lèi)別(Categories)

有時(shí)候,你可能會(huì)想給一個(gè)對(duì)象增加一點(diǎn)點(diǎn)額外的功能。比如你想給 NSArray 增加一個(gè)方法 arrayByRemovingFirstObject。不用子類(lèi),你可以把這個(gè)函數(shù)放到一個(gè)類(lèi)別里。像這樣:

@interface NSArray (OBJExtras)

- (void)obj_arrayByRemovingFirstObject;

@end

在用類(lèi)別擴(kuò)展一個(gè)不是你自己的類(lèi)的時(shí)候,在方法前添加前綴是個(gè)比較好的習(xí)慣做法。如果不這么做,有可能別人也用類(lèi)別對(duì)此類(lèi)添加了相同名字的函數(shù)。那時(shí)候程序的行為可能跟你想要的并不一樣,未預(yù)期的事情可能會(huì)發(fā)生。

使用類(lèi)別還有另外一個(gè)風(fēng)險(xiǎn),那就是,到最后你可能會(huì)使用一大堆的類(lèi)別,連你自己都會(huì)失去對(duì)代碼全局的認(rèn)識(shí)。假如那樣的話(huà),創(chuàng)建自定義的類(lèi)可能更簡(jiǎn)單一些。

替代方案:配置對(duì)象(Configuration Objects)

在我經(jīng)常會(huì)犯的錯(cuò)誤中(現(xiàn)在很快就能發(fā)現(xiàn)了),其中有一條是:使用一個(gè)含有幾個(gè)抽象方法的類(lèi)并讓很多子類(lèi)來(lái)重寫(xiě)某個(gè)方法。例如,在一個(gè)幻燈片應(yīng)用里,你有一個(gè)主題類(lèi) Theme ,它有幾個(gè)屬性,比如 backgroundColor 和 font ,還有一些在一張幻燈片上如何布局的邏輯函數(shù)。

然后,對(duì)每種主題,你都創(chuàng)建一個(gè) Theme 的子類(lèi),重寫(xiě)某個(gè)函數(shù)(例如 setup )并配置其屬性。直接使用父類(lèi)對(duì)此做不了什么事。在這種情況下,你可以使用配置對(duì)象來(lái)讓代碼更簡(jiǎn)單些。你可以把共有的邏輯(比如幻燈片布局)放在 Theme 類(lèi)中,把屬性的配置放到較簡(jiǎn)單的對(duì)象中,這些對(duì)象中只含有這些屬性。

例如,類(lèi) ThemeConfiguration 具有 backgroundColor 和 font 屬性,而類(lèi) Theme 在其初始化函數(shù)中獲取一個(gè)配置類(lèi) ThemeConfiguration 的值。

替代方案:組合

組合是代替子類(lèi)化的最強(qiáng)大有效的方案。如果你想重用已有代碼而不想共享同樣的接口,組合就是你的首選武器。例如,假設(shè)你要設(shè)計(jì)一個(gè)緩存類(lèi):

@interface OBJCache : NSObject

- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;

@end

簡(jiǎn)單點(diǎn)的做法是直接繼承 NSDictionary,通過(guò)調(diào)用字典的函數(shù)來(lái)實(shí)現(xiàn)上面的兩個(gè)方法。

@interface OBJCache : NSDictionary
但是這么做有幾個(gè)弊端。它本來(lái)是應(yīng)該被詳細(xì)實(shí)現(xiàn)的,但只是通過(guò)字典來(lái)實(shí)現(xiàn)?,F(xiàn)在,在任何需要一個(gè) NSDictionary 參數(shù)的時(shí)候,你可以直接提供一個(gè) OBJCache 值。但如果你想把它轉(zhuǎn)為其它完全不同的東西(例如你自己的庫(kù)),你就可能需要重構(gòu)很多代碼了。

更好的方式是,將這個(gè)字典存在一個(gè)私有屬性(或者實(shí)例變量)中,對(duì)外僅僅暴露這兩個(gè) cache 方法?,F(xiàn)在,當(dāng)你有了更深入想法的時(shí)候,你可以在靈活地修改其實(shí)現(xiàn),而該類(lèi)的使用者們不用進(jìn)行重構(gòu)。

此文章原文鏈接自己的個(gè)人博客: www.koalaliu.com ,因簡(jiǎn)書(shū)平臺(tái)規(guī)范性以及用戶(hù)量,搬至簡(jiǎn)書(shū)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容