类簇是基础框架中大量使用的一种设计模式。类簇聚合了大量的私有实体子类在一个抽象父类下面。这种聚合多个类的方式在不减少功能丰富性的前提下简化了面向对象框架的公共可视化的体系结构。类簇基于抽象工厂设计模式。
Without Class Clusters: Simple Concept but Complex Interface
为了说明类簇的体系架构及其好处,假设构造一个用来定义存储不同类型数据(char
, int
, float
, double
)的类层级这一问题,由于这几种类型有许多通用的共性(可以从一种类型转为另一种类型,都可以用string表示等等),他们都可以通过一个简单的类来描述。然而,他们的存储方式是不一样的,用一个相同的类来描述他们会导致效率低下。考虑到这些因素,可以通过下图描述的类层级来解决这个问题。
Figure 1-1 A simple hierarchy for number classes
Number是一个抽象的父类,该类中定义的都是子类通用的方法。然而,该类不定义一个变量用来存储数值,子类定义这么个属性用来存储实际数据。
目前,这种设计模式相对是简单的。然而,假如把C语言基础类型通用修饰符也考虑进来,类的层级将看着如下:
Figure 1-2 A more complete number class hierarchy
一个简单的概念——创建一个存储数值的类,轻易地引入了一把的类。类簇体系结构呈现的设计模式简化了这一概念。
With Class Clusters: Simple Concept and Simple Interface
对于这一问题应用类簇设计模式后,类层级模式如下(私有类灰色)
Figure 1-3 Classcluster architecture applied to number classes
这个层级图的用户只能看到公共类,Number,问题来了,怎么创建正确的子类的实体,答案是抽象父类负责实例化。
Creating Instances
类簇中的抽象父类必须定义创建私有子类实例的方法。根据你调用的创建方法,父类创建正确的子类对象。你不可以也不能选择创建的子类类型。
在基础架构中,通常你通过调用一个+
className...
方法或者alloc...
或者
init...
方法创建对象。以基础架构中的
NSNumber类为例,你可以调用下面的方法创建数值对象
NSNumber *aChar = [NSNumber numberWithChar:’a’]; |
NSNumber *anInt = [NSNumber numberWithInt:1]; |
NSNumber *aFloat = [NSNumber numberWithFloat:1.0]; |
NSNumber *aDouble = [NSNumber numberWithDouble:1.0]; |
你不需要释放工厂类返回的对象。许多类也提供了标准的alloc...
和 init...
方法创建对象,这个就要求释放。
每个返回的对象——
aChar
, anInt
, aFloat
,和 aDouble
,可能属于不同的私有类,实际也确实如此。尽管每个对象的类成员是不可见,但是它们的接口是公共的,作为抽象类声明的接口,
NSNumber,
尽管这样描述不是非常准确,把
aChar
,anInt
, aFloat
,和 aDouble
对象当作是
NSNumber
类的实例是很方便的,因为他们都是NSNumber
的类方法创建,并且可以通过
NSNumber
定义的实例方法进行操作。
Class Clusters with Multiple Public Superclasses
在上述例子中,一个抽象公共类为多个私有子类声明接口。这是类簇中最常见的场景。同时也有可能,或者通常必须的,两个或者多个抽象公共类为簇声明接口。下表包含了部分基础架构中的类簇,存在多个父类
Table 1-1 Class clusters and their public superclasses | |
Class cluster | Public superclasses |
| |
| |
| |
| |
其他类似的类簇也存在,但是上面这些清楚地说明了两个抽象节点是如何合作共同为类簇提供编程接口的。上面每一个簇,一个公共节点定义的方法,簇中对象都能响应,另一个节点,定义的方法只适合提供给簇中允许修改内容的对象。
簇中接口这个特性提升了面向对象架构的编程接口更具表达性。例如,假设代表书的一个对象定义了如下方法:
- (NSString *)title;
这个书对象可以返回它本身的实例变量或者创建一个新的string对象返回,这些都不重要,可以确定的是这个方法声明返回的string是不可以更改的,任何企图更改这个返回对象的行为都会触发编译器告警。
Creating Subclasses Within a Class Cluster
类簇架构在易用性与扩展性涉及平衡问题:少数几个公共类代替多个私有类更加易学易用,但是在任何簇创建子类都将变得更难。然而,假如很少需要创建子类,簇的体系架构显而易见是有利的。簇就是在这种情形下用于基础架构中的。
假如你发现一个簇不提供你编程所需要的方法,你可能需要创建一个子类。例如,假设你需要创建一个基于文件存储而不是内存存储的数组对象,如类簇中的NSArray
。因为你改变了类底层的存储机制,你必须要创建一个子类。
另一方面,在某些情形下,定义一个类,持有簇中对象的引用可能更加有效更加简单。假设你的程序在某些数据被更改时需要发出告警,在这种情形下,创建一个简单的类,包含一个基础架构中定义过的对象是更好的方式,这个对象可以干预任何修改数据的信息,拦截信息,处理信息,然后作用到内置的数据对象上。
总之,假如你只是需要管理对象存储机制,创建一个真正的子类。否则,创建一个组合对象,一个你自己设计的嵌入了标准基础框架对象的对象。下面板块就关于这两种方式提供了更多详细信息。
A True Subclass
在类簇中创建新的类你需要:
· 继承簇抽象父类
· 定义存储机制
· 覆写父类所有的初始化方法
· 覆写父类的原始方法(下面描述)
由于簇的抽象父类是簇层级中唯一公共可见的节点,所以第一点是显而易见的。这意味着新的子类将继承簇的接口,但是不包括实例变量,因为抽象父类未包含实例变量,所以,第二点,子类必须定义自身所需要的实例变量。最后,子类必须覆写继承自父类的用于直接操作对象实例变量的方法,这些方法被称为原始方法。
类的原始方法是接口的基础。例如,以NSArray
类为例,它为管理数组的对象定义接口。数组保存了许多数据项,每个数据项通过下标索引操作。
NSArray
通过两个原始方法表述了这个抽象概念,
count
和 objectAtIndex:
。以这两个方法为基础,其他的方法
-
继承的方法
-
都可以实现。表
1-2 给了两个派生方法的实例。
Table 1-2 Derived methods and their possible implementations | |
Derived Method | Possible Implementation |
| Find the last object by sending the array object this message: |
| Find an object by repeatedly sending the array object an |
接口中对原始方法和派生方法进行区分使得创建子类更加容易。你的子类必须覆盖继承的原始方法,如此才能确保所有继承的派生的方法能正确地运行。
原始与派生的区分应用于完全初始化对象的接口,关于init...
方法在子类中如何处理的问题也是需要解决的。
一般的,簇的抽象父类声明了许多init...
和 +className
方法,正如
Creating Instances所描述,抽象类根据你调用的init...
or + className
方法决定实例化哪个具体的子类。你可以认为为了方便子类抽象父类定义了这一系列方法。因为抽象父类没有实例变量,本身不需要初始化方法。
你的子类应该定义自身的init..
(假如有实例变量需要初始化)和可能需要的
+className
方法。子类不应该依赖于继承自父类的相关方法。为了维护初始化链,子类应该在自身特定的初始化方法中调用父类的特定的初始化方法。子类还应该重写所有其它所继承的初始化方法并且以合理的方式实现它们。(参照Multiple Initializers and the Designated Initializer关于特定初始化器的讨论)在一个类簇中,抽象父类的特定初始化方法一直是init
.
TrueSubclasses: An Example
让我们假设你想创建一个NSArray
的名为
MonthArray
的子类,这个子类根据给定的下标索引返回月份名称。然而,这个对象并不以实例变量真正存储月份名称数组。相反,这个返回名称的方法会根据给定索引下标
(objectAtIndex:
)返回常量字符串。因此,只需要分配存储
12
个字符串对象就可以了,不管应用中存在多少个
MonthArray
对象。
MonthArray
类声明如下:
#import <foundation/foundation.h> |
@interface MonthArray : NSArray |
{ |
} |
|
+ monthArray; |
- (unsigned)count; |
- (id)objectAtIndex:(unsigned)index; |
|
@end |
注意MonthArray
类没有声明init...
,因为它没有实例变量需要初始化。
count
和objectAtIndex:
方法,如上所述,只是简单覆盖继承的原始方法。
MonthArray
实现看着如下:
#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; |
} |
|
- objectAtIndex:(unsigned)index |
{ |
if (index >= [self count]) |
[NSException raise:NSRangeException format:@"***%s: index |
(%d) beyond bounds (%d)", sel_getName(_cmd), index, |
[self count] - 1]; |
else |
return months[index]; |
} |
|
@end |
因为MonthArray
覆盖了继承的原始方法,继承的派生的方法不需要重新就能运行正确,
MonthArray
继承自NSArray的lastObject
, containsObject:
, sortedArrayUsingSelector:
,objectEnumerator
和其他方法都没有问题。
A Composite Object
通过在你自己设计的对象中嵌入一个私有的簇中对象,你可以创建一个组合对象。这个组合对象的基础功能依赖于簇对象的方法,只是调用簇对象方法前根据实际需求拦截了调用信息。这个架构减少了你的代码量并且让你可以利用基础机构提供的测试代码。
图1-4 说明了这种结构
这个组合对象必须声明自己为簇抽象父类的子类。作为子类,它必须复写所有父类的原始方法,也可以选择性地覆盖派生的方法,但这不是必须的,因为派生方法依赖原始方法。
以NSArray
类的
count
方法为例,子类实现如下:
- (unsigned)count { |
return [embeddedObject count]; |
} |
然而,你的对象在实现复写的方法时,可以根据需要写入任何代码。
A Composite Object: An Example
为了说明组合对象的使用,假设你想要一个可变数组对象,在允许任何改变数组内容前,做一些条件校验。下面的例子定义了一个ValidatingArray
类,这个类包含了一个标准的可变数组对象。
ValidatingArray
重写了所有父类(
NSArray
和NSMutableArray
.)定义的原始方法,同时还定义了
array
, validatingArray
,和 init
方法,用来创建和初始化实例。
#import <foundation/foundation.h> |
|
@interface ValidatingArray : NSMutableArray |
{ |
NSMutableArray *embeddedArray; |
} |
|
+ validatingArray; |
- init; |
- (unsigned)count; |
- objectAtIndex:(unsigned)index; |
- (void)addObject:object; |
- (void)replaceObjectAtIndex:(unsigned)index withObject:object; |
- (void)removeLastObject; |
- (void)insertObject:object atIndex:(unsigned)index; |
- (void)removeObjectAtIndex:(unsigned)index; |
|
@end |
实现类显示了如何在ValidatingArray
类的
init
方法中,创建嵌入对象和把对象赋值给
embeddedArray 变量。简单的获取数组而不是修改内容的调用直接传递给嵌入对象,那些修改内容的指令将会被审核(伪代码)并且只有通过假设的校验测试才能传递给嵌入对象。
#import "ValidatingArray.h" |
|
@implementation ValidatingArray |
|
- init |
{ |
self = [super init]; |
if (self) { |
embeddedArray = [[NSMutableArray allocWithZone:[self zone]] init]; |
} |
return self; |
} |
|
+ validatingArray |
{ |
return [[[self alloc] init] autorelease]; |
} |
|
- (unsigned)count |
{ |
return [embeddedArray count]; |
} |
|
- objectAtIndex:(unsigned)index |
{ |
return [embeddedArray objectAtIndex:index]; |
} |
|
- (void)addObject:object |
{ |
if (/* modification is valid */) { |
[embeddedArray addObject:object]; |
} |
} |
|
- (void)replaceObjectAtIndex:(unsigned)index withObject:object; |
{ |
if (/* modification is valid */) { |
[embeddedArray replaceObjectAtIndex:index withObject:object]; |
} |
} |
|
- (void)removeLastObject; |
{ |
if (/* modification is valid */) { |
[embeddedArray removeLastObject]; |
} |
} |
- (void)insertObject:object atIndex:(unsigned)index; |
{ |
if (/* modification is valid */) { |
[embeddedArray insertObject:object atIndex:index]; |
} |
} |
- (void)removeObjectAtIndex:(unsigned)index; |
{ |
if (/* modification is valid */) { |
[embeddedArray removeObjectAtIndex:index]; |
} |
} |