类簇(class clusters)
类簇是Foundation framework框架下广泛使用的一种设计模式。它管理了一组隐藏在公共抽象父类下的具体私有子类。
没有使用类簇(Simple Concept but Complex Interface)
为了说明类簇的结构体系和好处,我们先思考一个问题:如何构建一个类的结构体系用它来定义一个对象存储不同数据类型的数字(char
,int
, float
, double
)。因为不同数据类型的数字有很多共同点(例如:它们都能从一种类型转换成另一种类型,都能用字符串表示),所以可以用一个类来表示它们。然而,不同的数据类型的数字的存储空间是不同的,所以用一个类来表示它们是很低效的。考虑到这个问题,我们设计了如下图1-1的结构解决这个问题。
Number
是一个抽象父类,在其方法声明中声明了子类的共有操作。但是,Number
不会声明一个实例变量存储不同类型的数据,而是由其子类创建对应类型的实例变量并将调用接口共享给抽象父类Number
。
到目前为止,这个类结构的设计十分简单。然而,如果C语言的基本数据类型被修改了(例如:加入了些新的数据类型),那么我们Number
类结构如下图1-2所示:
这种创建一个类保存一种类型数据的概念很容易扩展成十几个类。类簇的体系结构展示了一种概念简洁性的设计。
使用类簇(Simple Concept and Simple Interface)
使用类簇的设计模式来解决这个问题,类结构设计如图1-3所示:
使用类簇我们只能看到一个公共父类Number
,它是如何创建正确子类的实例的呢?解决方式是利用抽象父类来处理实例化。
创建实例(Creating Instances)
在类簇中的抽象父类必须声明创建私有子类变量的方法。抽象父类的主要职责是当调用创建实例对象的方法时,根据调用的方法去分配合适的子类对象(不能选择创建实例对象的类)。
在Foundation framework中,你可能调用类方法或者alloc
和init
创建对象。以Foundation framework的NSNumber
创建数字对象为例:
NSNumber *aChar = [NSNumber numberWithChar:’a’];
NSNumber *anInt = [NSNumber numberWithInt:1];
NSNumber *aFloat = [NSNumber numberWithFloat:1.0];
NSNumber *aDouble = [NSNumber numberWithDouble:1.0];
使用上面方法返回的对象aChar
, anInt
, aFloat
, aDouble
是由不同的私有字类创建的。尽管每个对象的从属关系(class membership)被隐藏了,但是它的接口是公开的,能够通过抽象父类NSNumber
声明的接口来访问。当然这种做法是及其不严谨的,某种意义上是不正确的,因为用NSNumber
方法创建的对象并不是一个NSNumber
的对象,而是返回了一个被隐藏了的私有子类的对象。但是我们可以很方便的使用抽象类NSNumber
接口中声明的方法来实例化对象和操作它们。
拥有多个公共抽象父类的类簇(Class Clusters with Multiple Public Superclasses)
在上面的例子中,使用一个公共抽象父类声明多个私有子类的接口。但是在Foundation framework框架中也有很多使用两个或两个以上的公共抽象父类声明私有子类接口的例子,如表1-1所示:
类簇 | 公共抽象父类 |
---|---|
NSData | NSData,NSMutableData |
NSArray | NSArray,NSMutableArray |
NSDictionary | NSDictionary,NSMutableDictionary |
NSString | NSString,NSMutableString |
还存在这种类型的类簇,但这些清楚说明了两个公共抽象父类是如何协同工作来声明类簇的编程接口的。一个公共抽象父类声明了所有类簇对象都能相应的方法,而另一个公共抽象父类声明的方法只适合允许修改内容的类簇对象。
创建子类(Creating Subclasses Within a Class Cluster)
类蔟的体系结构是在易用性和可扩展性之间均衡的结果:类簇的应用使得学习和使用框架中的类十分简单,但是在类簇中创建子类是困难的。但是很少情况下需要在类簇中创建子类,因为类簇的好处是显而易见的。
如果你发现类簇提供的功能不能满足你的变成需要,那么在类簇创建子类是一种选择。例如:假如你想在NSArray
的类簇中创建一个基于文件存储而不是基于内存存储的数组。因为改变了类的底层存储机制,就不得不在类簇中创建子类。
另一方面,在某些情况下我们创建一个类内嵌类簇对象就足够了。例如:如果你的程序需要被提醒,当某些数据没被修改的时候。在这种情况下,创建一个包装Foundation framework框架定义的数据对象的类可能是最好的方法。这个类的对象能干预修改数据的消息,拦截这个消息,对这个消息采取相应的行动,然后重定向给内嵌的数据对象。
综上所述,如果你想管理对象的存储空间,就在类簇中创建子类。否则,创建一个复合对象(composite object),复合对象:你自己设计的类对象内嵌一个标准Foundation framework框架的对象。
真正子类(A True Subclass)
在类簇中创建一个子类,你必须:
- 创建类簇中抽象超类的子类
- 声明自己的存储空间
- 重写父类的所有初始化方法
- 重写父类的所有原始方法(primitive methods)
第一点:因为在类簇的体系结构中只有类簇中的抽象父类是公开可见的节点。第二点:子类会继承类簇的接口但没有实例变量,因为抽象父类没有声明,所以子类必须声明它所需要的任意实例变量。最后:子类必须重写继承的所有方法。
一个类的原始方法(primitive methods)是构成其接口的基础。拿NSArray
为例,它声明类管理数组对象的接口。在概念上,一个数据保存了很多数据项,它们都能通过下标(index)访问。NSArray
通过这两个原始方法表达了这一抽象概念,count
和objectAtIndex:
,以这些方法为基础可以实现其它派生方法。
派生方法(Derived Method) | 可能实现(Possible Implementation) |
---|---|
lastObject | 查找数组对象中的最后一个对象:[self objectAtIndex: ([self count] –1)] 。 |
containsObject: | 查找对象通过遍历数组对象给其发送objectAtIndex: 消息,直到数组里的所有对象都检测完为止。 |
原始方法(primitive methods)和派生方法(derived methods)的接口区分使创建子类更简单。子类必须重写所有继承的原始方法(primitive methods),这样做可以确保所有继承的派生方法(derived methods)都能正常运行。
原始和派生的区别同样适用于完全初始化对象接口。子类中需要解决如何处理init…
方法的问题。
通常,一个类簇的抽象父类方法声明了一系列init…
方法和+ className
类方法。基于你选择的init…
方法或+ className
类方法,抽象类决定用哪个具体的子类来实例化。你可以认为抽象类是为子类服务的,因为抽象类没有实例变量,它也不需要初始化方法。
自定义的子类应该声明自己的init…
和+ className
方法,不应该依赖继承的方法。为了保持初始化链,它应该在自己的指定初始化函数里调用父类的指定初始化函数(designated initializers)。在类簇中它也应该以合理方式重写继承的所有初始化方法,抽象父类的指定初始化函数总是init
。
复合对象(A Composite Object)
在你自定义的对象中内嵌一个私有的类簇对象称为复合对象。复合对象可以利用类簇对象来实现基本的功能,只拦截复合对象想要特殊处理的消息。这种结构减少了代码量,利用了Foundation framework的测试代码。如图1-4所示:
复合对象必须声明它自己是类簇抽象父类的子类,必须重写父类的所有原始方法,也可以重写派生方法但不是必须的。
总结
在Cocoa中,实际上许多类都是以类簇的方式实现的,即它们是一群隐藏在通用接口之下与实现相关的类。例如创建数组时可能是__NSArray0
,__NSSingleObjectArray
, __NSArrayI
,所以请不要轻易尝试创建NSString
,NSArray
,NSDictionary
的子类。对类簇使用isKindOfClass
和isMemberOfClass
的结果可能是不正确的。因为类簇是由公共抽象类管理的一组私有类,公共抽象类并不是实例对应的真正的类,类簇中真正的类的从属关系被隐藏了。