类的抽象和继承:类族与工厂模式
“类族”(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 }