类簇 在官方文档中的解释
以下是翻译
类簇
类簇是Foundation框架广泛使用的设计模式。类簇在公共抽象超类下对多个私有的具体子类进行分组。以这种方式对类进行分组简化了面向对象框架的公共可见体系结构,而不会降低其功能丰富度。类簇是基于抽象工厂设计模式的。
没有类簇的情况:简单的概念,复杂的接口
为了说明类簇结构及其优点,考虑构建定义对象以存储不同类型(char、int、float、double)数量的类层次结构的问题。因为许多不同类型具有许多共同特征(例如,它们可以从一种类型转换为另一种类型,并且可以表示为字符串),所以它们可以由单个类表示。但是,它们的存储要求不同,因此用同一个类表示它们的效率很低。考虑到这一事实,我们可以用下图所示的类结构来解决这个问题。
Number是抽象超类,在其方法中声明其子类共有的操作。但是,它不会声明实例变量来存储数字。子类声明了这样的实例变量,并在声明的编程接口中共享Number。
到目前为止,这种设计相对简单。但是,如果考虑这些基本C类型的常用修改,则类层次结构图看起来更像下图。
简单的概念 - 创建一个容纳数字值的类 - 可以很容易地发展到十几个类。类簇结构提供了一种反映概念简单性的设计。
使用类簇的情况:简单的概念,简单的接口
将类簇结构应用于此问题会产生下图的类层次结构(私有类为灰色)。
此层次结构的用户只能看到一个公共类,Number那么如何分配正确子类的实例呢?答案就在于抽象超类处理实例化的方式。
创建实例
在类簇中,一个抽象的超类必须声明用于创建其私有子类的实例的方法。超类负责根据您调用的创建方法分配正确子类的对象 - 您不会,也不能手动选择实例的类。
在Foundation框架中,通常通过调用 +className...方法或alloc...和init...方法来创建对象。以Foundation框架的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和a.对象看作NSNumber类的实例是很方便的,因为它们是由NSNumber类方法创建并通过NSNumber声明的实例方法访问的。
具有多个公共超类的类簇
在上面的示例中,一个抽象公共类声明了多个私有子类的接口。这是一个纯粹意义上的类集群。也有可能并且通常需要有两个(或可能更多)抽象公共类来声明集群的接口。这在Foundation框架中很明显,其中包括下图中列出的集合。
其他的用到这种类型的集合也存在,但这些集合清楚地说明了两个抽象节点如何协作将类编程接口声明为类簇。在每个集合中,一个公共节点声明所有集合对象都可以响应的方法,另一个节点声明仅适用于允许修改其内容的集合对象的方法(译者注:换个简单的说法-可变和不可变的集合)。
集合接口的这种因子分解使面向对象框架的程序接口更加形象。举个例子,想象一个表示声明此方法的book的对象:
- (NSString *)title;
book对象可以返回自己的实例变量或创建一个新的字符串对象并返回它 - 这无关紧要。从此声明中可以清楚地看出,返回的字符串无法修改。任何修改返回对象的尝试都会引发编译器警告。
在类簇中创建子类
类簇体系包含了简洁性和可扩展性之间的权衡:一小部分公共类代表大量私有类,使得在框架中学习和使用更容易,但在集合中创建子类有点困难。然而,如果很少需要创建子类,那么集合结构显然是有益的。在符合上述这些条件的情况下,集合(类簇)在Foundation框架中使用。
如果您发现类簇不提供程序所需的功能,那么子类可能是合理的。例如,假设您要创建一个数组对象,其存储基于文件,而不是像在NSArray类簇中一样基于内存。因为您正在更改类的基础存储机制,所以您必须创建一个子类。
另一方面,在某些情况下,定义一个,在类簇中嵌入对象的类,可能就足够了(也更容易)。假设您需要在修改某些数据时,在程序中被提醒。在这种情况下,创建一个包装Foundation框架定义的数据对象的简单类可能是最好的手段。此类的对象可以干预修改数据,拦截消息,对其进行操作,然后将它们转发到嵌入数据对象的消息。
总之,如果您需要管理对象的存储,就请创建一个真正的子类。否则,就去创建一个复合对象,该对象将标准Foundation框架对象嵌入到您自己设计的对象中。下面的部分提供了有关这两种方法的更多详细信息。
真正的子类
您在类簇中创建的新类必须:
- 是集合的抽象超类的子类
- 声明自己的存储空间
- 覆盖超类的所有初始化方法
- 覆盖超类的原始方法(如下所述)
因为集群的抽象超类是集群层次结构中唯一公开可见的节点,所以第一点是显而易见的。这意味着新的子类将继承集群的接口,但不会继承实例变量,因为抽象超类声明无。因此第二点:子类必须声明它需要的任何实例变量。最后,子类必须覆盖它继承的任何直接访问对象实例变量的方法。这种方法称为原始方法。
类的原始方法构成了其接口的基础。例如,使用NSArray类,该类声明接口到管理对象数组的对象。概念上,数组存储了许多数据项,每个数据项都可以通过索引访问。NSArray通过它的两种原始方法来表达这种抽象概念,count并且objectAtIndex:。以这些方法为基础,可以实现其他方法衍生方法 ; 表1-2给出了派生方法的两个例子。
方法 | 可能的实现 |
---|---|
lastObject | 通过向此消息发送数组对象来查找最后一个对象:[self objectAtIndex: ([self count] –1)]。 |
containsObject | 通过重复向数组对象发送objectAtIndex:消息来查找对象,每次递增索引,直到测试了阵列中的所有对象。 |
基元和派生方法之间的接口划分使得创建子类更容易。您的子类必须重写继承的基元,同时要确保它继承的所有派生方法都能正常运行。
原始派生的区别适用于完全初始化的对象的接口。如何init...的方法需要被掌握的问题还需要被处理。
通常,集群的抽象超类声明了许多init...和+className方法。如创建实例中所述,抽象类基于您的选择init...或+ className方法决定实例化哪个具体子类。您可以认为抽象类声明了这些方法以方便子类。由于抽象类没有实例变量,因此不需要初始化方法。
您的子类应该声明它自己的init...(如果它需要初始化它的实例变量)和可能的+ className方法。它不应该依赖于它继承的任何东西。为了在初始化链中维护其链接,它应该在自己指定的初始化方法中调用其超类的指定初始化方法。它还应该重写所有其他继承的初始化方法,并让它们以合理的方式运行。(有关指定初始值设定项的讨论,请参阅Object Initialization。)在类集群中,抽象超类的指定初始值设定项始终为init。
真正的子类:一个例子
假设您要创建一个NSArray名为的子类,该子类MonthArray返回给定其索引位置的月份名称。但是,MonthArray对象实际上不会将月份名称数组存储为实例变量。相反,给定索引位置(objectAtIndex:)返回名称的方法将返回常量字符串。因此,无论MonthArray应用程序中存在多少个对象,都只会分配12个字符串对象。
在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重写了继承的原始方法,所以它继承的派生方法可以正常工作而不会被覆盖。NSArray的lastObject,containsObject:,sortedArrayUsingSelector:,objectEnumerator,和其他没有MonthArray对象问题的方法依然照常执行。
复合的对象
通过将私有集合对象嵌入到您自己设计的对象中这个方法,可以创建复合对象。此复合对象可以依赖于集合对象的基本功能,仅拦截复合对象希望以某种特定方式处理的消息。此体系结构减少了必须编写的代码量,并允许您利用Foundation Framework提供的测试代码。
复合对象必须声明自己是集群的抽象超类的子类。作为子类,它必须覆盖超类的原始方法。它也可以覆盖派生方法,但这不是必需的,因为派生方法通过原始方法工作。
NSArray的count方法就是一个例子; 介入对象的覆盖方法的实现可以简单如下:
- (unsigned)count {
return [embeddedObject count];
}
这样,您的对象可以将代码用于其自身的目的,以实现它覆盖的任何方法。
复合的对象:一个例子
为了说明复合对象的使用,假设您需要一个可变数组对象,该对象在允许对数组内容进行任何修改之前,不可以根据某些验证条件去修改确认标准。下面的示例描述了一个名为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];
}
}