面向对象分析与设计(第3版)(修订版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.3 类的本质

类的概念和对象的概念是紧密交织在一起的,因为我们在谈论一个对象时不得不提到它的类。但是,这两个术语之间又存在着重要的差别。

3.3.1 什么是类,什么不是类

对象是存在于时间和空间中的具体实体,而类仅代表一种抽象,即一个对象的“本质”。因此,可以说Mammal类,它代表了所有哺乳动物的共同特征。要确定这个类中的某个具体的哺乳动物,则必须说“这个哺乳动物”或“那个哺乳动物”。

在日常用语中,Webster的Third New International Dictionary将“class”定义为“由一些共同特征或一项共同特征所标识的一组、一群或一类,根据品质、资格或条件进行的分组、区分或分级”[17]

在面向对象分析和设计的上下文中,我们将类定义为:

“类是一组对象,它们拥有共同的结构、共同的行为和共同的语义。”

一个对象就是类的一个实例。

img

类是一组对象,它们拥有共同的结构和行为

什么不是一个类?一个对象不是一个类。没有共同结构和行为的对象不能够被划分为一类,因为根据定义,它们除了都是对象之外,没有别的共同点。

值得一提的是,类(正如大多数编程语言中所定义的)对于分解是必要的,但不是充分的。某些抽象非常复杂,所以不能够利用单个类定义的方式很方便地表达。例如,一种相当高层的抽象、一个GUI框架、一个数据库和整个库存系统在概念上都是独立的对象,但它们都不能表示为一个单独的类。[4]相反,最好是将这些抽象表示为一组类,这些类的实例互相协作,提供我们所期望的结构和功能。Stroustrup将这样的一组类称为一个组件[18]

3.3.2 接口和实现

Meyer[19]和Snyder[20]都指出,编程在很大程度上是一种“制定契约”:一个较大问题的不同功能通过子契约被分配给不同的设计元素,从而被分解成较小的问题。没有别的情况比在设计类时更能体现这种思想了。

一个单独的对象是一个具体实体,在整体系统中扮演某个角色,而类则记录了所有相关对象的共同结构和行为。因此,类起到的作用是在一种抽象和所有它的客户之间建立起协议。通过在类的接口中记录下这些决定,一种强类型的编程语言可以在编译时检查是否违反了这一协议。

这种编程,即契约的观点,可以区分一个类的外部视图和内部视图。一个类的接口提供了它的外部视图,因此强调了抽象,隐藏了它的结构和行为的秘密。这个接口主要由所有的操作声明构成,这些操作适用于这个类的所有对象,但它也可能包括其他类、常量、变量和异常的声明,因为这种抽象可能需要这些东西。与接口不同,类的实现是它的内部视图,它包含类行为的秘密。一个类的实现主要由类接口中定义的所有操作的实现构成。

可以进一步将类的接口分成以下四个部分。

■ 公有:所有客户都可以访问的声明。

■ 保护:只能由该类本身及其子类访问的声明。

■ 私有:只能由该类本身访问的声明。

■ 包:只能由同一个包中的类访问的声明。

这些类型的可见性的详细语义,可能根据实现的语言而有所不同。

可见性和友元

不同的编程语言提供了不同的公有、保护、私有和包可见性的组合,开发者可以进行选择,为类接口的每一部分建立具体的访问权限,从而控制客户可以看见什么、不能看见什么(即可见性)。

具体来说,C++允许开发者显式地声明这四种不同的可见性。[5] C++的友元机制允许一个类区分一些具有特权的类,让这些特权类能够看到这个类的保护部分和私有部分。友元打破了类的封装,所以在实际设计中必须慎重选择。Java没有友元但它具有某种类似的可见性类型,称为包可见性,在同一个包中的所有类都可以互相访问。除了友元之外,公有、保护和私有可见性在Java和C++中是一样的。Ada允许公有和私有可见性声明,但不支持保护可见性。在Smalltalk中,所有实例变量都是私有的,所有方法都是公有的。在Object Pascal中,字段和操作都是公有的,所以是未被封装的。

构成类的表示形式的常量和变量有各种叫法,这取决于我们使用的具体语言。例如,Smalltalk使用的是术语“实例变量(instance variable)”,Object Pascal和Java使用的是术语“字段(field)”,C++使用的是术语“数据成员(data member)”。我们将互换使用这些术语来表示类的这些部分,代表类实例的状态。

对象的状态必须在它对应的类中有某种表示形式,所以通常表示为常量或变量声明,作为类接口的保护或私有部分。通过这种方式,一个类的所有实例的共同表示形式被封装起来,对这种表示形式的修改不会在功能上影响任何外部客户。

3.3.3 类的生命周期

只要理解了一个简单类的公有操作的语义,就可以懂得这个类的行为。但对于某些更有趣的类来说(如移动DisplayItem类的一个实例,或者设置TemperatureController类的一个实例),就要涉及每个实例生命周期的不同阶段的不同操作。本章前面曾提到,这些类的实例就像一个小的自动机。由于这些实例具有共同的行为,所以可以通过类来描述这些共同的事件次序和时间次序语义。第5章中将讨论,我们可以利用有限状态自动机来描述某些有趣的类的这种动态行为。