从NSArray看Objective-C中的类族(Class Clusters)和工厂模式

如果要继承自NSString,NSArray,NSDictionary,NSNumber,要当心!

概念

  • 工厂模式(Factory pattern):

“Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.”

即:定义一个接口来创建对象,让子类来决定实例哪个类。工厂方法让一个类的实例化延迟到子类中。

  • 类簇(class cluster):

Class clusters are a design pattern that the Foundation framework makes extensive use of. Class clusters group a number of private concrete subclasses under a public abstract superclass. The grouping of classes in this way simplifies the publicly visible architecture of an object-oriented framework without reducing its functional richness. Class clusters are based on the Abstract Factory design pattern. -《Concepts in Objective-C Programming》

类簇是Foundation框架中广泛使用的设计模式。类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。类族是基于抽象工厂模式。
这样做的目的是为了接口的简单性。
官方文档中举了NSNumber 作为例子.

类族模式体现在 使用NSNumber的numberWith…方法时,返回类型并不总是NSNumber类型,例如

1
2
3
4
5
6
7
8
9
10
11
NSNumber *boolNumber = [NSNumber numberWithBool:true];
NSNumber *intNumber = [NSNumber numberWithInteger:1];

NSLog(@"boolNumber class = %@ ",[boolNumber class]);
NSLog(@"intNumber class = %@",[intNumber class]);

/*
Output:
2016-07-16 23:27:29.834 Test[19339:874972] boolNumber class = __NSCFBoolean
2016-07-16 23:27:29.835 Test[19339:874972] intNumber class = __NSCFNumber
*/

__NSCFBoolean__NSCFNumber都是NSNumber的子类,但是使用者不必关心这些子类,只需要把它当作是一个NSNumber对象就行了。这种设计模式保证了接口的简单性。
但是当我要创建子类时,麻烦就来了。

SubClass

以NSArray为例,
如果我创建一个继承自NSArray的MonthArray类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MonthArray *months = [[MonthArray alloc]init];
NSLog(@"%@",months.count);

//上述代码并没有和预想的一样,输出0;而是报了如下错误:

/*Output:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSArray count]: method only defined for abstract class. Define -[MonthArray count]!'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff8b4124f2 __exceptionPreprocess + 178
1 libobjc.A.dylib 0x00007fff93302f7e objc_exception_throw + 48
2 CoreFoundation 0x00007fff8b47cccf __CFRequireConcreteImplementation + 271
3 CoreFoundation 0x00007fff8b469d97 -[NSArray count] + 39
4 Test 0x0000000100000e9e main + 94
5 libdyld.dylib 0x00007fff89a775ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
*/

method only defined for abstract class 即:抽象类仅定义了方法

官方文档中找到了类簇的子类扩展的方法:

A True Subclass
A new class that you create within a class cluster must:
Be a subclass of the cluster’s abstract superclass
Declare its own storage
Override all initializer methods of the superclass
Override the superclass’s primitive methods (described below)

要做类族的子类,就要做到:

  1. 以公共“抽象”类为父类,比如NSNumber、NSArray等,而非其子类
  2. 提供自定义存储
  3. 重写(覆盖)父类所有初始化方法
  4. 重写父类中“原始”方法

原始方法(primitive methods)是一个类的接口的基础,以NSArray为例,以它的两个原始方法countobjectAtIndex:为基础,其他派生方法(derived methods)可以派生实现,例如:

Derived Method Possible Implementation
lastObject [self objectAtIndex: ([self count] –1)]
containsObject: 递增索引反复调用objectAtIndex,然后对所得到的对象进行等同性判断

到这里,《Effective-Objective-C 2.0》第9条讲到从类族的公共类抽象基类中继承子类时要当心,若有开发文档,则应首先阅读的原因,已经清晰明了了。
示例代码:

1
2
3
4
5
6
7
//.h
#import <foundation/foundation.h>
@interface MonthArray : NSArray
+ monthArray;
- (unsigned)count;
- (id)objectAtIndex:(unsigned)index;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//.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
{
if (!sharedMonthArray) {
sharedMonthArray = [[MonthArray alloc] init];
}
return sharedMonthArray;
}

- (unsigned)count
{
return 12;
}

-(id)objectAtIndex:(unsigned int)index{
if (index >= [self count]){
[NSException raise:NSRangeException format:@"***%s: index(%d) beyond bounds (%d)", sel_getName(_cmd), index,[self count] - 1];
}
return months[index];
}

@end
1
2
3
4
5
6
7
8
9
10
11
MonthArray *months = [[MonthArray alloc]init];

NSLog(@"lastmonth = %@",months.lastObject);
NSLog(@"firstmonth = %@",months.firstObject);


/*
Output:
2016-07-17 00:13:05.558 Test[19513:920103] lastmonth = December
2016-07-17 00:13:05.559 Test[19513:920103] firstmonth = January
*/

对象的所属类判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MonthArray *months = [[MonthArray alloc]init]; 
BOOL isEqual1 = [months class] == [NSArray class];//0
BOOL isEqual2 = [months isKindOfClass:[NSArray class]];//1
BOOL isEqual3 = [months isMemberOfClass:[NSArray class]];//0

BOOL isEqual4 = [months class] == [MonthArray class];//1
BOOL isEqual5 = [months isKindOfClass:[MonthArray class]];//1
BOOL isEqual6 = [months isMemberOfClass:[MonthArray class]];//1

NSLog(@"%d",isEqual1);
NSLog(@"%d",isEqual2);
NSLog(@"%d",isEqual3);
NSLog(@"%d",isEqual4);
NSLog(@"%d",isEqual5);
NSLog(@"%d",isEqual6);

/*
Output:
2016-07-17 00:17:01.650 Test[19556:924326] 0
2016-07-17 00:17:01.651 Test[19556:924326] 1
2016-07-17 00:17:01.651 Test[19556:924326] 0
2016-07-17 00:17:01.651 Test[19556:924326] 1
2016-07-17 00:17:01.651 Test[19556:924326] 1
2016-07-17 00:17:01.651 Test[19556:924326] 1
*/

isKindOfClass:是否为该类或者该类的子类实例对象;
isMemberOfClass 是否为该类的实例对象。
==在此处和isMemberOfClass效果一样。

最后

当然一般我们不会继承自类族类,扩充方法用Category实现,遇到需要增加实例变量时,可以考虑一下关联引用