前言 |
Class Clusters是Foundation框架下广泛使用的设计模式 ,类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。(来自百度百科)
简单来说,类簇就是一个“工厂类”,它想外界提供了很多方法的调用接口,而方法的实现具体由内部的类来实现。当一个类簇要生成一个对象时,初始化方法会判断哪个“自己内部的类”最适合生成这个对象,然后这个“工厂”就会通过该类的初始化方法生成一个对象返回给调用者。这种由外层类提供统一抽象接口,而具体实现对外界隐藏并由内部类完成的设计模式称之为“抽象工厂”模式。就像这样:
id str = [NSString alloc];
NSString *str1 = [[NSString alloc] init];
NSString *str2 = [[NSString alloc] initWithFormat:@"string2"];
NSString *str3 = @"string3";
NSLog(@"str class: %@ str1 class: %@ str2 class: %@ str3 class: %@", NSStringFromClass([str class]), NSStringFromClass([str1 class]), NSStringFromClass([str2 class]), NSStringFromClass([str3 class]));
//打印结果:
//str class: NSPlaceholderString str1 class: __NSCFConstantString str2 class: NSTaggedPointerString str3 class: __NSCFConstantString
简单的概念,简单的接口 |
在官方文档中举了一个关于NSNumber的例子来说明类簇的好处:对于int、float、BOOL、char等的基本数据类型,有时我们需要将它们封装成类来使用,先假设没有NSNumber这个类,我们会怎么做呢?或许我们会想到将每一种基本的数据类型都封装成类,比如讲int封装成NSInt类,然后向外界暴露封装接口就OK了!乍一看没什么问题,而且概念也很简单,但是当需要支持的基本数据类型增多时,这些类也会越来越多。对程序员来说,需要记忆的类就越多,这无疑增加了负担,同时它们之间还有相同的数据特征,int可以转换成float,也可以转换成char。于是Apple就提出了一个人性化的设计——类簇,就像类簇的官方文档说的那样:没有类簇就是简单的概念,复杂的接口,有了类簇就是简单的概念,简单的接口。
就像使用NSNumber包装基本数据类型一样,我们只需要通过一个NSNumber类的一个方法就可以创建数据类型对应的对象,一个方法又可以还原成基本的数据,而不用关心内部的实现细节:
NSNumber *aChar = [NSNumber numberWithChar:’a’];
NSNumber *anInt = [NSNumber numberWithInt:1];
NSNumber *aFloat = [NSNumber numberWithFloat:1.0];
NSNumber *anInt = [NSNumber numberWithBool:YES];
//NSNumber就是暴露给外界的抽象父类,通过下面的代码可以知道对应的数据类型的对象的真实的类
NSLog(@"%@", NSStringFromClass([anInt class]));
//依次打印后所对应的NSNumber下的私有子类是:__NSCFNumber、__NSCFNumber、__NSCFNumber、__NSCFBoolean
多公共超类的类簇 |
NSNumber、NSValue都是单一超类的类簇,这只是类簇简单的应用。在Foundation框架下存在多公共超类的类簇,如下表:
Class cluster | Public superclasses |
NSData | NSMutableData |
NSData | |
NSArray | NSMutableArray |
NSArray | |
NSDictionary | NSMutableDictionary |
NSDictionary | |
NSString | NSMutableString |
NSString |
它们的不同在于所生成的实例对象是否可变,实际上就是对应的超类中是否存在改变对象大小的方法,如果尝试修改不可变的对象就会引起编译器的报错:该类型下没有可用的修改接口。
在类簇里面新建类 |
为了实现这个目标,需要完成以下几点:
- 继承类簇的抽象超类
- 定义自己的存储空间
- 重载父类的所有初始化方法
- 重载父类的原始方法
因为在类簇的层级中类簇的抽象超类是唯一公共可视的节点,这意味着新的子类将继承类簇的接口除了实例变量,因为抽象超类并没有定义。因此子类必须定义它所需要的任何实例变量。最后,子类必须重载继承的任何直接访问一个对象的实例变量的方法,这种方法称为原始方法。
一个类的原始方法形成其接口的基础,对于NSArray来说,通过count、objectAtIndex:这两个原始方法为基础就可以实现其衍生方法lastObject ([self objectAtIndex: ([self count] –1)])、containsObject:。
用原始方法和衍生方法划分接口使得创建子类更容易,你的子类必须重载继承的原始方法来保证继承的衍生方法能正确的执行。(以上译自官方文档)
接下来在看官方的例子:
//MonthArray.h
#import <Foundation/Foundation.h>
@interface MonthArray : NSArray
+ (MonthArray *)monthArray;
- (NSUInteger)count;
- (id)objectAtIndex:(NSUInteger)index;
@end
//MonthArray.m
#import "MonthArray.h"
@implementation MonthArray
static MonthArray *sharedMonthArray = nil;
static NSString *months[] = {@"January", @"February", @"March",
@"April", @"May", @"June", @"July", @"August", @"September",
@"October", @"November", @"December"};
+ (MonthArray *)monthArray {
if (!sharedMonthArray) {
//实现父类的初始化方法
sharedMonthArray = [[MonthArray alloc] init];
}
return sharedMonthArray;
}
- (NSUInteger)count {
return 12;
}
- (id)objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
[NSException raise:NSRangeException format:@"***%s: index (%lu)beyond bounds (%lu)", sel_getName(_cmd), (unsigned long)index,
[self count] - 1];
return nil;
} else {
return months[index];
}
}
//main.m
#import <Foundation/Foundation.h>
#import "MonthArray.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MonthArray *mon = [MonthArray monthArray];
NSLog(@"%@", [mon objectAtIndex:1]);
NSLog(@"%@", [mon lastObject]);
}
return 0;
}
@end
可以看到两个原始的方法能正常工作,同时没有重载的衍生方法对于MonthArray对象也能返回正常结果。
定义自己的类簇 |
假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”两个属性,管理者可以命令其执行日常工作。但是各种雇员的工作内容却不同。经理在带领雇员做项目时,无需关心每个人如何完成其工作,仅需指示其开工即可。(代码源自这里)
//Employee.h
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, EmployeeType) {
EmployeeTypeDesigner,
EmployeeTypeDeveloper,
EmployeeTypeFinance
};
@interface Employee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
+ (Employee *)employeeWithType:(EmployeeType)type;
- (void)doADaysWork;
@end
//Employee.m
#import "Employee.h"
@interface EmployeeDesigner : Employee
@end
@implementation EmployeeDesigner
- (void)doADaysWork {
NSLog(@"设计素材");
}
@end
@interface EmployeeDeveloper : Employee
@end
@implementation EmployeeDeveloper
- (void)doADaysWork {
NSLog(@"写代码");
}
@end
@interface EmployeeFinance : Employee
@end
@implementation EmployeeFinance
- (void)doADaysWork {
NSLog(@"计算财政收入");
}
@end
@implementation Employee
+ (Employee *)employeeWithType:(EmployeeType)type {
switch (type) {
case EmployeeTypeDesigner: {
return [[EmployeeDesigner alloc] init];
break;
}
case EmployeeTypeDeveloper: {
return [[EmployeeDeveloper alloc] init];
break;
}
case EmployeeTypeFinance: {
return [[EmployeeFinance alloc] init];
break;
}
}
}
- (void)doADaysWork {
NSLog(@"叫人做事");
}
@end
//main.m
#import <Foundation/Foundation.h>
#import "Employee.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Employee *employer = [Employee employeeWithType:EmployeeTypeDeveloper];
//抽象超类的实例一般不能被直接使用,而应该通过它所提供的接口实例化其子类
Employee *employer1 = [[Employee alloc] init];
[employer doADaysWork];
[employer1 doADaysWork];
//如果对象所属的类位于类簇中,查询其类型信息时应格外注意。你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。在Employee这个例子中,[employee isMemberOfClass:[Employee class]]似乎会返回YES,但实际上返回的却是NO,因为employee并非Employee类的实例,而是其某个子类的实例。
//此方法可理解为对象是否为该类簇中所包含的类的对象,is a kind of就是isa指针的意义
NSLog(@"%zd", [employer isKindOfClass:[Employee class]]);
//此方法可理解为对象是否为该类的直接对象
NSLog(@"%zd", [employer isMemberOfClass:[Employee class]]);
}
return 0;
}
总结 |
如果你只想扩展类的方法,Category就已经能满足你了。总之不要轻易的继承一个类簇,比如NSString、NSMutableArray等等,因为你可能需要做一大堆事情来保证该类能正常工作,其中定义数据的存储空间和找到抽象超类的原始方法并重载它们都不是很轻松的事。如果你对这些事情不熟悉的话,就另辟蹊径,干嘛要在一棵树上吊死呢!
参考:
https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html#//apple_ref/doc/uid/TP40010810-CH4-SW1
http://blog.csdn.net/hierarch_lee/article/details/50458921