iOS 類的抽象和繼承:類族與工廠模式~詳解
//聯絡人:石虎 QQ: 1224614774暱稱:嗡嘛呢叭咪哄
一、Objective-C類族和工廠模式
/**
連結http://blog.csdn.net/shihuboke/article/details/72817169 */在iOS的系統類庫中也有一種方式使得開發者不必關注類中具體的儲存實現,但可以根據不同需求場景創建出合適的物件來。比如Foudation中的NSArray、UIkit中的UIButton。
本文結合幾種工廠設計模式的原理,對Objective-C類族的概念做一下簡要的整理。
0. 三種工廠
其實除了《Design Patterns》中提到的
在簡單工廠中,產品有一個統一的interface,而可以有不同implementation,同時有一個(通常是僅有一個)工廠物件。需要產品的時候,工廠會根據已有條件做switch,選擇一種產品實現,構建一個例項出來。這種設計將產品的抽象和實現分離開來,可以針對統一的產品介面進行通訊,而不必特意關注具體實現的不同。對於產品類別的擴充也是可以的,但每增加一個產品,都需要修改工廠的邏輯,有一定維護成本。
工廠方法更進一步,將工廠也抽象出來,進行介面、實現分離。這樣具體工廠和具體產品可以對應著同時擴充,而不需要修改現有邏輯。當然,使用者也許在不同場景要在一定程度上自己對應的工廠選擇(這個總要有人知道,不可避免)。
抽象工廠相對於工廠方法主要是對整個產品類的體系進行了橫向擴充,構成一個更為完整的系統。
1. Objective-C的類族(Class Cluster)
做iOS開發的朋友們一定用過NSNumber的numberWith…方法。但大家有可能都不知道NSNumber這樣的方法呼叫返回的不是NSNumber類本身的物件,這正是Objective-C類族的微妙之處。
如上圖所示,Number的概念很大。而實際上NSNumber實際上是有很多隱藏的子類的,而我們通過NSNumber的numberWith…方法得到的物件正是其子類的物件,但對於使用者幾乎可以不必知道這一點,只要知道它是一個NSNumber
“Simple Concept and Simple Interface”,這正是蘋果設計類族的初衷,也是類族的優點所在。可以想象我們要用整數作為引數拿到一個NSNumber物件和一個布林值引數拿到的NSNumber物件是不同的,這略微有些類似於switch邏輯(雖然是通過不同的方法),根據不同的條件提供不同的子類物件,而這一切都集中宣告在公共介面類NSNumber中。我們很容易聯想到上面提到的Simple Factory(簡單工廠)設計模式。
沒錯,與簡單工廠類似,類族的一個缺點也顯現出來,那就是已有的類族不好擴充套件。比如你想NSNumber再多支援一種情況,這個恐怕很難。好在這些系統的類庫已經將大部分可能都做進去了,考慮得比較完善,通常你只是去用就可以了。
2. 類族的子類擴充套件
瞭解了類族的概念,我們在實際開發當中也可以採用其方式,利用其優點。上面提到對已有類族進行子類擴充套件是很難的,但這不代表NSNumber、NSArray等類就沒法繼承了。他們還是可以有自定義的子類的。
既然要做類族的子類,就要做到:
· 以公共“抽象”類為父類,比如NSNumber、NSArray等,而非其子類
· 提供自定義儲存
· 重寫(覆蓋)父類所有初始化方法
· 重寫父類中“原始”方法
其中第二點最重要,系統的類族通常在父類中只是提供了各種方法宣告,而自身並不提供儲存,所以要自定義子類一定要提供自己的儲存,一般情況下這也是自定義子類的意義所在。
重寫初始化方法,要遵從Objective-C初始化鏈的規範。
而重寫“原始”方法,這個要說一下。按照蘋果的文件,和指定初始化方法形式類似,這些類裡面的眾多方法可以分為兩類,“原始”方法和“衍生”方法。“原始”方法定義了這個類及物件的最基本行為,而“衍生”方法則基於這些“原始”方法進行更復雜邏輯的包裝。所以,重寫了“原始”方法,“衍生”方法也自然效果就不同了。
除了自定義子類外,蘋果官方更建議開發者用組合的方式對類族類進行包裝。
3. 物件所屬類的判斷
有人會問,如果我沒有特殊需求,不需要寫NSArray、NSNumber的子類,是不是瞭解類族就沒有多大意義了。這裡記一下,通過了解類族概念,我們至少知道了,通過NSNumber得到的物件,不一定是(基本上就不會是)NSNumber類本身的物件。
可以試驗下,通過[NSNumber numberWithInt:2]和[NSNumber numberWithBool:YES]得到的物件對應的類,一個是__NSCFNumber,另一個是__NSCFBoolean。
那麼,如下這樣的判斷就不行了:
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
// Will never be hit
}
需要在適當的情況下選擇使用isMemberOfClass和isKindOfClass。
“類族”(class cluster)是一種很有用的模式(pattern),可以隱藏“抽象基類”(abstract base class)背後的實現細節。Objective-C的系統框架中普遍使用此模式,比如iOS的使用者介面框架(user interface framework)UIKit中就有一個名為UIButton的類,想建立按鈕,需要呼叫下面這個“類方法”(class method):
+ (UIButton*)buttonWithType:(UIButtonType)type;
該方法所返回的物件,其型別取決於傳入的按鈕型別(button type),然而,不管返回什麼型別的物件,它們都繼承自同一個基類:UIButton。這麼做的意義在於:UIButton類的使用者無須關心創建出來的按鈕具體屬於哪個子類,也不用考慮按鈕的繪製方式等實現細節,使用者只需明白如何建立按鈕,如何設定像“標題”(title)這樣的屬性,如何增加觸控動作的目標物件等問題就好。
建立類族
現在舉例來演示如何建立類族,假設有一個處理僱員的類,每個僱員都有“名字”和“薪水”這兩個屬性,管理者可以命令其執行日常工作,但是,各種僱員的工作內容卻不同,經理在帶領僱員做專案時,無須關心每個人如何完成其工作,僅需指示其開工即可。
首先要定義抽象基類:
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance,
};
@interface EOCEmployee : NSObject
@property (copy) NSString *name;
@property NSUInteger salary;
// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
// Make Employees do their respective day's work
- (void)doADaysWork;
@end
@implementation EOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
return [EOCEmployeeDesigner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
// Subclasses implement this.
}
@end
每個“實體子類”(concrete subclass)都從基類繼承而來,例如:
@interface EOCEmployeeDeveloper : EOCEmployee
@end
@implementation EOCEmployeeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
在本例中,基類實現了一個“類方法”,該方法根據待建立的僱員類別分配好對應的僱員類例項,這種“工廠模式”(Factory pattern)是建立類族的辦法之一。
可惜Objective-C這門語言沒辦法指明某個基類是“抽象的”(abstract),於是,開發者通常會在文件中寫明類的用法,這種情況下,基類介面一般都沒有名為init的成員方法,這暗示該類的例項也許不應該由使用者直接建立。
Cocoa裡的類族
系統框架中有許多類族,大部分collection類都是類族,如NSArray,下面這種程式碼:
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
// Will never be hit
}
其中的if語句永遠不可能為真。[maybeAnArray class]所返回的類絕不可能是NSArray類本身,因為由NSArray的初始化方法所返回的那個例項其型別是隱藏在類族公共介面(public facade)後面的某個內部型別(internal type)。
要判斷出某個例項所屬的類是否位於類族之中,應該使用型別資訊查詢方法(introspection method),不要直接檢測兩個“類物件”是否等同,而應該採用下列程式碼:
id maybeAnArray = /* ... */;
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
// Will be hit
}