為避免撕逼,提前宣告:本文純屬翻譯,僅僅是為了學習,加上水平有限,見諒!
【原文】https://www.objc.io/issues/13-architecture/singletons/
iOS子類化——by Chris Eidhof
這篇文章和我通常寫的文章有所不同。它不是一個指南更像是一系列想法和模式。我將要講述的差不多所有模式都是通過犯錯誤這樣艱難的方式找出來的。絕不是說我就是子類化方面的權威,但是,我只是想把我學到的一些事情分享出來。不要把它作為一個權威指南,而是一個例子集合。
當被問及物件導向程式設計時(OOP
),Alan Kay(創造者)寫到,他不是關於類的,而是訊息傳遞^?。 但,仍然有很多人關注於建立類層次。在這篇文章中,我們將看幾個有用的案例,但是我們主要關注建立複雜的類層次結構。在我們的經驗中,這會讓程式碼更加簡單,更容易維護。關於這個話題有很多文章,你可以在Clean Code和Code Complete這樣的書中找到,推薦大家閱讀這兩本書。
什麼時候子類化
首先,讓我們討論一下在哪些情況下建立子類是有意義的。如果你正在構建一個有自定義佈局的UITableViewCell
,那麼你需要建立一個子類。幾乎所有的觀點都是如此;一旦你開始佈局,把它移到子類裡就變得有意義了,這樣你不僅優雅的整理了你的程式碼,還讓程式碼在整個專案都可複用。
假設,你的程式碼是針對不同的平臺和版本的,你需要為每個平臺和版本編寫自定義的部分。這時建立一個OBJDevice
類就有意義了,它可以子類化出OBJIphoneDevice
和OBJIPadDevice
,甚至可以更深層的子類化出OBJIPhone5Device
,它們可以重寫特定的方法。例如,你的OBJDevice
類可以包含applyRoundedCornersToView:withRadius:
方法。它有一個預設的實現,但是這個方法可以被特定的子類重寫。
另外一種非常有用的子類化情形是在模型物件(model object
)中。絕大多數時候,模型物件從類中繼承的實現方法有isEqual:
、hash
、copyWithZone:
和description
。這些方法通過對屬性迭代實現一次,很難出錯。(如果你想找這樣的基類,可以考慮用Mantle,它正事這樣做的,而且不僅僅如此。)
什麼時候不子類化
我參與過很多的專案,我也見過深層次的子類化。很慚愧我也這樣做過。除非層次很淺,否則很快就會達到極限。
幸運的是,如果你發現自己正處於這樣深的層次,這有很多可選方案。在下面的小節中,我們會深入分析每一部分。如果你的子類只是共享相同的介面和協議,這是一個很好的選擇。如果你知道一個物件需要修改很多地方,你可能需要使用委託動態的改變和配置它。當你想要對已存在的物件擴充套件一些簡單地功能時,類別(category
)可能是一個選擇。當你有一系列子類,每一個都重寫(override
)同樣的方法,你可能會使用配置物件。最後,當你想重用一些功能時,最好組合多個物件而不是擴充套件他們。
可選方案
方案:協議
通常的,一個使用子類的原因是你想要保證一個物件響應一個確定的訊息。設想一下,在一個應用中你有一個可以播放視訊的播放器(player
)物件。現在,如果你想增加YouTobe
的支援,你需要同樣的介面,但是不同的實現。一種是用子類實現的方式如下:
@interface Player: NSObject
- (void)play;
- (void)pause;
@end
@interface YouTobePlayer: Player
@end
複製程式碼
也許,這兩個類並沒有共享太多程式碼,僅僅是是相同的介面。在這種情況下,使用協議可能是一個更好的解決方案。使用協議,你可能會寫下如下程式碼:
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player: NSObjet <VideoPlayer>
@end
@interface YouTobePlayer: NSObject <VideoPlayer>
@end
複製程式碼
這種情況下,YouTobePlayer
不需要知道Player
的內部情況。
方案:委託
再一次假設,你有一個像上面例子中那樣的Player
類。現在,在一個地方,你想要在paly
方法中執行一個自定義動作。做到它相當的容易:你可以建立一個自定義子類,重寫paly
方法,呼叫[super paly]
,然後執行自定義的功能。這是一種處理方式。另外一種是改變Player
物件並給他一個委託。例如:
@class Player;
@protocol PlayerDelegate
- (void)playerDidStartPlaying:(Player *)player;
@end
@interface Player: NSObject
@property (nonatomic, weak) id<PlayerDelegate> delegate;
- (void)play;
- (void)pause;
@end
複製程式碼
現在,在播放器的play
方法中,委託獲得了playerDidStartPlaying:
訊息。這個類的任何使用者只需實現委託協議而不需要建立子類,並且Player
物件依然保持著通用性。這是一個非常強大的技術,蘋果在他們自己的框架中大量的使用了該技術。有時,你想把不同的方法組織到不同的協議中,如UITableView做的那樣,它不僅有委託還有資料來源。
方案:類別(Category)
有時,你可能想對一個物件進行一點功能擴充套件。假設,你想向NSArray中擴充套件一個arrayByRemovingFirstObject
方法。你可以把它放入類別中,而不是建立一個子類。像這樣:
@interface NSArray (OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
複製程式碼
當使用類別擴充套件一個不是你自己建立的類時,在方法前加字首是一個很好的做法。如果不這樣做,其他人可能會使用同樣的技術實現同樣的一個方法。然後,如果行為不匹配,可能會有意想不到的事情發生。
使用類別的危險之一是,你的專案可能最終會使用大量的類別,這樣你可能會丟失你的概述(you can lose your overview.)。這種情況下,建立一個自定義類可能更簡單。
方案:配置物件
我一直在犯的一個錯誤(現在可以很快的意識到)是:會建立一個帶有很多抽象方法的類,然後很多子類都會重寫一個特定的方法。例如,在一個演示應用程式,你可能會一個Theme
類,他有幾個屬性,如:backgroundColor
和font
,還有一些在幻燈片上放置東西的邏輯。
然後,對於每一個主題,你建立一個Theme
的子類,重寫一個方法(如:setup
),並配置屬性。直接使用子類沒有意義。這種情況下,你可以使用配置物件讓你的程式碼更加簡單。你可以把共享邏輯放在Theme
類中(如:幻燈片佈局),但是把配置放在一個僅僅有屬性的簡單物件中。
例如,一個ThemeConfiguration
類有backgroundColor
和font
屬性,並且Theme
類在初始化的時候獲取一個這個類的例項。
方案:組合
最強大的子類化的替代方案是組合。如果你想重用已存在的程式碼但是沒有共享同樣的介面,組合是你可以選擇的“武器”。例如,假設你正在設計一個快取類:
@interface OBJCache: NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCacheValueForKey:(NSString *)key;
@end
複製程式碼
達到此目的的一個簡單方式是子類化NSDictionary
並且通過呼叫字典方法實現這兩個方法。
@interface OBJCache: NSDictionary
複製程式碼
然而,這存在幾個缺點。用字典實現的事實應該是一個實現細節。現在,在任何需要一個NSDictionary
型別引數的地方,你都可以提供一個OBJCache
值。如果你想換一種完全不同的東西(例如,你自己的類庫),你需要重構很多程式碼。
一個好的方式是把這個字典存放到一個私有屬性中(或者例項變數中),並且僅僅暴露出兩個cache
方法。現在,你可以維持靈活性,當你獲取更多的見解時可以隨意修改實現,並且類的使用者不需要進行重構。