什么是類族
"類族"是一種很有用的模式(pattern),可以隱藏"抽象基類"背后的實現細節.
比如UIKit框架中的UIButton類.想創建按鈕,需要調用下面這個"類方法":
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
該方法返回的對象,其類型取決于傳入的按鈕類型(button type).然而,不管返回什么類型的對象,他們都繼承自同一個基類:UIButton.這么做的意義在于:UIButton類的使用者無須關心創建出來的按鈕具體屬于哪個子類,也不用考慮按鈕的繪制方式等實現細節.使用者只需要明白如何創建按鈕,如何設置"標題"(title)這樣的屬性,如何增加觸摸動作的目標對象等問題就好.
- (void)drawRect:(CGRect)rect {
if (_type == TypeA) {
//Dram TypeA button
} else if (_type == TypeB) {
//Draw TypeB button
}
}
我們可以像上面代碼寫的那樣,把各種按鈕的繪制邏輯都放在一個類里,并根據按鈕類型來切換.
但是如果需要依按鈕類型來切換的繪制方法有許多種,那么就會變得麻煩了.
這時,比較好的做法是把各種按鈕所用的繪制方法放到相關子類中去.但是這樣做對使用這個類的用戶來說會有一個問題,就是他可能不知道這個類的子類有哪幾個,更不用說去使用了.
此時應該使用"類族模式",該模式可以靈活應對多個類,將它們的實現細節隱藏在抽象基類后面,以保持接口簡潔.用戶無需自己創建子類實例,只需要用基類方法來創建即可.
創建類族
假設有一個處理雇員的類,每個雇員都有"名字"和"薪水"這兩個屬性,管理者可以命令其執行日常工作.但是,各雇員的工作內容卻不同.經理在帶領雇員做項目時,無須關心每個人如何完成其工作,僅指示其開工即可.
定義抽象基類EOCEmployee
EOCEmployee.h
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance
};
@interface EOCEmployee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
//Helper for creating Employee objects
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
//Make Employees do their respective day's work
- (void)doDaysWork;
@end
EOCEmployee.m
@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
定義EOCEmployee的子類,以EOCEmployeeDeveloper為例
EOCEmployeeDeveloper.h
@interface EOCEmployeeDeveloper : EOCEmployee
@end
EOCEmployeeDeveloper.m
@implementation EOCEmployeeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
在本例中,基類實現了一個"類方法",該方法根據待創建的雇員類別分配好對應的雇員實例.這種"工廠模式"是創建類族的辦法之一.
在OC這門語言當中沒辦法指明某個基類是"抽象的".于是,開發者通常會在文檔中寫明類的用法.這種情況下,基類接口一般沒有名為init的成員方法,這暗示該類的實例也許不應該由用戶直接創建.
還有一種辦法可以確保用戶不會使用基類實例,那就是在基類的doADaysWork方法中拋出異常.然而這種做法相當極端,很少有人用.
如果對象所屬的類位于某個類族中,那么在查詢其內心信息時就要當心了.你可能覺得自己創建了某個類的實例,然而實際上創建的卻是其子類的實例.
在Employye這個例子中,[employye isMemberOfClass:[EOCEmployee class]]會返回NO,因為employye并非EOCEmployee類的實例,而是其某個子類的實例.
Cocoa里的類族
系統框架中有許多類族.大部分collection類都是類族,例如NSArray與其可變版本NSMutableArray.
id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
// Will never be hit
}
上面這段代碼if語句永遠不可能為真.[maybeAnArray class]所返回的類絕不可能是NSArray本身,因為由NSArray的初始化方法所返回的那個實例其類型是隱藏在類族公共接口后面的某個內部類型.
如果我們想判斷某個對象是否位于類族中,不要直接檢測兩個"類對象"是否相同,而應該采用下面的代碼:
id maybeAnArray = /* ... */;
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
// Will be hit
}
我們經常需要向類族中新增實體子類,不過在Employee這個例子中,若是沒有"工廠方法"的源代碼,那就無法向其中新增雇員類別了.
然而對于Cocoa中NSArray這樣的類族來說,還是有辦法新增子類的,但是要遵守幾條規則
- 子類應該繼承自類族中的抽象基類
若要編寫NSArray類族的子類,則需令其繼承自不可變數組的基類或可變數組的基類. - 子類應該定義自己的數據存儲方式
開發者編寫NSArray子類時,經常在這個問題上受阻.子類必須用一個實例變量來存放數組中的對象.我們以為NSArray自己肯定會保存那些對象,所以在子類中就無須再存一份了.但是NSArray本身只不過是包在其他隱藏對象外面的殼,它僅僅定義了所有數組都需要具備的一些接口.對于這個自定義的數組子類來說,可以用NSArray來保存其實例. - 子類應當覆寫超類文檔中指明需要覆寫的方法.
在每個抽象基類中,都有一些子類必須覆寫的方法.比如說,想要編寫NSArray的子類,就需要實現count及"objectAtIndex:"方法.像lastObject這種方法則無需實現,因為基類可以根據前兩個方法實現出這個方法.
在類族中實現子類時所需遵守的規范一般都會定義于基類的文檔之中,編碼前應該先看看.
參考文獻:[1]Matt Galloway.Effective Objective-C 2.0[M].北京:機械工業出版社, 2015: 35-39