客观世界由许多具体的事物、事件、概念和规则组成,这些均可被看成对象。面向对象(Object-Oriented,OO)方法是一种非常实用的系统化软件开发方法,它以客观世界中的对象为中心,其分析和设计思想符合人们的思维方式,分析和设计的结果与客观世界的实际比较接近,容易被人们所接受。面向对象方法有 Booch 方法、Coad 方法和 OMT 方法等。为了统一各种面向对象方法的术语、概念和模型,1997 年推出了统一建模语言(Unified Modeling Language,UML)。它是面向对象的标准建模语言,通过统一的语义和符号表示,使各种方法的建模过程和表示统一起来,现已成为面向对象建模的工业标准。
面向对象方法包括面向对象分析、面向对象设计和面向对象实现,其界线并不明显,它们采用相同的符号表示,能方便地从分析阶段平滑地过渡到设计阶段。此外,在现实生活中,用户的需求经常会发生变化,但客观世界的对象以及对象间的关系相对比较稳定,因此用面向对象方法分析和设计的结果也相对比较稳定。本章主要介绍面向对象技术的基本概念、原理及方法、UML 以及设计模式。
7.1 面向对象基础
众所周知,采用系统化的开发方法开发软件(特别是大规模软件)系统才可获得好的系统。什么是“好”系统呢?这个问题应从使用者(外部)和开发者(内部)两个角度来回答。从使用者观点出发,需要系统具有易学易用、界面友好、正确使用时能快速给出正确结果、效率高等优点,还要求系统安全可靠等;从系统开发者和管理者角度出发,要求系统易于修改和扩充、易于理解、易于测试和重用、易于与其他系统兼容和管理等。虽然不同的系统所强调的特性可能不同,但上述所要求的系统特性是基本特性。
相对而言,面向对象的开发技术更有利于开发具有上述特性的软件系统。本节简要介绍面向对象开发技术的基本概念和相关步骤。
7.1.1 面向对象的基本概念
Peter Coad 和 Edward Yourdon 提出用下面的等式识别面向对象方法。
$$ \text{面向对象} = \text{对象(Object)} + \text{分类(Classification)} + \text{继承(Inheritance)} + \text{通过消息的通信(Communication with Messages)} $$
可以说,采用这 4 个概念开发的软件系统是面向对象的。
1. 对象
在面向对象的系统中,对象是基本的运行时的实体,它既包括数据(属性),也包括作用于数据的操作(行为)。所以,一个对象把属性和行为封装为一个整体。封装是一种信息隐蔽技术,它的目的是使对象的使用者和生产者分离,使对象的定义和实现分开。从程序设计者来看,对象是一个程序模块;从用户来看,对象为他们提供了所希望的行为。在对象内的操作通常称为方法。一个对象通常可由对象名、属性和方法 3 个部分组成。
在现实世界中,每个实体都是对象,如学生、汽车、电视机和空调等都是现实世界中的对象。每个对象都有它的属性和操作,如电视机有颜色、音量、亮度、灰度、频道等属性,可以有切换频道、增大/减低音量等操作。电视机的属性值表示了电视机所处的状态,而这些属性只能通过其提供的操作来改变。电视机的各组成部分,如显像管、电路板和开关等都封装在电视机机箱中,人们不知道也不关心电视机是如何实现这些操作的。
2. 消息
对象之间进行通信的一种构造叫作消息。当一个消息发送给某个对象时,包含要求接收对象去执行某些活动的信息。接收到信息的对象经过解释,然后予以响应。这种通信机制称为消息传递。发送消息的对象不需要知道接收消息的对象如何对请求予以响应。
3. 类
一个类定义了一组大体上相似的对象。一个类所包含的方法和数据描述一组对象的共同行为和属性。把一组对象的共同特征加以抽象并存储在一个类中是面向对象技术最重要的一点。是否建立了一个丰富的类库,是衡量一个面向对象程序设计语言成熟与否的重要标志。
类是在对象之上的抽象,对象是类的具体化,是类的实例(Instance)。在分析和设计时,通常把注意力集中在类上,而不是具体的对象。也不必逐个定义每个对象,只需对类做出定义,而对类的属性进行不同赋值即可得到该类的对象实例。
类可以分为三种:实体类、接口类(边界类)和控制类。实体类的对象表示现实世界中真实的实体,如人、物等。接口类(边界类)的对象为用户提供一种与系统合作交互的方式,分为人和系统两大类,其中人的接口可以是显示屏、窗口、Web 窗体、对话框、菜单、列表框、其他显示控制、条形码、二维码或者用户与系统交互的其他方法。系统接口涉及到把数据发送到其他系统,或者从其他系统接收数据。控制类的对象用来控制活动流,充当协调者。
有些类之间存在一般和特殊关系,即一些类是某个类的特殊情况,某个类是一些类的一般情况。这是一种 is-a 关系,即特殊类是一种一般类。例如“汽车”类、“轮船”类、“飞机”类都是一种“交通工具”类。特殊类是一般类的子类,一般类是特殊类的父类。同样,“汽车”类还可以有更特殊的类,如“轿车”类、“货车”类等。在这种关系下形成一种层次的关联。
通常,把一个类和这个类的所有对象称为“类及对象”或对象类。
4. 继承
继承是父类和子类之间共享数据和方法的机制。这是类之间的一种关系,在定义和实现一个类的时候,可以在一个已经存在的类的基础上进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
一个父类可以有多个子类,这些子类都是父类的特例,父类描述了这些子类的公共属性和方法。一个子类可以继承它的父类(或祖先类)中的属性和方法,这些属性和操作在子类中不必定义,子类中还可以定义自己的属性和方法。
只从一个父类得到继承,叫作“单重继承”。如果一个子类有两个或更多个父类,则称为“多重继承”。
5. 多态
在收到消息时,对象要予以响应。不同的对象收到同一消息可以产生完全不同的结果,这一现象称为多态(Polymorphism)。在使用多态的时候,用户可以发送一个通用的消息,而实现的细节则由接收对象自行决定。这样,同一消息就可以调用不同的方法。
多态的实现受到继承的支持,利用类的继承的层次关系,把具有通用功能的消息存放在高层次,而不同的实现这一功能的行为放在较低层次,在这些低层次上生成的对象能够给通用消息以不同的响应。
多态有不同的形式,Cardelli 和 Wegner 把它分为 4 类,如下所示。其中,参数多态和包含多态称为通用的多态,过载多态和强制多态称为特定的多态。
多态├── 通用的│ ├── 参数的│ └── 包含的└── 特定的├── 过载的└── 强制的
参数多态是应用比较广泛的多态,被称为最纯的多态。包含多态在许多语言中都存在,最常见的例子就是子类型化,即一个类型是另一个类型的子类型。过载(Overloading)多态是同一个名字在不同的上下文中所代表的含义不同。
6. 动态绑定(Dynamic Binding)
绑定是一个把过程调用和响应调用所需要执行的代码加以结合的过程。在一般的程序设计语言中,绑定是在编译时进行的,叫作静态绑定。动态绑定则是在运行时进行的,因此,一个给定的过程调用和代码的结合直到调用发生时才进行。
动态绑定是和类的继承以及多态相联系的。在继承关系中,子类是父类的一个特例,所以父类对象可以出现的地方,子类对象也可以出现。因此在运行过程中,当一个对象发送消息请求服务时,要根据接收对象的具体情况将请求的操作与实现的方法进行连接,即动态绑定。
7.1.2 面向对象分析
同其他分析方法一样,面向对象分析(Object-Oriented Analysis,OOA)的目的是为了获得对应用问题的理解。理解的目的是确定系统的功能、性能要求。面向对象分析方法与功能/数据分析法之间的差别是前期的表述含义不同。功能/数据分析法分开考虑系统的功能要求和数据及其结构,面向对象分析方法是将数据和功能结合在一起作为一个综合对象来考虑。面向对象分析技术可以将系统的行为和信息间的关系表示为迭代构造特征。
面向对象分析包含 5 个活动:认定对象、组织对象、描述对象间的相互作用、确定对象的操作、定义对象的内部信息。
1. 认定对象
在应用领域中,按自然存在的实体确立对象。在定义域中,首先将自然存在的“名词”作为一个对象,这通常是研究问题、定义域实体的良好开始。通过实体间的关系寻找对象常常没有问题,困难在于寻找(选择)系统关心的实质性对象,实质性对象是系统稳定性的基础。例如在银行应用系统中,实质性对象应包含客户账务、清算等,而门卫值班表不是实质性对象,甚至可不包含在该系统中。
2. 组织对象
分析对象间的关系,将相关对象抽象成类,其目的是为了简化关联对象,利用类的继承性建立具有继承性层次的类结构。抽象类时可从对象间的操作或一个对象是另一个对象的一部分来考虑,如房子是由门和窗构成的。由对象抽象类,通过相关类的继承构造类层次,所以说系统的行为和信息间的分析过程是一种迭代表征过程。
3. 对象间的相互作用
描述出各对象在应用系统中的关系,如一个对象是另一个对象的一部分,一个对象与其他对象间的通信关系等。这样可以完整地描述每个对象的环境,由一个对象解释另一个对象,以及一个对象如何生成另一个对象,最后得到对象的界面描述。
4. 基于对象的操作
当考虑对象的界面时,自然要考虑对象的操作。其操作有从对象直接标识的简单操作,如创建、增加和删除等;也有更复杂的操作,如将几个对象的信息连接起来。一般而言,避免对象太复杂比较好,当连接的对象太复杂时,可将其标识为新对象。当确定了对象的操作后,再定义对象的内部,对象内部定义包括其内部数据信息、信息存储方法、继承关系以及可能生成的实例数等属性。
分析阶段最重要的是理解问题域的概念,其结果将影响整个工作。经验表明,从应用定义域概念标识对象是非常合理的,完成上述工作后写出规范文档,文档确定每个对象的范围。
早期面向对象的目标之一是简化模型与问题域之间的语义差距。事实上,面向对象分析的基础是软件系统结构,这依赖于人类看待现实世界的方法。当人们理解求解问题的环境时,常采用对象、分类法和层次性这类术语。面向对象分析与功能/数据分析方法相比,面向对象的结果比较容易理解和管理。面向对象分析方法的另一个优点是便于修改,早期阶段的修改容易提高软件的可靠性。
7.1.3 面向对象设计
面向对象设计(Object-Oriented Design,OOD)是将 OOA 所创建的分析模型转化为设计模型,其目标是定义系统构造蓝图。通常的情况是,由概念模型生成的分析模型被装入到相应的执行环境中时,需要考虑实现问题加以调整和增补,如根据所用编程语言是否支持多继承或继承,而调整类结构。OOA 与 OOD 之间不存在鸿沟,采用一致的概念和一致的表示法,OOD 同样应遵循抽象、信息隐蔽、功能独立、模块化等设计准则。
1. 面向对象设计的活动
OOD 在复用 OOA 模型的基础上,包含与 OOA 对应如下五个活动。
(1)识别类及对象。
(2)定义属性。
(3)定义服务。
(4)识别关系。
(5)识别包。
和所有其他设计活动一样,要使系统的结构好且效率高,做好相互间的平衡是困难的。分析模型已经提供了概念结构,它将试图长期保存。另外,设计期必须充分考虑系统的稳定性,如目标环境所需的最大存储空间、可靠性和响应时间等,所有这些都会影响系统的结构。
对象标识期间的目标是分析对象,设计过程也是发现对象的过程,称之为再处理,并补充几个新的组成部分,即与实现有关的因素:图形用户界面系统,硬件操作系统及网络,数据管理系统以及编程语言和可复用构件库等。OOD 应该尽可能隔离实现条件对系统的影响,对不可隔离的因素按实现条件调整 OOA 模型。因此,必须有从分析模型到设计模型到程序设计语言的线性转换规则。对象可以用预先开发的源代码实现,称这样的部分为构件(Component)。由于这些构件是功能和数据的组装,因此,常常用于简化面向对象环境的产生。
如前所述,面向对象系统的开发需要面向对象的程序设计风格,这与面向对象程序设计语言无直接关系。面向对象是一种程序设计风格,而不只是一种具有构造继承性、封装性和多态的程序设计语言族的命名。
2. 面向对象设计的原则
(1)单一责任原则(Single Responsibility Principle,SRP)。就一个类而言,应该仅有一个引起它变化的原因。即,当需要修改某个类的时候原因有且只有一个,让一个类只做一种类型责任。
(2)开放-封闭原则(Open \& Close Principle,OCP)。软件实体(类、模块、函数等)应该是可以扩展的,即开放的;但是不可修改的,即封闭的。
(3)里氏替换原则(Liskov Substitution Principle,LSP)。子类型必须能够替换掉他们的基类型。即,在任何父类可以出现的地方,都可以用子类的实例来赋值给父类型的引用。当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有是一个(is-a)关系。
(4)依赖倒置原则(Dependence Inversion Principle,DIP)。抽象不应该依赖于细节,细节应该依赖于抽象。即,高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
(5)接口分离原则(Interface Segregation Principle,ISP)。不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。即:依赖于抽象,不要依赖于具体,同时在抽象级别不应该有对于细节的依赖。这样做的好处就在于可以最大限度地应对可能的变化。
上述(1)~(5)是面向对象方法中的五大原则。除了这五大原则之外,Robert C. Martin 提出的面向对象设计原则还包括以下几个。
(6)重用发布等价原则(Release Reuse Equivalency Principle,REP)。重用的粒度就是发布的粒度。
(7)共同封闭原则(Common Closure Principle,CCP)。包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。
(8)共同重用原则(Common Reuse Principle,CRP)。一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中的所有类。
(9)无环依赖原则(Acyclic Dependencies Principle,ADP)。在包的依赖关系图中不允许存在环,即包之间的结构必须是一个直接的五环图形。
(10)稳定依赖原则(Stable Dependencies Principle,SDP)。朝着稳定的方向进行依赖。
(11)稳定抽象原则(Stable Abstractions Principle,SAP)。包的抽象程度应该和其稳定程度一致。
7.1.4 面向对象程序设计
程序设计范型(Programming Paradigm)是人们在程序设计时所采用的基本方式模型,决定了程序设计时采用的思维方式、使用的工具,同时又有一定的应用范畴。其发展经历了过程程序设计、模块化程序设计、函数程序设计、逻辑程序设计,发展到现在的面向对象程序设计范型。
面向对象程序设计(Object-Oriented Programming,OOP)的实质是选用一种面向对象程序设计语言(Object-Oriented Programming Language,OOPL),采用对象、类及其相关概念所进行的程序设计。它的关键在于加入了类和继承性,从而进一步提高了抽象程度。特定的 OOP 概念一般是通过 OOPL 中特定的语言机制来体现的。本节以 C++ 语言为主从程序设计的角度进一步讨论这些概念。
OOP 现在已经扩展到系统分析和软件设计的范畴,出现了面向对象分析和面向对象设计的概念,这两部分内容见本节的 7.1.2 和 7.1.3 两小节。
1. 类
通常,在介绍 OOP 的书籍或文章中总是先引入对象的概念,然后从对对象进行抽象的角度来引入类的概念。但是,当设计和实现一个面向对象的程序时,首先接触到的不是对象,而是类和类层次结构。
【例 7.1】雇员类的定义。
class Employee {private:char *Name;int Age;public:void Change(char *name, int age);void Retire();Employee(char *name, int age); // 实例生成函数(Constructor)~Employee(); // 实例消除函数(Destructor)};
类具有实例化功能,包括实例生成(由类的 Constructor 完成)和实例消除(由类的 Destructor 完成)。类的实例化功能决定了类及其实例具有下面的特征。
(1)同一个类的不同实例具有相同的数据结构,承受的是同一方法集合所定义的操作,因而具有规律相同的行为。
(2)同一个类的不同实例可以持有不同的值,因而可以具有不同的状态。
(3)实例的初始状态(初值)可以在实例化时确定。
2. 继承和类层次结构
孤立的类只能描述实体集合的特征同一性,而客观世界中实体集合的划分通常还要考虑实体特征方面有关联的相似性。在 OOP 中使用继承机制解决这一问题。
在 OOPL 中,继承一般通过定义类之间的关系来体现。
在一个面向对象系统中,子类与父类之间的继承关系构成了这个系统的类层次结构,可以用树(对应于单继承)或格(对应于多继承)这样的图来描述。
【例 7.2】经理类的定义。
class Manager : public Employee {int Level;public:void ChangeLevel(int n);Manager(char *name, int age, int level);~Manager();};
当执行一个子类的实例生成方法时,首先在类层次结构中从该子类沿继承路径上溯至它的一个基类,然后自顶向下执行该子类所有父类的实例生成方法;最后执行该子类实例生成方法的函数体。当执行一个子类的实例消除方法时,顺序恰好与执行该子类实例生成方法相反:先执行该子类实例消除方法的函数体,再沿继承路径自底向上执行该子类所有父类的实例消除方法。
与一般数据类型的实例化过程相比,类的实例化过程是一种实例的合成过程,而不仅仅是根据单个类型进行的空间分配、初始化和绑定。
OOPL 中的继承机制体现了一条重要的面向对象程序设计原则:开发人员在构造程序时不必从零开始,而只需对差别进行程序设计。支持继承也是 OOPL 与传统程序设计语言在语言机制方面最根本的区别。
3. 对象、消息传递和方法
对象是类的实例。尽管对象的表示在形式上与一般数据类型十分相似,但是它们之间存在一种本质区别:对象之间通过消息传递方式进行通信。
消息传递源是一种与通信有关的概念,OOP 使得对象具有交互能力的主要模型就是消息传递模型。对象被看成用传递消息的方式互相联系的通信实体,它们既可以接收,也可以拒绝外界发来的消息。
一般情况下,对象接收它能够识别的消息,拒绝它不能识别的消息。对于一个对象而言,任何外部的代码都不能以任何不可预知或事先不允许的方式与这个对象进行交互。
发送一条消息至少应给出一个对象的名字和要发给这个对象的那条消息的名字。通常,消息的名字就是这个对象中外界可知的某个方法的名字。在消息中,经常还有一组参数(也就是那个方法所要求的参数),将外界的有关信息传给这个对象。
对于一个类来说,它关于方法界面的定义规定了实例的消息传递协议,而它本身则决定了消息传递的合法范围。由于类是先于对象构造而成的,所以一个类为它实例提供了可以预知的交互方式。例如,假设 m1 是类 Manager 的一个实例(或对象),当外界要求把这个对象所代表的那位经理的级别改变为 2 时,就应以下面的方式向这个对象发出一条消息:
m1.ChangeLevel(2);
4. 对象自身引用
对象自身引用(self-Reference)是 OOPL 中的一种特有结构。这种结构在不同的 OOPL 中有不同的名称,在 C++ 和 Java 中称为 this,在 Smalltalk-80、Object-C 和其他一些 OOPL 中则称为 self。
以 C++ 语言为例,对于类 c 和方法 c::m,在 c::m 方法体中出现的 c 的成员名 n 将被编译程序按 this->n 来对待。这里的 this 是一个类型为 c* 的指针(在 Java 语言中 this 是一个引用),它的值由语言中的消息传递机制提供。
对象自身引用的值和类型分别扮演了两种意义的角色:对象自身引用的值使得方法体中引用的成员名与特定的对象相关,对象自身引用的类型则决定了方法体被实际共享的范围。
对象自身引用机制使得在进行方法的设计和实现时并不需要考虑与对象联系的细节,而是从更高一级的抽象层次,也就是类的角度来设计同类型对象的行为特征,从而使得方法在一个类及其子类的范围内具有共性。在程序运行过程中,消息传递机制和对象自身引用将方法与特定的对象动态地联系在一起,使得不同的对象在执行同样的方法体时,可以因对象的状态不同而产生不同的行为,从而使得方法对具体的对象具有个性。
5. 重置
重置或覆盖(Overriding)是在子类中重新定义父类中已经定义的方法,其基本思想是通过一种动态绑定机制的支持,使得子类在继承父类接口定义的前提下用适合自己要求的实现去置换父类中的相应实现。
在 OOPL 中,重置机制有相应的语法供开发人员选择使用。在 C++ 语言中,通过虚拟函数(Virtual Function)的定义来进行重置的声明,通过虚拟函数跳转表(Virtual Functions Jump Tables,VTBL)结构来实现重置方法体的动态绑定。在 Java 语言中,通过抽象方法(Abstract Method)进行重置的声明,通过方法查找(Method Lookup)实现重置方法体的动态绑定。
下面以 C++ 语言为例,说明重置机制的使用。
【例 7.3】使用重置修改类 Employee 和 Manager。
class Employee {protected:char *Name;int Age;public:void Change(char * name, int age);virtual void Retire(); // 虚拟函数Employee(char *name, int age);~Employee();};class Manager : public Employee {int Level;public:void Retire(); // 重置从父类继承的 Retire 方法virtual void ChangeLevel(int n); // 虚拟函数,供 Manager 的子类重置Manager(char *name, int age, int level);~Manager();};
6. 类属类
类属是程序设计语言中普遍注重的一种参数多态机制,在 OOPL 中也不例外。本节主要介绍与类有关的类属。
在 C++ 语言中,类属有专门的术语——template。
【例 7.4】类属类。
template <class T> class Vector {T *v;int sz;public:T& operator[](int i); // 运算符过载Vector(int VectorSize);/* ... */};
其中,<class T> 中的 class 用来说明 T 是一个类型参数,但并不意味着 T 一定是一个类,也可以是基本类型。
类属类可以看成是类的模板。一个类属类是关于一组类的一个特性抽象,它强调的是这些类的成员特征中与具体类型无关的那些部分,而与具体类型相关的那些部分则用变元来表示。这就使得对类的集合也可以按照特性的相似性再次进行划分。类属类的一个重要作用,就是对类库的建设提供了强有力的支持。
重置和类属都是一种多态机制,多态的基本概念以及多态的分类见本章 7.1 节,对于其他种类的多态不在本节进行介绍。
7. 无实例的类
在前几节中曾指出,类是对象的模板,对象是类的实例。那么是否每个类都至少有一个实例?如果在类之间没有定义继承关系,回答是肯定的。这是因为若存在没有实例的类,那么这样的类对程序的行为没有任何贡献,因而是冗余的。相反,如果存在继承关系,那么的确有可能在类层次结构的较高层次上看到始终没有实例的类。
要创建无实例的类,仍然需要语言的支持。在 C++ 和 Java 语言中,抽象类就是这样的类。在 C++ 中通过在类中定义纯虚拟函数来创建一个抽象类,在 Java 中通过将一个类声明为抽象类来创建一个抽象类,并在抽象类中定义抽象方法,抽象类中也可以没有抽象方法。
【例 7.5】用 C++ 语言创建抽象类。
class Person { // 抽象类protected:char *Name;/* ... */public:void Change(char *name, int age);virtual void PrintOn() = 0; // 纯虚拟函数Person();Person(char *name, int age, char *addr, char *tele); // 过载的构造函数~Person();};
【例 7.6】用 Java 语言创建抽象类。
abstract class Person { // 抽象类protected String Name;/* ... */public void Change(String name, int age) { /* ... */ }public abstract void PrintOn(); // 抽象方法public Person() { /* ... */ }public Person(String name, int age, String addr, String tele) { /* ... */ }}
7.1.5 面向对象测试
就测试而言,用面向对象方法开发的系统测试与其他方法开发的系统测试没有什么不同,在所有开发系统中都是根据规范说明来验证系统设计的正确性。程序验证应尽可能早地开始。程序调试步骤是从最底层开始,从单元测试、综合测试到系统测试。单元测试是系统构件的分体测试。将测试好的系统构件接起来看它们之间相互作用的正确性称为综合测试。最后是整个系统的测试,包括软件系统所在相关环境的测试。通常综合测试是一种“主攻”活动,在系统开发期是非常关键的。这一阶段应随时连接已开发的每一部分,再看它们的实际工作,这种“主攻”活动在面向对象系统中是一种实质性的、渐渐增长的测试策略。
面向对象的系统由一些对象和它们相互间的通信组成,这些对象包括了它们的数据属性和操作(动作),比传统系统开发方法中独立工作的子程序大。对象的操作局限于特定数据,一个对象的所有操作自然由同一个设计者开发。这导致单元测试比传统系统的单元大,综合测试尽可能在早期阶段处理,因为通信是系统开发的实质。所有对象有预定义的界面,这也有利于综合测试。当综合测试继续到较高层次时,那么越来越多的对象就会被逐步连接起来。面向对象的综合测试是由底向上的测试。
一般来说,对面向对象软件的测试可分为下列 4 个层次进行。
(1)算法层。测试类中定义的每个方法,基本上相当于传统软件测试中的单元测试。
(2)类层。测试封装在同一个类中的所有方法与属性之间的相互作用。在面向对象软件中类是基本模块,因此可以认为这是面向对象测试中所特有的模块测试。
(3)模板层。测试一组协同工作的类之间的相互作用,大体上相当于传统软件测试中的集成测试,但是也有面向对象软件的特点(例如,对象之间通过发送消息相互作用)。
(4)系统层。把各个子系统组装成完整的面向对象软件系统,在组装过程中同时进行测试。
软件工程中传统的测试用例设计技术,如逻辑覆盖、等价类划分和边界值分析等方法,仍然可以作为测试类中每个方法的主要技术。面向对象测试的主要目标也是用尽可能低的测试成本和尽可能少的测试用例,发现尽可能多的错误。但是,面向对象程序中特有的封装、继承和多态等机制,也给面向对象测试带来一些新特点,增加了测试和调试的难度。
7.2 UML
面向对象分析强调的是对一个系统中对象的特征和行为的定义。目前,国际上已经出现了多种面向对象的方法,例如 Peter Coad 和 Edward Yourdon 的 OOA 和 OOD 方法、Booch 的 OOD 方法、OMT(Object Modeling Technique,面向对象建模技术)方法及 UML(Unified Modeling Language,统一建模语言)。本节将对目前业界普遍接受的 UML 方法进行简要介绍。
统一建模语言是面向对象软件的标准化建模语言。由于其简单、统一,又能够表达软件设计中的动态和静态信息,目前已经成为可视化建模语言事实上的工业标准,当前版本为 2.4.1。
从企业信息系统到基于 Web 的分布式应用,甚至严格的实时嵌入式系统都适合用 UML 来建模。它是一种富有表达力的语言,可以描述开发所需要的各种视图,然后以此为基础装配系统。UML 由 3 个要素构成:UML 的基本构造块、支配这些构造块如何放置在一起的规则和运用与整个语言的一些公共机制。限于篇幅,下面仅对 UML 中的基本构造块进行讨论。
UML 的词汇表包含 3 种构造块:事物、关系和图。事物是对模型中最具有代表性的成分的抽象;关系把事物结合在一起;图聚集了相关的事物。
7.2.1 事物
UML 中有 4 种事物:结构事物、行为事物、分组事物和注释事物。
(1)结构事物(Structural Thing)。结构事物是 UML 模型中的名词。它们通常是模型的静态部分,描述概念或物理元素。结构事物包括类(Class)、接口(Interface)、协作(Collaboration)、用例(Use Case)、主动类(Active Class)、构件(Component)、制品(Artifact)和结点(Node)。
(2)行为事物(Behavior thing)。行为事物是 UML 模型的动态部分。它们是模型中的动词,描述了跨越时间和空间的行为。行为事物包括交互(Interaction)、状态机(State Machine)和活动(Activity)。
交互由在特定语境中共同完成一定任务的一组对象之间交换的消息组成。一个对象群体的行为或单个操作的行为可以用一个交互来描述。交互涉及一些其他元素,包括消息、动作序列(由一个消息所引起的行为)和链(对象间的连接)。在图形上,把一个消息表示为一条有向直线,通常在表示消息的线段上总有操作名。
状态机描述了一个对象或一个交互在生命期内响应事件所经历的状态序列。单个类或一组类之间协作的行为可以用状态机来描述。一个状态机涉及到一些其他元素,包括状态、转换(从一个状态到另一个状态的流)、事件(触发转换的事物)和活动(对一个转换的响应)。在图形上,把状态表示为一个圆角矩形,通常在圆角矩形中含有状态的名称及其子状态。
活动是描述计算机过程执行的步骤序列,注重步骤之间的流而不关心哪个对象执行哪个步骤。活动的一个步骤称为一个动作。在图形上,把动作画成一个圆角矩形,在其中含有指明其用途的名字。状态和动作靠不同的语境得以区别。
交互、状态机和活动是可以包含在 UML 模型中的基本行为事物。在语义上,这些元素通常与各种结构元素(主要是类、协作和对象)相关。
(3)分组事物(Grouping Thing)。分组事物是 UML 模型的组织部分,是一些由模型分解成的“盒子”。在所有的分组事物中,最主要的分组事物是包(Package)。包是把元素组织成组的机制,这种机制具有多种用途。结构事物、行为事物甚至其他分组事物都可以放进包内。包与构件(仅在运行时存在)不同,它纯粹是概念上的(即它仅在开发时存在)。
(4)注释事物(Annotational Thing)。注释事物是 UML 模型的解释部分。这些注释事物用来描述、说明和标注模型的任何元素。注解(Note)是一种主要的注释事物。注解是一个依附于一个元素或者一组元素之上,对它进行约束或解释的简单符号。
7.2.2 关系
UML 中有 4 种关系:依赖、关联、泛化和实现。
(1)依赖(Dependency)。依赖是两个事物间的语义关系,其中一个事物(独立事物)发生变化会影响另一个事物(依赖事物)的语义。在图形上,把一个依赖画成一条可能有方向的虚线。
(2)关联(Association)。关联是一种结构关系,它描述了一组链,链是对象之间的连接。聚集(Aggregation)是一种特殊类型的关联,它描述了整体和部分间的结构关系。
在关联上可以标注重复度(Multiplicity)和角色(Role)。
(3)泛化(Generalization)。泛化是一种特殊/一般关系,特殊元素(子元素)的对象可替代一般元素(父元素)的对象。用这种方法,子元素共享了父元素的结构和行为。在图形上,把一个泛化关系画成一条带有空心箭头的实线,它指向父元素。
(4)实现(Realization)。实现是类元之间的语义关系,其中一个类元指定了由另一个类元保证执行的契约。在两种情况下会使用实现关系:一种是在接口和实现它们的类或构件之间;另一种是在用例和实现它们的协作之间。在图形上,把一个实现关系画成一条带有空心箭头的虚线。
这 4 种关系是 UML 模型中可以包含的基本关系事物。它们也有变体,例如,依赖的变体有精化、跟踪、包含和延伸。
7.2.3 UML 中的图
图(Diagram)是一组元素的图形表示,大多数情况下把图画成顶点(代表事物)和弧(代表关系)的连通图。为了对系统进行可视化,可以从不同的角度画图,这样图是对系统的投影。
UML 2.0 提供了 13 种图,分别是类图、对象图、用例图、序列图、通信图、状态图、活动图、构件图、组合结构图、部署图、包图、交互概览图和计时图。序列图、通信图、交互概览图和计时图均被称为交互图。
1. 类图
类图(Class Diagram)展现了一组对象、接口、协作和它们之间的关系。在面向对象系统的建模中所建立的最常见的图就是类图。类图给出系统的静态设计视图。包含主动类的类图给出了系统的静态进程视图。
类图中通常包括下述内容:
(1)类。
(2)接口。
(3)协作。
(4)依赖、泛化和关联关系。
类图中也可以包含注解和约束。类图还可以含有包或子系统,二者都用于把模型元素聚集成更大的组块。
类图用于对系统的静态设计视图建模。这种视图主要支持系统的功能需求,即系统要提供给最终用户的服务。当对系统的静态设计视图建模时,通常以下述 3 种方式之一使用类图。
(1)对系统的词汇建模。对系统的词汇建模涉及做出这样的决定:哪些抽象是考虑中的系统的一部分,哪些抽象处于系统边界之外。用类图详细描述这些抽象和它们的职责。
(2)对简单的协作建模。协作是一些共同工作的类、接口和其他元素的群体,该群体提供的一些合作行为强于所有这些元素的行为之和。例如,当对分布式系统的事务语义建模时,不能仅仅盯着一个单独的类来推断要发生什么,而要有相互协作的一组类来实现这些语义。用类图对这组类以及它们之间的关系进行可视化和详述。
(3)对逻辑数据库模式建模。将模式看作为数据库的概念设计的蓝图。在很多领域中,要在关系数据库或面向对象数据库中存储永久信息,可以用类图对这些数据库的模式建模。
2. 对象图
对象图(Object Diagram)展现了某一时刻一组对象以及它们之间的关系,描述了在类图中所建立的事物的实例的静态快照。对象图一般包括对象和链。
和类图一样,对象图给出系统的静态设计视图或静态进程视图,但它们是从真实的或原型实例的角度建立的。这种视图主要支持系统的功能需求,即系统应该提供给最终用户的服务。利用对象图可以对静态数据结构建模。
在对系统的静态设计视图或静态进程视图建模时,主要是使用对象图对对象结构进行建模。对象结构建模涉及在给定时刻抓取系统中对象的快照。对象图表示了交互图表示的动态场景的一个静态画面,可以使用对象图可视化、详述、构造和文档化系统中存在的实例以及它们之间的相互关系。
3. 用例图
用例图(Use Case Diagram)展现了一组用例、参与者(Actor)以及它们之间的关系。
用例图通常包括以下内容:
(1)用例。
(2)参与者。
(3)用例之间的扩展关系(<<extend>>)和包含关系(<<include>>),参与者和用例之间的关联关系,用例与用例以及参与者与参与者之间的泛化关系。
用例图用于对系统的静态用例视图进行建模。这个视图主要支持系统的行为,即该系统在它的周边环境的语境中所提供的外部可见服务。
当对系统的静态用例视图建模时,可以用下列两种方式来使用用例图。
(1)对系统的语境建模。对一个系统的语境进行建模,包括围绕整个系统画一条线,并声明有哪些参与者位于系统之外并与系统进行交互。在这里,用例图说明了参与者以及它们所扮演的角色的含义。
(2)对系统的需求建模。对一个系统的需求进行建模,包括说明这个系统应该做什么(从系统外部的一个视点出发),而不考虑系统应该怎样做。在这里,用例图说明了系统想要的行为。通过这种方式,用例图使我们能够把整个系统看作一个黑盒子,采用矩形框表示系统边界;可以观察到系统外部有什么,系统怎样与哪些外部事物相互作用,但却看不到系统内部是如何工作的。
4. 交互图
交互图用于对系统的动态方面进行建模。一张交互图表现的是一个交互,由一组对象和它们之间的关系组成,包含它们之间可能传递的消息。交互图表现为序列图、通信图、交互概览图和计时图,每种针对不同的目的,能适用于不同的情况。序列图是强调消息时间顺序的交互图;通信图是强调接收和发送消息的对象的结构组织的交互图;交互概览图强调控制流的交互图。
交互图用于对一个系统的动态方面建模。在多数情况下,它包括对类、接口、构件和结点的具体的或原型化的实例以及它们之间传递的消息进行建模,所有这些都位于一个表达行为的脚本的语境中。交互图可以单独使用,来可视化、详述、构造和文档化一个特定的对象群体的动态方面,也可以用来对一个用例的特定的控制流进行建模。
交互图一般包含对象、链和消息。
1)序列图
序列图(Sequence Diagram)是场景(Scenario)的图形化表示,描述了以时间顺序组织的对象之间的交互活动。形成序列图时,首先把参加交互的对象放在图的上方,沿水平方向排列。通常把发起交互的对象放在左边,下级对象依次放在右边。然后,把这些对象发送和接收的消息沿垂直方向按时间顺序从上到下放置。这样,就提供了控制流随时间推移的清晰的可视化轨迹。
序列图有两个不同于通信图的特征。
(1)序列图有对象生命线。对象生命线是一条垂直的虚线,表示一个对象在一段时间内存在。在交互图中出现的大多数对象存在于整个交互过程中,所以这些对象全都排列在图的顶部,其生命线从图的顶部画到图的底部。但对象也可以在交互过程中创建,它们的生命线从接收到构造型为 create 的消息时开始。对象也可以在交互过程中撤销,它们的生命线在接收到构造型为 destroy 的消息时结束(并且给出一个大 X 的标记表明生命的结束)。
(2)序列图有控制焦点。控制焦点是一个瘦高的矩形,表示一个对象执行一个动作所经历的时间段,既可以是直接执行,也可以是通过下级过程执行。矩形的顶部表示动作的开始,底部表示动作的结束(可以由一个返回消息来标记)。还可以通过将另一个控制焦点放在它的父控制焦点的右边来显示(由循环、自身操作调用或从另一个对象的回调所引起的)控制焦点的嵌套(其嵌套深度可以任意)。如果想特别精确地表示控制焦点在哪里,也可以在对象的方法被实际执行(并且控制还没传给另一个对象)期间将那段矩形区域阴影化。
2)通信图
通信图(Communication Diagram)强调收发消息的对象的结构组织,在早期的版本中也被称作协作图。通信图强调参加交互的对象的组织。产生一张通信图时,首先要将参加交互的对象作为图的顶点,然后把连接这些对象的链表示为图的弧,最后用对象发送和接收的消息来修饰这些链。这就提供了在协作对象的结构组织的语境中观察控制流的一个清晰的可视化轨迹。
通信图有两个不同于序列图的特性。
(1)通信图有路径。为了指出一个对象如何与另一个对象链接,可以在链的末端附上一个路径构造型(如构造型 local,表示指定对象对发送者而言是局部的)。通常只需要显式地表示以下几种链的路径:local(局部)、parameter(参数)、global(全局)以及 self(自身),但不必表示 association(关联)。
(2)通信图有顺序号。为表示一个消息的时间顺序,可以给消息加一个数字前缀(从 1 号消息开始),在控制流中,每个新消息的顺序号单调增加(如 2、3 等)。为了显示嵌套,可使用带小数点的号码(1 表示第一个消息;1.1 表示嵌套在消息 1 中的第一个消息,1.2 表示嵌套在消息 1 中的第二个消息,等等)。嵌套可为任意深度。还要注意的是,沿同一个链可以显示许多消息(可能发自不同的方向),并且每个消息都有唯一的一个顺序号。
序列图和通信图是同构的,它们之间可以相互转换。
3)交互概览图
交互概览图(Interaction Overview Diagram)是 UML 2.0 新增的交互图之一,它是活动图的变体,描述业务过程中的控制流概览,软件过程中的详细逻辑概览,以及将多个图进行连接,抽象掉了消息和生命线。它使用活动图的表示法。纯粹的交互概览图中所有的活动都是交互发生。
4)计时图
计时图(Timing Diagram)是另一种新增的、特别适合实时和嵌入式系统建模的交互图,关注沿着线性时间轴、生命线内部和生命线之间的条件改变。它描述对象状态随着时间改变的情况,很像示波器,适合分析周期和非周期性任务。
5. 状态图
状态图(State Diagram)展现了一个状态机,它由状态、转换、事件和活动组成。状态图关注系统的动态视图,对于接口、类和协作的行为建模尤为重要,强调对象行为的事件顺序。
状态图通常包括简单状态和组合状态、转换(事件和动作)。状态是指对象的生命周期中某个条件或者状态,在此期间对象将满足某些条件、执行某些活动或等待某些事件,是对象执行了一系列活动的结果,当某个事件发生后,对象的状态将发生变化。嵌套在另外一个状态中的状态称为子状态,含有子状态的状态称为组合状态。转换是两个状态之间的一种关系,表示对象将在源状态中执行一定的动作,并在某个特定事件发生而且某个特定的警界(监护)条件满足时进入目标状态。动作是一个可执行的原子操作,是不可中断的,其执行时间是可忽略不计的。直接通过进入节点进入状态,通过退出节点可以结束状态。使用历史状态记住从组合状态中退出时所处的子状态,作用是当再次进入组合状态时,可以直接进入这个子状态,而不是再次从组合状态的初态开始。状态图可以分为区域,而区域又包括退出或者当前执行的子状态,说明组合状态可以在某一时刻同时到达多个子状态,此时通常在其前后使用 fork 和 join 标识。
可以用状态图对系统的动态方面建模。这些动态方面可以包括出现在系统体系结构的任何视图中的任何一种对象的按事件排序的行为,这些对象包括类(各主动类)、接口、构件和结点。
当对系统、类或用例的动态方面建模时,通常是对反应型对象建模。
一个反应型或事件驱动的对象是这样一个对象,其行为通常是由对来自语境外部的事件做出反应来刻画的。反应型对象在接收到一个事件之前通常处于空闲状态。当它接收到一个事件时,它的反应常常依赖于以前的事件。在这个对象对事件做出反应后,它就又变成闲状态,等待下一个事件。对于这种对象,将着眼于对象的稳定状态、能够触发从状态到状态的转换的事件,以及当每个状态改变时所发生的动作。
6. 活动图
活动图(Activity Diagram)是一种特殊的状态图,它展现了在系统内从一个活动到另一个活动的流程。活动图专注于系统的动态视图,它对于系统的功能建模特别重要,并强调对象间的控制流程。
活动图一般包括活动状态和动作状态、转换和对象。
用活动图建模的控制流中,会发生一些事情。可能要对一个设置属性值或返回一些值的表达式求值;也可能要调用对象上的操作,发送一个消息给对象,甚至创建或销毁对象,这些可执行的原子计算被称作动作状态,因为它们是该系统的状态,每个原子计算都代表一个动作的执行。动作状态不能被分解。动作状态是原子的,也就是说事件可以发生,但动作状态的工作不能被中断。最后,动作状态的工作所占用的执行时间一般被看作是可忽略的。
活动状态能够进一步被分解,它们的活动由其他的活动图表示。活动状态不是原子的,它们可以被中断。并且,一般来说,还要考虑到它需要花费一段时间来完成。可以把一个动作状态看作一个活动状态的特例。类似地,可以把一个活动状态看作一个组合,它的控制流由其他的活动状态和动作状态组成。
活动图可以表示分支、合并、分岔和汇合。分支描述基于布尔表达式的可选择路径,可有一个入流和两个或多个出流,在每个出流上放置一个布尔表达式条件,每个出流的条件不应该重叠,但需要覆盖所有可能性。合并描述当两条控制路径重新合并时,不需要监护条件,只有一个出流。分岔描述把一个控制流分成两个或多个并发控制流,可以有一个进入转移和两个或多个离去转移,每个离去的转移表示一个独立的控制流,这些流可以并行的进行。汇合表示两个或多个并发控制流的同步,可以有两个或多个进入转移和一个离去转移,意味着每个进入流都等待,直到所有进入流都达到这个汇合处。
当对一个系统的动态方面建模时,通常有两种使用活动图的方式。
(1)对工作流建模。此时所关注的是与系统进行协作的参与者所观察到的活动。工作流常常位于软件系统的边缘,用于可视化、详述、构造和文档化开发系统所涉及的业务过程。在活动图的这种用法中,对对象流的建模是特别重要的,常采用泳道将活动图中的活动状态分组。
(2)对操作建模。此时是把活动图作为流程图使用,对一个计算的细节部分建模。在活动图的这种用法中,对分支、合并、分岔和汇合状态的建模是特别重要的。用于这种方式的活动图语境包括该操作的参数和它的局部对象。
7. 构件图
构件图(Component Diagram)展现了一组构件之间的组织和依赖。构件图专注于系统的静态实现视图。它与类图相关,通常把构件映射为一个或多个类、接口或协作。
8. 组合结构图
组合结构图(Composite Structure Diagram)用于描述一个分类器(如类、构件或用例)的内部结构,分类器与系统中其他组成部分之间的交互端口,展示一组相互协作的实例如何完成特定的任务,描述设计、架构模式或策略。
9. 部署图
部署图(Deployment Diagram)是用来对面向对象系统的物理方面建模的方法,展现了运行时处理结点以及其中构件(制品)的配置。部署图对系统的静态部署视图进行建模,它与构件图相关。通常,一个结点是一个在运行时存在并代表一项计算资源的物理元素,至少拥有一些内容,常常具有处理能力,包含一个或多个构件。其中,<<artifact>> 表示制品。
10. 包图
包图(Package Diagram)是用于把模型本身组织成层次结构的通用机制,不能执行,展现由模型本身分解而成的组织单元以及其间的依赖关系。
包可以拥有其他元素,可以是类、接口、构件、结点、协作、用例和图,甚至是嵌套的其他包。拥有是一种组成关系,是一种按规模来处理问题的重要机制,也意味着元素被声明在包中,一个元素只能被一个包所拥有,拥有关系的包形成了一个命名空间,其中同一种元素的名称必须唯一。
7.3 设计模式
7.3.1 设计模式的要素
“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。设计模式的核心在于提供了相关问题的解决方案,使得人们可以更加简单方便地复用成功的设计和体系结构。
设计模式一般有以下 4 个基本要素。
(1)模式名称(Pattern Name)。一个助记名,它用一两个词来描述模式的问题、解决方案和效果。命名一个新的模式增加了设计词汇。设计模式允许在较高的抽象层次上进行设计。基于一个模式词汇表,就可以讨论模式并在编写文档时使用它们。模式名可以帮助人们思考,便于人们与其他人交流设计思想及设计结果。找到恰当的模式名也是设计模式工作的难点之一。
(2)问题(Problem)。问题描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,可能描述了特定的设计问题,如怎样用对象表示算法等;也可能描述了导致不灵活设计的类或对象结构。有时候,问题部分会包括使用模式必须满足的一系列先决条件。
(3)解决方案(Solution)。解决方案描述了设计的组成成分、它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合。所以解决方案并不描述一个特定的具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。
(4)效果(Consequences)。效果描述了模式应用的效果及使用模式应权衡的问题。尽管描述设计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模式效果包括它对系统的灵活性、扩充性或可移植性的影响,显式地列出这些效果对理解和评价这些模式很有帮助。
设计模式确定了所包含的类和实例,它们的角色、协作方式以及职责分配。每一个设计模式都集中于一个特定的面向对象设计问题或设计要点,描述了什么时候使用它,在另一些设计约束条件下是否还能使用,以及使用的效果和如何取舍。按照设计模式的目的可以分为三大类,如表 7-1 所示。
创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
表 7-1 设计模式分类
| 范围 | 创建型 | 结构型 | 行为型 |
|---|---|---|---|
| 类 | Factory Method | Adapter(类) | Interpreter Template Method |
| 对象 | Abstract Factory Builder Prototype Singleton |
Adapter(对象) Bridge Composite Decorator Facade Flyweight Proxy |
Chain of Responsibility Command Iterator Mediator Memento Observer State Strategy Visitor |
7.3.2 创建型设计模式
创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(Hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类。
在这些模式中有两个不断出现的主旋律。
第一,它们都将关于该系统使用哪些具体的类的信息封装起来。
第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。
因此,创建型模式在什么被创建,谁创建它,它是怎样被创建的,以及何时创建这些方面给予了很大的灵活性。它们允许用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。
1. Abstract Factory(抽象工厂)
1)意图
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
2)结构
抽象工厂模式的角色构成如下:
AbstractFactory:声明一个创建抽象产品对象的操作接口。ConcreteFactory:实现创建具体产品对象的操作。AbstractProduct:为一类产品对象声明一个接口。ConcreteProduct:定义一个将被相应的具体工厂创建的产品对象,实现AbstractProduct接口。Client:仅使用由AbstractFactory和AbstractProduct类声明的接口。
3)适用性
Abstract Factory 模式适用于:
- 一个系统要独立于它的产品的创建、组合和表示时。
- 一个系统要由多个产品系列中的一个来配置时。
- 当要强调一系列相关的产品对象的设计以便进行联合使用时。
- 当提供一个产品类库,只想显示它们的接口而不是实现时。
2. Builder(生成器)
1)意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
2)结构
生成器模式的角色构成如下:
Builder:为创建一个Product对象的各个部件指定抽象接口。ConcreteBuilder:实现Builder的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,提供一个检索产品的接口。Director:构造一个使用Builder接口的对象。Product:表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程。包含定义组成组件的类,包括将这些组件装配成最终产品的接口。
3)适用性
Builder 模式适用于:
- 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
- 当构造过程必须允许被构造的对象有不同的表示时。
3. Factory Method(工厂方法)
1)意图
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
2)结构
工厂方法模式的角色构成如下:
Product:定义工厂方法所创建的对象的接口。ConcreteProduct:实现Product接口。Creator:声明工厂方法,该方法返回一个Product类型的对象。Creator也可以定义一个工厂方法的默认实现,它返回一个默认的ConcreteProduct对象,可以调用工厂方法以创建一个Product对象。ConcreteCreator:重定义工厂方法以返回一个ConcreteProduct实例。
3)适用性
Factory Method 模式适用于:
- 当一个类不知道它所必须创建的对象的类的时候。
- 当一个类希望由它的子类来指定它所创建的对象的时候。
- 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
4. Prototype(原型)
1)意图
用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
2)结构
原型模式的角色构成如下:
Prototype:声明一个复制自身的接口。ConcretePrototype:实现一个复制自身的操作。Client:让一个原型复制自身从而创建一个新的对象。
3)适用性
Prototype 模式适用于:
- 当一个系统应该独立于它的产品创建、构成和表示时。
- 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
- 为了避免创建一个与产品类层次平行的工厂类层次时。
- 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们,可能比每次用合适的状态手工实例化该类更方便一些。
5. Singleton(单例)
1)意图
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
2)结构
单例模式指定一个 Instance 操作,允许客户访问它的唯一实例,Instance 是一个类操作;可能负责创建它自己的唯一实例。
3)适用性
Singleton 模式适用于:
- 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
- 当这个唯一实例应该是通过子类化可扩展的,并且客户无须更改代码就能使用一个扩展的实例时。
6. 创建型模式比较
用于系统创建的那些对象的类对系统进行参数化有两种常用方法:生成创建对象的类的子类和对系统进行参数化的方法。前者对应于使用 Factory Method 模式,其主要缺点是仅为了改变产品类就可能需要创建一个新的子类。这种改变可能级联发生,例如,如果产品的创建者本身是一个工厂方法创建的,那么也必须重定义它的创建者。后者更多地依赖于对象的复合,定义一个对象负责明确产品对象的类,并将它作为该系统的参数。这是 Abstract Factory、Builder 和 Prototype 模式的关键特征,都涉及创建一个新的负责创建产品对象的“工厂对象”。Abstract Factory 由这个工厂对象产生多个对象。Builder 由这个工厂对象使用一个相对复杂的协议,逐步创建一个复杂产品。Prototype 由该工厂对象通过复制原型对象来创建产品对象。在这种情况下,由于原型负责返回产品对象,所以工厂对象和原型是同一个对象。
7.3.3 结构型设计模式
结构型设计模式涉及如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。其中一个例子是类形式的 Adapter 模式。一般来说,适配器使得一个接口与其他接口兼容,从而给出了多个不同接口的统一抽象。为此,类适配器对一个 adaptee 类进行私有继承。这样,适配器就可以用 adaptee 的接口表示它的接口。
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可以在运行时刻改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
Composite 模式是结构型对象模式的一个实例。它描述了如何构造一个类层次式结构,这一结构由两种类型的对象所对应的类构成。其中的组合对象使得用户可以组合基元对象以及其他的组合对象,从而形成任意复杂的结构。在 Proxy 模式中,proxy 对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式,例如可以在局部空间中代表一个远程地址空间中的对象,也可以表示一个要求被加载的较大的对象,还可以用来保护对敏感对象的访问。Proxy 模式还提供了对对象的一些特有性质的一定程度上的间接访问,从而可以限制、增强或修改这些性质。Flyweight 模式为了共享对象定义了一个结构。至少有两个原因要求对象共享:效率和一致性。Flyweight 的对象共享机制主要强调对象的空间效率。使用很多对象的应用必须考虑每一个对象的开销。使用对象共享而不是进行对象复制,可以节省大量的空间资源。但是,仅当这些对象没有定义与上下文相关的状态时,它们才可以被共享。Flyweight 的对象没有这样的状态。任何执行任务时需要的其他一些信息仅当需要时才传递过去。由于不存在与上下文相关的状态,因此 Flyweight 对象可以被自由地共享。
如果说 Flyweight 模式说明了如何生成很多较小的对象,那么 Facade 模式则描述了如何用单个对象表示整个子系统。模式中的 facade 用来表示一组对象,facade 的职责是将消息转发给它所表示的对象。Bridge 模式将对象的抽象和其实现分离,从而可以独立地改变它们。
Decorator 模式描述了如何动态地为对象添加职责。Decorator 模式是一种结构型模式,这一模式采用递归方式组合对象,从而允许添加任意多的对象职责。例如,一个包含用户界面组件的 Decorator 对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口滚动和缩放这样的功能添加到组件中。可以将一个 Decorator 对象嵌套在另外一个对象中,就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个 Decorator 对象必须与其组件的接口兼容并且保证将消息传递给它。Decorator 模式在转发一条信息之前或之后都可以完成它的工作(例如绘制组件的边框)。许多结构型模式在某种程度上具有相关性。
1. Adapter(适配器)
1)意图
将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
2)结构
类适配器使用多重继承对一个接口与另一个接口进行匹配。对象适配器依赖于对象组合。
其中:
Target:定义Client使用的与特定领域相关的接口。Client:与符合Target接口的对象协同。Adaptee:定义一个已经存在的接口,这个接口需要适配。Adapter:对Adaptee的接口与Target接口进行适配。
3)适用性
Adapter 模式适用于:
- 想使用一个已经存在的类,而它的接口不符合要求。
- 想创建一个可以服用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- (仅适用于对象 Adapter)想使用一个已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
2. Bridge(桥接)
1)意图
将抽象部分与其实现部分分离,使它们都可以独立地变化。
2)结构
桥接模式的角色构成如下:
Abstraction:定义抽象类的接口,维护一个指向Implementor类型对象的指针。RefinedAbstraction:扩充由Abstraction定义的接口。Implementor:定义实现类的接口,该接口不一定要与Abstraction的接口完全一致;事实上这两个接口可以完全不同。一般来说,Implementor接口仅提供基本操作,而Abstraction定义了基于这些基本操作的较高层次的操作。ConcreteImplementor:实现Implementor接口并定义它的具体实现。
3)适用性
Bridge 模式适用于:
- 不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如,这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
- 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这是 Bridge 模式使得开发者可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
- 对一个抽象的实现部分的修改应对客户不产生影响,即客户代码不必重新编译。
- (C++)想对客户完全隐藏抽象的实现部分。
- 有许多类要生成的类层次结构。
- 想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。
3. Composite(组合)
1)意图
将对象组合成树型结构以表示“部分-整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。
2)结构
组合模式的角色构成如下:
Component:为组合中的对象声明接口;在适当情况下实现所有类共有接口的默认行为;声明一个接口用于访问和管理Component的子组件;(可选)在递归结构中定义一个接口,用于访问一个父组件,并在合适的情况下实现它。Leaf:在组合中表示叶结点对象,叶结点没有子结点;在组合中定义图元对象的行为。Composite:定义有子组件的那些组件的行为;存储子组件;在Component接口中实现与子组件有关的操作。Client:通过Component接口操纵组合组件的对象。
3)适用性
Composite 模式适用于:
- 想表示对象的部分-整体层次结构。
- 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
4. Decorator(装饰)
1)意图
动态地给一个对象添加一些额外的职责。就增加功能而言,Decorator 模式比生成子类更加灵活。
2)结构
装饰模式的结构如图 7-34 所示。
其中:
- Component 定义一个对象接口,可以给这些对象动态地添加职责。
- ConcreteComponent 定义一个对象,可以给这个对象添加一些职责。
- Decorator 维持一个指向 Component 对象的指针,并定义一个与 Component 接口一致的接口。
- ConcreteDecorator 向组件添加职责。
3)适用性
Decorator 模式适用于:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤销的职责。
- 当不能采用生成子类的方式进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是,由于类定义被隐藏,或类定义不能用于生成子类。
5. Facade(外观)
1)意图
为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
2)结构
外观模式的结构如图 7-35 所示。
其中:
- Facade 知道哪些子系统类负责处理请求;将客户的请求代理给适当的子系统对象。
- Subsystem classes 实现子系统的功能;处理有 Facade 对象指派的任务;没有 Facade 的任何相关信息,即没有指向 Facade 的指针。
3)适用性
Facade 模式适用于:
- 要为一个复杂子系统提供一个简单接口时,子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类,这使得子系统更具有可重用性,也更容易对子系统进行定制,但也给那些不需要定制子系统的用户带来一些使用上的困难。Facade 可以提供一个简单的默认视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过 Facade 层。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 Facade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
- 当需要构建一个层次结构的子系统时,使用 Facade 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,则可以让它们仅通过 Facade 进行通信,从而简化了它们之间的依赖关系。
6. Flyweight(享元)
1)意图
运用共享技术有效地支持大量细粒度的对象。
2)结构
享元模式的结构如图 7-36 所示。
其中:
- Flyweight 描述一个接口,通过这个接口 Flyweight 可以接受并作用于外部状态。
- ConcreteFlyweight 实现 Flyweight 接口,并为内部状态(如果有)增加存储空间。ConcreteFlyweight 对象必须是可共享的。它所存储的状态必须是内部的,即它必须独立于 ConcreteFlyweight 对象的场景。
- 并非所有的 Flyweight 子类都需要被共享。Flyweight 接口使共享成为可能,但它并不强制共享。在 Flyweight 对象结构的某些层次,UnsharedConcreteFlyweight 对象通常将 ConcreteFlyweight 对象作为子结点。
- FlyweightFactory 创建并管理 Flyweight 对象;确保合理地共享 Flyweight,当用户请求一个 Flyweight 时,FlyweightFactory 对象提供一个已创建的实例或者在不存在时创建一个实例。
- Client 维持一个对 Flyweight 的引用;计算或存储一个或多个 Flyweight 的外部状态。
3)适用性
Flyweight 模式适用于:
- 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖于对象标识。由于 Flyweight 对象可以被共享,所以对于概念上明显有别的对象,标识测试将返回真值。
7. Proxy(代理)
1)意图
为其他对象提供一种代理以控制对这个对象的访问。
2)结构
代理模式的结构如图 7-37 所示。
其中:
- Proxy 保存一个引用使得代理可以访问实体;提供一个与 Subject 的接口相同的接口,使代理可以用来代替实体;控制对实体的存取,并可能负责创建和删除它;其他功能依赖于代理的类型:Remote Proxy 负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求;Virtual Proxy 可以缓存实体的附加信息,以便延迟对它的访问;Protection Proxy 检查调用者是否具有实现一个请求所必需的访问权限。
- Subject 定义 RealSubject 和 Proxy 的共用接口,这样就在任何使用 RealSubject 的地方都可以使用 Proxy。
- RealSubject 定义 Proxy 所代表的实体。
3)适用性
Proxy 模式适用于在需要比较通用和复杂的对象指针代替简单的指针的时候,常见情况有:
- 远程代理(Remote Proxy)为一个对象在不同地址空间提供局部代表。
- 虚代理(Virtual Proxy)根据需要创建开销很大的对象。
- 保护代理(Protection Proxy)控制对原始对象的访问,用于对象应该有不同的访问权限的时候。
- 智能引用(Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。典型用途包括:对指向实际对象的引用计数,这样当该对象没有引用时,可以被自动释放;当第一次引用一个持久对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
8. 结构型模式比较
Adapter 模式和 Bridge 模式具有一些功能特征,都给另一个对象提供了一定程度上的间接性,因而有利于系统的灵活性,另外都涉及从自身以外的一个接口向这个对象转发请求。Adapter 模式主要是为解决两个已有接口之间不匹配的问题,不考虑这些接口是怎样实现的,也不考虑它们各自可能会如何演化。这种方式不需要对两个独立设计的类中的任何一个进行重新设计,就能够使它们协同工作。Bridge 模式则对抽象接口与它的(可能是多个)实现部分进行桥接。虽然这一模式运行使用者修改实现它的类,但是它仍然为用户提供了一个稳定的接口,也会在系统演化时适应新的实现。Adapter 模式和 Bridge 模式通常被用于软件生命周期的不同阶段,针对不同的问题。Adapter 模式在类已经设计好后实施;而 Bridge 模式在设计类之前实施。
Composite 模式和 Decorator 模式具有类似的结构,说明它们都是基于递归组合来组织可变数目的对象。Decorator 旨在能够不需要生成子类即可给对象添加职责,这避免了静态实现所有功能组合而导致子类急剧增加。Composite 旨在构造类,使多个相关的对象能够以统一的方式处理,而多重对象可以被当作一个对象来处理,重点在于表示。两者通常协同使用。
Decorator 模式和 Proxy 模式都描述了怎样为对象提供一定程度上的间接引用。Proxy 模式构成一个对象并为用户提供一致的接口,与 Decorator 模式不同的是,Proxy 模式不能动态地添加或分离性质,也不是为递归组合而设计的,它强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态地表达。其目的是,当直接访问一个实体不方便或不符合需要时,为这个实体提供一个替代者,例如,实体在远程设备上,访问受到限制或者实体是持久存储的。在 Proxy 模式中,实体定义了关键功能,而 Proxy 提供(或拒绝)对它的访问。在 Decorator 模式中,组件仅提供了部分功能,而一个或多个 Decorator 负责完成其他功能。Decorator 模式适用于编译时不能(至少不方便)确定对象的全部功能的情况。
7.3.4 行为设计模式
行为模式涉及算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻画了在运行时难以跟踪的、复杂的控制流。它们将用户的注意力从控制流转移到对象间的联系方式上来。
行为类模式使用继承机制在类间分派行为。本小节包括两个这样的模式。其中 Template Method 较为简单和常用。模板方法是一个算法的抽象定义,它逐步地定义该算法,每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。另一种行为类模式是 Interpreter,它将一个文法表示为一个类层次,并实现一个解释器作为这些类的实例上的一个操作。
行为对象模式使用对象复合而不是继承。一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。这里一个重要的问题是对等的对象。如何互相了解对方。对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。在极端情况下,每一个对象都要了解所有其他的对象。Mediator 在对等对象间引入一个 mediator 对象以避免这种情况的出现。mediator 提供了松耦合所需的间接性。
Chain of Responsibility 提供更松的耦合。它让用户通过一条候选对象链隐式地向一个对象发送请求。根据运行时刻情况任一候选者都可以响应相应的请求。候选者的数目是任意的,可以在运行时刻决定哪些候选者参与到链中。
Observer 模式定义并保持对象间的依赖关系。典型的 Observer 的例子是 Smalltalk 中的模型/视图/控制器,其中,一旦模型的状态发生变化,模型的所有视图都会得到通知。
其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。Strategy 模式将算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。Command 模式将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表中,或者以其他方式使用。State 模式封装一个对象的状态,使得这个对象的状态对象变化时,该对象可改变它的行为。Visitor 封装分布于多个类之间的行为,而 Iterator 抽象了访问和遍历一个集合中的对象的方式。
1. Chain of Responsibility(责任链)
1)意图
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
2)结构
责任链模式的结构图如图 7-38 所示。
其中:
- Handler 定义一个处理请求的接口;(可选)实现后继链。
- ConcreteHandler 处理它所负责的请求;可访问它的后继者;如果可处理该请求,就处理它,否则将该请求转发给后继者。
- Client 向链上的具体处理者(ConcreteHandler)对象提交请求。
3)适用性
Chain of Responsibility 模式适用于以下条件:
- 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
- 想在不明确指定接收者的情况下向多个对象中的一个提交一个请求。
- 可处理一个请求的对象集合应被动态指定。
2. Command(命令)
1)意图
将一个请求封装为一个对象,从而使得可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
2)结构
命令模式的结构图如图 7-39 所示。
其中:
- Command 声明执行操作的接口。
- ConcreteCommand 将一个接收者对象绑定于一个动作;调用接收者相应的操作,以实现 Execute。
- Client 创建一个具体命令对象并设定它的接收者。
- Invoker 要求该命令执行这个请求。
- Receiver 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
3)适用性
Command 模式适用于:
- 抽象出待执行的动作以参数化某对象。Command 模式是过程语言中的回调(Callback)机制的一个面向对象的替代品。
- 在不同的时刻指定、排列和执行请求。一个 Command 对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可以将负责该请求的命令对象传递给另一个不同的进程并在那儿实现该请求。
- 支持取消操作。Command 的 Execute 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command 接口必须添加一个 Unexecute 操作,该操作取消上一次 Execute 调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用 Unexecute 和 Execute 来实现重数不限的“取消”和“重做”。
- 支持修改日志。这样当系统崩溃时,这些修改可以被重做一遍。在 Command 接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用 Execute 操作重新执行它们。
- 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务(Transaction)的信息系统中很常见。Command 模式提供了对事务进行建模的方法。Command 有一个公共接口,使得可以用同一种方式调用所有的事务,同时使用该模式也易于添加新事务以扩展系统。
3. Interpreter(解释器)
1)意图
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
2)结构
解释器模式的结构图如图 7-40 所示。
其中:
- AbstractExpression 声明一个程序的解释操作,这个接口为抽象语法树中所有的结点所共享。
- TerminalExpression 实现与文法中的终结符相关联的解释操作;一个句子中的每个终结符需要该类的一个实例。
- NonterminalExpression 对文法中的每一条规则都需要一个 NonterminalExpression 类;为每个符号都维护一个 AbstractExpression 类型的实例变量;为文法中的非终结符实现解释(Interpret)操作。
- Context 包含解释器之外的一些全局信息。
- Client 构建(或被给定)表示该文法定义的语言中一个特定的句子的抽象语法树,该抽象语法树由 NonterminalExpression 和 TerminalExpression 的实例装配而成;调用解释操作。
3)适用性
Interpreter 模式适用于当有一个语言需要解释执行,并且可将该语言中的句子表示为一个抽象语法树时,以下情况效果最好:
- 该文法简单。对于复杂的发文,文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无须构建抽象语法树即可解释表达式,这样可以节省空间还可能节省时间。
- 效率不是一个关键问题。最高效的解释器通常不是通过直接解释语法分析树实现的,而是首先将它们转换成另一种形式。不过,即使在这种情况下,转换器仍然可用该模式实现。
4. Iterator(迭代器)
1)意图
提供一种方法顺序访问一个聚合对象中的各个元素,且不需要暴露该对象的内部表示。
2)结构
迭代器模式的结构图如图 7-41 所示。
其中:
- Iterator(迭代器)定义访问和遍历元素的接口。
- ConcreteIterator(具体迭代器)实现迭代器接口;对该聚合遍历时跟踪当前位置。
- Aggregate(聚合)定义创建相应迭代器对象的接口。
- ConcreteAggregate(具体聚合)实现创建相应迭代器的接口,该操作返回 ConcreteIterator 的一个适当的实例。
3)适用性
Iterator 模式适用于:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 支持对聚合对象的多种遍历。
- 为遍历不同的聚合结构提供一个统一的接口。
5. Mediator(中介者)
1)意图
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
2)结构
中介者模式的结构图如图 7-42 所示。
其中:
- Mediator(中介者)定义一个接口用于各同事(Colleague)对象通信。
- ConcreteMediator(具体中介者)通过协调各同事对象实现协作行为;了解并维护它的各个同事。
- Colleague class(同事类)知道它的中介者对象;每一个同事类对象在需要与其他同事通信的时候与它的中介者通信。
3)适用性
Mediator 模式适用于:
- 一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解。
- 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。
- 想定制一个分布在多个类中的行为,而又不想生成太多的子类。
6. Memento(备忘录)
1)意图
在不破坏封装性的前提下捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可以将对象恢复到原先保存的状态。
2)结构
此模式的结构图如图 7-43 所示。
其中:
- Memento(备忘录)存储原发器对象的内部状态,原发器根据需要决定备忘录存储原发器的哪些内部状态;防止原发器以外的其他对象访问备忘录。
- Originator(原发器)创建一个备忘录,用于记录当前时刻它的内部状态;使用备忘录恢复内部状态。
- Caretaker(管理者)负责保存好备忘录;不能对备忘录的内容进行操作或检查。
3)适用性
Memento 模式适用于:
- 必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态。
- 如果一个用接口来让其他对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
7. Observer(观察者)
1)意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
2)结构
观察者模式的结构图如图 7-44 所示。
其中:
- Subject(目标)知道它的观察者,可以有任意多个观察者观察同一个目标;提供注册和删除观察者对象的接口。
- Observer(观察者)为那些在目标发生改变时需获得通知的对象定义一个更新接口。
- ConcreteSubject(具体目标)将有关状态存入各 ConcreteObserver 对象;当它的状态发生改变时,向它的各个观察者发出通知。
- ConcreteObserver(具体观察者)维护一个指向 ConcreteSubject 对象的引用;存储有关状态,这些状态应与目标的状态保持一致;实现 Observer 的更新接口,以使自身状态与目标的状态保持一致。
3)适用性
Observer 模式适用于:
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变时。
- 当一个对象必须通知其他对象,而它又不能假定其他对象是谁,即不希望这些对象是紧耦合的。
8. State(状态)
1)意图
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
2)结构
状态模式的结构图如图 7-45 所示。
其中:
- Context(上下文)定义客户感兴趣的接口;维护一个 ConcreteState 子类的实例,这个实例定义当前状态。
- State(状态)定义一个接口以封装与 Context 的一个特定状态相关的行为。
- ConcreteState(具体状态子类)每个子类实现与 Context 的一个状态相关的行为。
3)适用性
State 模式适用于:
- 一个对象的行为决定于它的状态,并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态常用一个或多个枚举常量表示。通常,有多个操作包含这一相同的条件结构。State 模式将每一个条件分支放入一个独立的类中。这使得开发者可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象独立变化。
9. Strategy(策略)
1)意图
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。此模式使得算法可以独立于使用它们的客户而变化。
2)结构
策略模式的结构图如图 7-46 所示。
其中:
- Strategy(策略)定义所有支持的算法的公共接口。Context 使用这个接口来调用某 ConcreteStrategy 定义的算法。
- ConcreteStrategy(具体策略)以 Strategy 接口实现某具体算法。
- Context(上下文)用一个 ConcreteStrategy 对象来配置;维护一个对 Strategy 对象的引用;可定义一个接口来让 Strategy 访问它的数据。
3)适用性
Strategy 模式适用于:
- 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
- 需要使用一个算法的不同变体。例如,定义一些反映不同空间的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时,可以使用策略模式。
- 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,将相关的条件分支移入它们各自的 Strategy 类中,以代替这些条件语句。
10. Template Method(模板方法)
1)意图
定义一个操作中的算法骨架,而将一些步骤延迟到子类中。Template Method 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
2)结构
模板方法模式的结构图如图 7-47 所示。
其中:
- AbstractClass(抽象类)定义抽象的原语操作,具体的子类将重定义它们以实现一个算法的各步骤;实现模板方法,定一个算法的骨架,该模板方法不仅调用原语操作,也调用定义在 AbstractClass 或其他对象中的操作。
- ConcreteClass(具体类)实现原语操作以完成算法中与特定子类相关的步骤。
3)适用性
Template Method 模式适用于:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中,以避免代码重复。
- 控制子类扩展。模板方法旨在特定点调用“hook”操作(默认的行为,子类可以在必要时进行重定义扩展),这就只允许在这些点进行扩展。
11. Visitor(访问者)
1)意图
表示一个作用于某对象结构中的各元素的操作。它允许在不改变各元素的类的前提下定义作用于这些元素的新操作。
2)结构
访问者模式的结构图如图 7-48 所示。
其中:
- Visitor(访问者)为该对象结构中 ConcreteElement 的每一个类声明一个 Visit 操作。该操作的名字和特征标识了发送 Visit 请求给该访问者的那个类,这使得访问者可以确定正被访问元素的具体类。这样访问者就可以通过该元素的特定接口直接访问它。
- ConcreteVisitor(具体访问者)实现每个有 Visitor 声明的操作,每个操作实现本算法的一部分,而该算法片段乃是对应于结构中对象的类。ConcreteVisitor 为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中累积结果。
- Element(元素)定义以一个访问者为参数的 Accept 操作。
- ConcreteElement(具体元素)实现以一个访问者为参数的 Accept 操作。
- ObjectStructure(对象结构)能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个组合或者一个集合,如一个列表或一个无序集合。
3)适用性
Visitor 模式适用于:
- 一个对象结构包含很多类对象,它们有不同的接口,而用户想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而又想要避免这些操作“污染”这些对象的类。Visitor 使得用户可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用 Visitor 模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
12. 行为模式比较
很多行为模式注重封装变化。当一个程序的某个方面的特征经常发生改变时,这些模式就定义一个封装这个方面的对象。这样,当该程序的其他部分依赖于这个方面时,它们都可以与此对象协作。这些模式通常定义一个抽象类来描述这些封装变化的对象,并且通常该模式依据这个对象来命名。例如:
- 一个 Strategy 对象封装一个算法。
- 一个 State 对象封装一个与状态相关的行为。
- 一个 Mediator 对象封装对象间的协议。
- 一个 Iterator 对象封装访问和遍历一个聚集对象中的各个组件的方法。
这些模式描述了程序中很可能会改变的方面。大多数模式有两种对象:封装该方面特征的新对象和使用这些新对象的已有对象。但并非所有的对象行为模式都有这样的分割功能,例如,Chain of Responsibility 可以处理任意数目的对象(即一个链),而所有这些对象可能已经存在于系统中了。这也说明了行为模式的另一个不同点:并非所有的行为模式都定义类之间的静态通信关系。
一些模式引入总是被用作参数的对象。例如 Visitor,一个 Visitor 对象是一个多态的 Accept 操作的参数,这个操作作用于该 Visitor 对象访问的对象。其他模式定义一些可作为令牌进行传递的对象,这些对象将在稍后被调用。例如,Command 和 Memento。在 Command 中,令牌代表一个请求;在 Memento 中,令牌代表在一个对象在某个特定时刻的内部状态。在这两种情况下,令牌都可以有一个复杂的内部表示,但客户并不会意识到这一点。另外,在 Command 模式中多态特别重要,这是因为执行 Command 对象是一个多态操作。而 Memento 接口非常小,以至于备忘录只能作为一个值传递,因此,它很可能根本不给它的客户提供任何多态操作。
Mediator 和 Observer 是相互竞争的模式,它们之间的差别是:Observer 通过引入 Observer 和 Subject 对象来分布通信,而 Mediator 对象则封装了其他对象间的通信。Observer 模式中不存在封装一个约束的单个对象,而必须是 Observer 和 Subject 对象相互协作来维护这个约束。通信模式由 Observer 和 Subject 连接的方式决定:一个 Subject 通常有多个 Observer,并且有时一个 Subject 的 Observer 也是另一个 Observer 的目标。Observer 模式有利于 Observer 和 Subject 之间的分割和松耦合,易于产生更细粒度且更易于复用的类。Mediator 模式的目的是集中而不是分布,它将维护一个约束的职责直接放在一个中间者中。
Command、Observer、Mediator 和 Chain of Responsibility 等模式都涉及如何对发送者和接收者解耦,但各自有不同的权衡考虑。
Command 模式使用一个 Command 对象来定义一个发送者和一个接收者之间的绑定关系,从而支持解耦。Command 对象提供了一个提交请求的简单接口(即 Execute 操作),将发送者和接收者之间的连接定义在一个对象,使得该发送者可以与不同的接收者一起工作,达到将发送者和接收者解耦,使发送者更易于复用。此外,可以复用 Command 对象,用不同的发送者参数化一个接收者。
Observer 模式通过定义一个接口来通知目标中发生的改变,从而将发送者(目标)与接收者(观察者)解耦。Observer 定义了一个比 Command 更松的发送者-接收者绑定,这是因为一个目标可能有多个观察者,并且其数目可以在运行时变化。此模式中的 Subject 和 Observer 接口是为了处理 Subject 的变化而设计的,因此,当对象间有数据依赖时,最好用此模式来对它们进行解耦。
Mediator 模式让对象通过一个 Mediator 对象间接相互引用,从而对它们进行解耦。一个 Mediator 对象为各 Colleague 对象间的请求提供路由,并集中它们的通信。因此,各 Colleague 对象仅能通过 Mediator 接口相互通信。Mediator 接口是固定的,为了增加灵活性,Mediator 可能不得不实现它自己的分发策略。可以用一定方式对请求编码并打包参数,使得 Colleague 对象可以请求的操作数目不限。由于此模式将通信行为集中到一个类中而不是将其分布在各个子类中,所以它可以减少一个系统的子类生成。
Chain of Responsibility 模式通过沿一个潜在接收者链传递请求而将发送者与接收者解耦。因为发送者和接收者之间的接口是固定的,责任链可能也需要一个定制的分发策略。
7.3.5 应用举例
【例 7.7】Visitor 模式。
某图书管理系统中管理着两种类型的文献:图书和论文。现在要求统计所有馆藏文献的总页码(假设图书馆中有一本 540 页的图书和两篇各 25 页的论文,那么馆藏文献的总页码就是 590 页)。适合采用 Visitor 模式实现该要求,设计图如图 7-49 所示。
C++ 程序如下:
class LibraryVisitor;class LibraryItemInterface {public:virtual void accept(LibraryVisitor* visitor) = 0;};class Article : public LibraryItemInterface {private:string m_title; //论文名string m_author; //论文作者int m_start_page;int m_end_page;public:Article(string p_author, string p_title, int p_start_page, int p_end_page);int getNumberOfPages();void accept(LibraryVisitor* visitor);};class Book : public LibraryItemInterface {private:string m_title; //书名string m_author; //作者int m_pages; //页数public:Book(string p_author, string p_title, int p_pages);int getNumberOfPages();void accept(LibraryVisitor* visitor);};class LibraryVisitor {public:virtual void visit(Book* p_book) = 0;virtual void visit(Article* p_article) = 0;virtual void printSum() = 0;};class LibrarySumPrintVisitor : public LibraryVisitor { //打印总页数private:int sum;public:LibrarySumPrintVisitor();void visit(Book* p_book);void visit(Article* p_article);void printSum();};// visitor.cppint Article::getNumberOfPages() {return m_end_page - m_start_page;}void Article::accept(LibraryVisitor* visitor) {visitor->visit(this);}Book::Book(string p_author, string p_title, int p_pages) {m_title = p_title;m_author = p_author;m_pages = p_pages;}int Book::getNumberOfPages() {return m_pages;}void Book::accept(LibraryVisitor* visitor) {visitor->visit(this);}//其余代码省略
Java 程序如下:
import java.util.*;interface LibraryVisitor {void visit(Book p_book);void visit(Article p_article);void printSum();}class LibrarySumPrintVisitor implements LibraryVisitor { //打印总页数private int sum = 0;public void visit(Book p_book) {sum = sum + p_book.getNumberOfPages();}public void visit(Article p_article) {sum = sum + p_article.getNumberOfPages();}public void printSum() {System.out.println("SUM = " + sum);}}interface LibraryItemInterface {void accept(LibraryVisitor visitor);}class Article implements LibraryItemInterface {private String m_title; //论文名private String m_author; //论文作者private int m_start_page;private int m_end_page;public Article(String p_author, String p_title, int p_start_page, int p_end_page) {m_title = p_title;m_author = p_author;m_start_page = p_start_page;m_end_page = p_end_page;}public int getNumberOfPages() {return m_end_page - m_start_page;}public void accept(LibraryVisitor visitor) {visitor.visit(this);}}class Book implements LibraryItemInterface {private String m_title; //书名private String m_author; //书作者private int m_pages; //页数public Book(String p_author, String p_title, int p_pages) {m_title = p_title;m_author = p_author;m_pages = p_pages;}public int getNumberOfPages() {return m_pages;}public void accept(LibraryVisitor visitor) {visitor.visit(this);}}
【例 7.8】Observer 模式。
一大型购物中心的收银软件中,要求其能够支持购物中心在不同时期推出的各种促销活动,如打折、返利(例如,满 300 返 100)等等。针对促销活动的不同策略,适合采用策略(Strategy)模式实现该要求,得到如图 7-50 所示的类图。
C++ 程序如下:
#include <iostream>using namespace std;enum TYPE { NORMAL, CASH_DISCOUNT, CASH_RETURN };class CashSuper {public:virtual double acceptCash(double money) = 0;};class CashNormal : public CashSuper { //正常收费子类public:double acceptCash(double money) {return money;}};class CashDiscount : public CashSuper {private:double moneyDiscount; //折扣率public:CashDiscount(double discount) {moneyDiscount = discount;}double acceptCash(double money) {return money * moneyDiscount;}};class CashReturn : public CashSuper { //满额返利private:double moneyCondition; //满额数额double moneyReturn; //返利数额public:CashReturn(double moneyCondition, double moneyReturn) {this->moneyCondition = moneyCondition;this->moneyReturn = moneyReturn;}double acceptCash(double money) {double result = money;if (money >= moneyCondition)result = money - (int)(money / moneyCondition) * moneyReturn;return result;}};class CashContext {private:CashSuper *cs;public:CashContext(int type) {switch(type) {case NORMAL: //正常收费cs = new CashNormal();break;case CASH_RETURN: //满 300 返 100cs = new CashReturn(300, 100);break;case CASH_DISCOUNT: //打 8 折cs = new CashDiscount(0.8);break;}}double GetResult(double money) {return cs->acceptCash(money);}};//此处略去 main()函数
Java 程序如下:
import java.util.*;enum TYPE { NORMAL, CASH_DISCOUNT, CASH_RETURN }interface CashSuper {public double acceptCash(double money);}class CashNormal implements CashSuper { //正常收费子类public double acceptCash(double money) {return money;}}class CashDiscount implements CashSuper {private double moneyDiscount; //折扣率public CashDiscount(double moneyDiscount) {this.moneyDiscount = moneyDiscount;}public double acceptCash(double money) {return money * moneyDiscount;}}class CashReturn implements CashSuper { //满额返利private double moneyCondition; //满额数额private double moneyReturn; //返利数额public CashReturn(double moneyCondition, double moneyReturn) {this.moneyCondition = moneyCondition; //满额数额this.moneyReturn = moneyReturn; //返利数额}public double acceptCash(double money) {double result = money;if (money >= moneyCondition)result = money - Math.floor(money / moneyCondition) * moneyReturn;return result;}}class CashContext {private CashSuper cs;private TYPE t;public CashContext(TYPE t) {switch(t) {case NORMAL: //"正常收费"cs = new CashNormal();break;case CASH_DISCOUNT: //"打 8 折"cs = new CashDiscount(0.8);break;case CASH_RETURN: //"满 300 返 100"cs = new CashReturn(300, 100);break;}}public double GetResult(double money) {return cs.acceptCash(money);}}//此处略去 main()方法
