Effective Objective-C2.0 笔记(九)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_28285625/article/details/89248940

第9条:以“类族模式”隐藏实现细节

“类族”(class cluster 也叫族簇)是一种很有用的模式,可以隐藏“抽象基类”(abstract base class)背后的实现细节。Objective-C的系统框架普遍使用此模式。比如,iOS用户界面框架(user interface framework)UIKit中就有一个名为UIButton的类。想创建按钮,需要调用下面这个类方法:

+ (instancetype)buttonWithType:(UIButtonType)buttonType;

该方法返回的对象,其类型取决于传入的按钮类型(button type)。然而,不管返回什么类型的对象,它们都继承自同一个基类:UIButton.这么做的意义在于:UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。
使用者只需要明白如何创建按钮,如何设置像“title”这样的属性,如何增加触摸动作的目标对象就好。
回到开头说的问题上,我们可以把各种按钮的绘制逻辑都放在一个类里,并根据按钮类型切换:

- (void)drawRect:(CGRect)rect {
    // Drawing code
    if(_type == ButtonTypeA){
        //Draw TypeA button
    }else if (_type == ButtonTypeB){
         //Draw TypeB button
    }
}

这样写看上去还算简单,然而,若是需要根据按钮类型来切换的绘制方法有许多种,那么就会变得很麻烦了。优秀的程序员会将这种代码重构为多个子类,把各种按钮所用的绘制方法放到相关子类中去。不过,这么做需要用户知道各种子类才行,此时应该使用“类族模式”,该模式可以灵活应对多个类,把它们的实现细节隐藏在抽象基类后面,以保持接口简洁,用户无须自己创建子类实例,只需要调用基类方法来创建即可。

创建类族

现在来演示如何创建类族。假设 有一个处理雇员的类,每个雇员都有“name”和“salary”这两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容却不同。经理在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。
首先要定义抽象基类:

typedef enum :NSUInteger{
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
    
}EOCEmployeeType;
NS_ASSUME_NONNULL_BEGIN

@interface EOCEmployee : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger salary;
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADayWork;
@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)doADayWork{
    //Subclasses implement this.
}
@end

每个实体类(concrete subclass)都是从基类继承而来。例如:

@interface EOCEmployeeDeveloper : EOCEmployee
@end

@implementation EOCEmployeeDeveloper
- (void)doADayWork{
    NSLog(@"write code");
}
@end

实体子类一词中“concrete”与“抽象基类”一词中的“abstract”相对,意思是“非抽象的,可以实例化的”,也可以译为“具体”,“具象”。

在本例中基类实现了一个“类方法”,该方法根据待创建的雇员类分配好对应的雇员类实例。这种“工厂模式(Factory pattern)”是创建类族的方法之一。
可惜Objective-C这门语言没办法指明某个基类的“抽象的”(abstract)。于是,开发者常会在文档中写明类的用法。这种情况下,基类接口一般都没有名为init的成员方法,这暗示该类的实例也许不应该由用户直接创建。还有一种方法可以确保用户不会使用基类实例,那就是在基类的doADayWork方法中抛出异常。然而这种做法相当极端,很少有人用。
如果对象所属的类位于某个类族中,那么查询其类型信息(introspection)时就要当心了。你可能觉得自己创建了某个类的实例,然而创建的却是其子类的实例,在Employee这个例子中,[employee isMemberOfClass[EOCEmployee class]]似乎会返回yes,但是实际上返回的确实NO,因为employee并非Employee类的实例,而是某个子类的实例。

introspection是type introspection的简称,是某些面向对象语言可以在运行时检视对象类型与属性的一种功能。中文译作“内省”或“类型内省”。

Cocoa里的类族

系统框架中有许多类族。大部分collection类都是类族(作者有时把“类族中的抽象基类”直接成为类族。这句话实际上说:大部分collection类都是某个类族中的抽象基类。),例如NSArray与其可变版本NSMutableArray。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但是仍然可以合起来算作一个类族。(在传统类族模式中,通常只有一个类具备“public interface”,这个类就是类族中的抽象基类。)。
像NSArray这样的类的背后其实是类族(对于大部分collection类都是这样的),明白这一点很重要,否则就会写出下面的代码:

id array = /*...*/;
if([array isMemberOfClass:[NSArray class]]){
       //will never be hit
    }

你要是知道NSArray是个类族,那就会明白上诉代码错在哪里:其中if语句永远不可能为真。[array class]所返回的类绝不可能是NSArray类本身。不过仍然有办法可以判断某个实例所属的类是否位于本类族之中:

id array = /*...*/;
 if([array isKindOfClass:[NSArray class]]){
     //will be hit
  }

我们经常需要向类族中新增实体子类,不过这么做的时候得留心。在Employee这个例子中,若是没有“工厂方法”的源代码,那就无法向其中新增雇员类别了。然而对于Cocoa中的NSArray这样的类族来说,还是有办法新增子类的,但是需要新增几条规则:

  • 子类应该继承自类族中的抽象基类。
    若要编写NSArray子类时,则需令其继承自不可变数组中的基类或可变数组的基类。
  • 子类应该定义自己数据的存储方式。
    开发中编写NSArray子类时,经常在这个问题上受阻。子类必须用一个实例变量来存放数组中的对象。这似乎和大家预想的不同,我们以为NSArray自己肯定会保存那些对象,但是大家要记住,NSArray本身只不过是包在其他隐藏对象外卖的壳,它仅仅定义了所有数组都具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存实例。
  • 子类应该覆写超类文档中指明需要覆写的方法。
    每个抽象基类中,都有一些子类必须覆写的方法。不如说,想要编写NSArray的子类,就需要实现count际“objectAtIndex:”方法。向lastObject这种方法则无须实现,因为基类可以根据前两个方法实现除这个方法。

要点

  • 类族模式可以实现细节隐藏在一套简单的公共接口后面。
  • 系统框架中经常使用类族。
  • 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

猜你喜欢

转载自blog.csdn.net/qq_28285625/article/details/89248940