Cocoa 继承类
Cocoa 继承类是本文要介绍的内容,象Application Kit这样的框架都定义某种程序模型。由于这个模型具有一般性,很多不同类型的应用程序都可以共享。也由于这个模型具有一般性,框架中的某些类是抽象类或有意没有完成也并不奇怪。一个类通常会完成很多低级别的、公用的代码,而将工作的相当一部分留下来,或者以安全而又一般的“缺省”方式来完成。
应用程序通常需要创建子类来填充超类留下的缺口,提供框架类缺少的东西。子类是向框架添加具体应用程序行为的基本途径。定制子类的实例在框架定义的对象网络中代替其超类的位置,并通过继承从超类得到与框架中其它对象协同工作的能力。举例来说,如果您创建了一个NSCell的子类,则这个新类的实例可以出现在NSMatrix对象中,就象NSButtonCell、NSTextFieldCell、以及其它框架定义的cell对象一样。
在制作子类时,一个主要的任务就是实现一组由超类(或者超类采纳的协议)声明的具体方法。重新实现超类的方法被称为对该方法进行重载。
何时进行方法的重载
框架类中定义的大多数方法都是完全实现的,您可以对其进行调用,以得到它们提供的服务。您很少需要重载这种方法,而且也不应该试图这样做。依赖于这些类的框架只是做它们应该做的事—既不多,也不少。在某些场合下,您可以对这些方法进行重载,但是没有真正的原因需要这么做,框架版本的方法已经足够了。但是,正如您可能实现您自己的字符串比较函数、而不是使用strcmp函数那样,如果您愿意,可以选择重载框架的方法。
然而,有些框架方法的设计目的就是为了被重载的,您可以通过这种方式向框架加入程序的具体行为。这些方法在框架中的实现对应用程序通常价值很小,或者没有价值,但会在其它框架方法发出的消息中被调用。应用程序必须实现自己的版本,为这些方法加入新的内涵。
调用还是重载?
一般来说,您自己并不调用,至少不直接调用在子类中重载的框架方法。您只要简单地重新实现这些方法,然后将它留给框架就好了。实际上,越是那些实现应用程序具体行为的版本,您自己的代码对它调用的可能性就越小。这有一个很好的原因。在一般意义上,框架类负责声明一些公共方法,您作为开发者可以有两种使用方式:
调用这些方法,使类提供的服务为您所用
对这些方法进行重载,将您的代码引入到框架定义的程序模型中
有些时候,一个方法会同时符合上述两种情况,既可以通过被调用提供有价值的服务,也可以被策略性地重载。但是一般来说,一个方法如果可以被调用,就已经由框架完全定义好了,不需要在您的代码中进行精化;如果该方法需要在子类中重新实现,则说明框架为该方法分派了特殊的工作,而且会在恰当的时候对其进行调用。图3-2显示了这两种一般类型的框架方法。
图 调用一个框架方法,该方法又通过消息调用一个重载了的方法
使用Cocoa框架进行面向对象编程的大部分工作是实现一些方法,而您的程序只是间接地、通过框架安排的消息使用这些方法。
重载方法的类型
您可以选择在子类中定义几个不同类型的方法:
某些框架方法是完全实现的,且其设计的目的是被别的框架方法调用。换句话说,即使您重新实现这些方法,通常也不在其它代码的其它地方调用。它们提供特定的服务—数据或行为,这些服务是程序执行过程中某些地方的代码要求的。这些方法存在于公共接口中只有一个原因—就是让您在需要的时候可以对其进行重载,这使您有机会用自己的算法来替代框架使用的算法,或者对框架的算法进行修改和扩展。
这种类型的方法的一个例子是NSMenuView类定义的trackWithEvent:方法。NSMenuView类实现这个方法是为了满足看得见的需求—处理菜单跟踪和菜单项的选择,但是如果您希望实现不同的行为,则可以对其进行重载。
另一类方法负责做一些与具体对象有关的决定,比如是否打开某个属性,或者是否让特定的策略起作用。框架为这种方法实现一个缺省版本,从而提供一种工作方式,如果您需要有所改变,就必须实现自己的版本。在大多数情况下,实现就是简单地返回YES或者NO,或者对某个值进行计算,而不是使用缺省值。
NSResponder类的acceptsFirstResponder方法就是一个典型的例子。系统向视图对象发送消息中包含acceptsFirstResponder消息,用于询问它们是否响应按键或鼠标点击事件。缺省情况下,NSView对象在这个方法中返回NO—大多数视图对象并不接收按键输入。但是某些视图对象却是可以的,因此它们必须重载acceptsFirstResponder方法,使之返回YES。
某些方法必须被重载,但只是增加一些处理,而不是完全取代框架的实现。这种方法的子类版本对超类版本的行为进行增强。您的程序在实现这种方法时,很重要的一点是要吸收被重载方法,即向super(超类)对象发送消息,调用框架为该方法定义的版本。
这类方法通常是继承链中的每个类都希望有所贡献的。举例来说,可以自行归档的对象必须遵循NSCoding协议,并且实现initWithCoder:和encodeWithCoder:方法。但是,一个类在对自己特有的实例变量进行编解码的时候,必须调用相应方法的超类版本。
有些时候,方法的子类版本希望“重用”超类的行为,然后在最后的结果中加入一些小变化。比如NSView类的drawRect:方法,执行某些复杂描画的视图子类可能希望在描画结果中加上一个边界,这样就要首先调用super版本的方法。
某些框架方法什么事情都不做,或者只是返回一些试验性的缺省值(比如self),避免运行时或编译时的错误。这些方法的设计目的就是为了被重载。即便是最基本的行为,框架也无法为它们定义,因为它们执行的任务全部和具体程序相关。对于这种方法,没有必要通过向super发送消息来调用框架的实现。
子类重载的大部分方法都是这种类型。比如NSDocument类的dataOfType:error:和readFromData:ofType:error:(还有其它)方法,在您创建基于文档的应用程序时必须被重载。
对一个方法进行重载并不一定很难。通过认真地重写方法中的一两行代码,您常常就能显著改变超类的行为。在实现自己版本的方法时,也不是完全从头开始,您可以借助Cocoa框架提供的类、方法、和类型。
什么时候需要使用子类
和了解类的哪些方法需要重载—并真正地实施重载—一样重要的是,识别哪些类需要被继承。有些时候,这些决定可能是很明显的,而在另一些时候,做这样的决定则相当不简单。下面的一些设计上的考虑可以指导您做这样的选择。
首先,连接框架。您应该熟悉每个框架类的目的和能力。您希望做的事情可能已经在某个类中实现了,或者如果您发现希望完成的任务在某个类中已经差不多完成了,那就很幸运了,那个类很可能是您需要的定制类的超类。子类化是重用现有的类、并根据需要将它具体化的过程。有些时候,一个子类需要做的所有工作,就是对一个方法进行重载,并使它的行为和超类版本轻微不同。其它子类可能在超类的基础上增加一两个属性(以实例变量的形式),然后实现一些访问和操作这些属性的方法,从而将它们集成到超类的行为中。
在决定子类在类层次中的位置时,还有其它一些有益的考虑。您希望开发的应用程序、或者应用程序的一部分的本质是什么?有些Cocoa架构对子类有些要求。举例来说,如果您开发的是一个多文档的应用程序,则Cocoa基于文档的架构就要求您生成NSDocument类的子类,可能还有其它类。如果要让您的应用程序可以通过脚本进行控制(也就是说,可以响应AppleScript命令),可能必须生成诸如NSScriptCommand这样的脚本类的子类。
另一个因素是子类的实例在应用程序中发挥的作用。模型-视图-控制器模式是Cocoa的主要设计模式,它将对象的角色做如下分配:出现在用户界面上的对象属于视图对象,模型对象负责保存应用程序数据(和对该数据进行操作的算法),控制器对象则负责协调视图对象和模型对象(详细信息请参见"模型-视图-控制器设计模式"部分)。
了解一个对象的作用可以收窄其超类的选择范围。如果您的类实例是实现定制描画和事件处理的视图对象,可能应该选择NSView作为超类;如果您的应用程序需要一个控制器类,则可以使用某个复活类(比如NSObjectController类),或者如果您希望有不同的行为,也可以从NSController或NSObject派生出子类;如果您的类是一个典型的模型类—比如代表一个电子表格数据中的行的类—则可能应该从NSObject派生出子类,或者使用Core Data框架。
然而生成子类有时并不是解决问题的最好办法,可能有更好的办法可以选择。如果您只是希望为某个类增加一些便利方法,就可以通过创建范畴来实现,而不需要生成子类;或者,您也可以借助基于Cocoa开发“工具箱”资源的很多其它设计模式之一来实现,比如委托、通告、和目标-动作模式(在"和对象进行通讯"部分中描述)。在确定使用某个候选超类之前,先扫描一下它的头文件(或参考文档),看看是否有什么委托方法、通告、或者其它机制可以实现您需要的功能,而又不需要生成子类。
类似地,您也可以考察一下框架协议的头文件或文档。通过采纳协议,您既可以完成目标,又可以规避复杂子类的困难工作。举例来说,假定您希望管理菜单项的激活状态,则可以在定制的控制器子类中采纳NSMenuValidation协议,而不必从NSMenuItem或NSMenu派生子类来得到这个行为。
和某些框架方法不用于被重载一样,一些框架类(比如NSFileManager、NSFontPanel、和NSLayoutManager)也不用于生成子类。如果您确实希望有这样的子类,则应该谨慎处理。某些框架类的实现是相当复杂的,和其它类的实现、甚至是操作系统的不同部分紧密结合在一起。通常情况下,我们很难正确复制框架方法的行为,或者预期这种方法可能有的依赖性和效果。您对一些方法实现的修改,可能带来深远的、不可预见的、以及不希望的结果。
在某些情况下,您可以通过对象的合成来克服这种困难。对象的合成是一种将多个对象装配到一个“宿主”对象中的通用技术,宿主对象负责管理这些对象,并获得复杂而又高度定制的行为(参见图 3-3)。您不必直接从一个复杂的框架超类继承子类,而是创建一个定制类,然后将超类的实例作为类的一个实例变量。定制类自身可能相当简单,可能直接从NSObject根类继承就可以了。虽然从继承的角度来看是简单了,但是该类负责对嵌入的实例进行操作、扩展、和增强。对于客户对象来说,该类在某些方面就象是复杂超类的子类,虽然它可能并不共享超类的接口。
Foundation框架中的NSAttributedString类就是对象合成的一个实例。NSAttributedString以实例变量的形式保有一个NSString对象,并通过string方法将它暴露给客户代码。NSString是一个具有复杂行为的类,包括字符串编码、字符串检索、以及路径处理。NSAttributedString则在这些行为的基础上加入了新的能力,可以将字体、颜色、对齐、以及段落风格这样的信息附加到某个范围的字符中,而且在不生成NSString子类的前提下实现这个增强。
图 对象合成