1
面向对象软件工程实践指南
1.2.7.5 7.5 对 象 设 计
7.5 对 象 设 计

确定有哪些对象、有什么样的方法、属于谁、对象之间如何交互,这是开发面向对象系统的核心。对象设计就是根据分析阶段的成果,确定软件对象以及它们之间交互的具体细节的过程。

对象设计围绕用例描述展开,使得每一个用例的流程能够通过软件对象的交互完成,其模拟了现实中用例流程的实现,这是面向对象方法学的一个体现。然而,现在在进行软件设计时,又要依据软件知识、计算机知识考虑对执行速度、消耗资源等进行优化,因此会提出与实际中流程并不相同,但是更符合软件特点的方案。

7.5.1 对象设计的相关概念

现介绍几个与对象设计相关的概念。

(1)用例实现:用例实现是针对一个用例,考虑如何定义软件类,并定义这些类的对象之间的交互行为从而对用例进行实现。

(2)责任分配:对象设计的过程其实是责任分配的过程。每一个对象在软件系统中都需要“负责一些事情”,也就是说在接到一个消息后,要完成一些工作。这种工作可以分为两类:①自己完成某项工作,如创建一个对象或者进行一个计算;②控制或者协调其他对象。

一个对象要能够具有完成责任的“能力”,它需要:①具有内部的数据;②知道相关的对象。

这就好比一个人的能力不仅取决于自身的素质,也取决于其社会关系。

责任并不等同于类的方法,但是方法是用来完成责任的手段。

(3)设计类图:设计类图是经过设计过程确定的软件类以及它们的关系构成的类图,它是后面进行代码实现的直接参考。UML中并没有专门的设计类图,只有类图,用于表达软件类的类图就是设计类图。

7.5.2 对象设计的工具与过程

对象设计中围绕用例,确定相关的设计类,并通过在设计类中分配相关的责任,从而得到每个类应该具有的属性、方法。因此,表达对象设计成果的工具是设计类图,而如何依据设计决策,进行责任分配,最后得到类的细节的过程是通过画交互图完成的。因此交互图是对象设计中最重要的工具。初学者可能会认为类图是最重要的,实际上,画交互图比画类图重要,因为它直接反映了决策的过程,类图只是设计决策的结果。

对象设计可以有不同的步骤安排,以下是一个指导性的步骤。

(1)设计对象的识别:在分析阶段创建了分析类图,它们是领域概念的直接体现,在对象设计过程中,依据分析类图,进行设计类的识别,总体上,设计类与分析类具有对应关系,然而,由于设计类是软件类,所以可能会根据软件本身的特点,进行一些调整。比如出于软件技术本身的考虑,添加一些分析类中没有对应的设计类,例如增加一些类专门负责与数据库的交互等。

(2)设计类的定义:随着交互图的构造,我们逐步添加了设计对象及其设计类,依据领域模型中定义的分析类的属性,交互图中涉及的属性为设计类添加属性;同时依据交互图中消息信息,给设计类添加方法;并依据交互图中的消息发送关系添加类之间的关联关系,从而形成设计类图。

(3)模型优化与重构:在设计类定义过程中,我们可能会发现一些模型中值得优化与重构的地方,例如,如果发现多个类具有相似的属性和方法,就可以引入一个父类来定义共同的属性和方法。这个阶段中对模型的优化与重构甚至可能涉及对分析模型的优化与重构。

7.5.3 对象识别和定义

7.5.3.1 对象识别与责任分配

对象设计是用例驱动的,也就是说,我们需要针对用例描述,特别是用例描述中的系统操作,确定哪些对象需要参与进来、每个对象在其中担当的责任以及它们之间是如何通过消息交互进行协作的。因此,在设计时,首先从系统操作开始,从分析类图中寻找所需要的分析类,将它引入进来,变成对应的设计类,然后将责任逐一分配给设计类。

以下为针对一个系统操作展开的设计工作:

(1)选择一个用例。

(2)选择用例的一个系统操作。

(3)确定UI层上哪个UI对象负责接受用户操作产生该系统操作,这是一个UI设计问题。

(4)确定由哪一个控制类对象接受由UI对象传过来的系统操作消息。

(5)控制类对象决定接下来由哪个设计类对象接着进行处理并发送相应的消息给这些对象,如果所需要的设计类不存在,去分析类图中找可以承担该责任的类,创建一个对应的设计类。

(6)接到消息的对象如果自己单独能够完成消息的处理,则不再传递消息给其他对象;如果它自己无法独自完成消息的处理,那么需要确定由哪个设计类对象接着进行处理并发送相应的消息给这些对象,如果所需要的设计类不存在,去分析类图中找可以承担该责任的类,创建一个对应的设计类。

在上述过程中,第6步将不断重复直到不再需要别的对象参与进来。

在软件设计中,进行对象识别和责任分配非常类似于管理一个企业。如何成功地管理一个企业呢?需要定义清楚企业内部各种角色的职责,做到责任分明,各司其职。软件对象之间的责任分配也是如此。

以下我们列出了在进行对象识别和责任分配时值得注意的问题。

1)责任分配的基本出发点

对象设计是依据责任分配思路来进行的。责任分配的基本出发点是先确定完成这个责任需要哪些信息,然后看这些信息分别在哪些对象那儿,谁拥有信息,谁就应该承担一定的责任。这显然与我们的生活常识相匹配,在办一件事情时,我们得去找拥有信息干这件事情的人,这样最为直接,而不是绕几个圈子找其他人办事使得事情更为复杂。

2)控制类的选择问题

系统操作一般都是由UI层对象产生,然后经过一个控制类对象将消息传递给领域层的对象进行处理。通过这种方法实现了UI层和功能实现软件的分离,这是模型视图分离原则的一种体现。由于UI层唯一地通过控制类对象与领域层联系,所以UI层的改变或者整个的替换都不会影响领域层。

控制类的定义有两种情况,一种是系统比较复杂,功能比较多,此时,我们可以针对一个用例创建一个控制类,称为用例控制器;另一种是系统相对比较简单,那么整个系统中只要设计一个控制类就可以了,称为系统控制器。

值得指出的是控制类对象主要的职责是消息传递和协调,而非自己去完成工作。这就好比一个企业中的管理者,他们的责任通常也是消息传递和协调,而不是自己去完成业务。

3)生成一个新对象的责任分配

一些时候,需要生成一个新对象,那么这个责任应该分配给谁呢?这时候考虑将此责任分配给那些本来就与该对象有密切关系的对象,这样的话,不会增加新的耦合性。

4)多种方案的权衡

在许多时候,我们并没有一个唯一的解决方案。此时,指导我们在方案中进行选择的依据就是高内聚和低耦合。

5)发明新的软件类

虽然在引入软件类的时候,总是从领域模型中去找对应的分析类获得参考,然而软件毕竟不等同于实际问题。有的时候,我们会“发明”新的类,以负责一些与软件技术相关的专业性的事务,比如说有一些类是专门负责去创建对象的,有一些类是专门负责数据访问的,这些发明的新的类能够解决专门的问题,给其他对象提供了公共的服务。这就好比每一个企业原来都需要自己办食堂解决员工的吃饭问题,看中这个商机后,专门出现了给各个企业提供工作餐的企业,这样就可以节省社会成本。

7.5.3.2 对象定义

对象的定义是依据交互图进行的,当然,交互图的绘制也与对象定义有关,因此,对象定义与交互图绘制是并行开展的。

对象可见性(visibility)是对象看到或引用其他对象的能力。为了使发送者对象能够向接收者对象发送消息,发送者必须具有接收者的可见性,即发送者必须拥有对接收者对象的某种引用或指针。

实现对象A到对象B的可见性通常有四种方式:

(1)属性可见性(attribute visibility):B是A的属性。

(2)参数可见性(parameter visibility):B是A方法中的参数。

(3)局部可见性(local visibility):B是A方法中的局部对象(不是参数)。

(4)全局可见性(global visibility):B具有某种方式的全局可见性。

为了使对象A能够向对象B发送消息,对于A而言,B必须是可见的。以下为对象定义的步骤:

(1)检查交互图并列出提到的所有类。

(2)给这些类画类图,列出通过领域模型中识别出来,并在设计中采用的属性。

(3)添加方法名。通过分析交互图,每一个类具有的方法可以确定。在交互图中,如果一个对象接收一个消息,那么它就需要提供一个与此消息对应的方法。以下为一些特别的方法设置:

a.获取或者设置属性值。从信息隐藏的观点看,应该把类里面所有的属性设置为私有,并分别定义Get和Set方法来获取属性值或者修改属性值。在实践中,出于简化软件实现或者提高访问效率的考虑,我们可能会把某些属性设置为公共。

b.对象的初始化。在交互图中通过create消息进行表示,但是,每一种语言有各自特定的实例化或者初始化的方法,我们需要按照特定的语言进行该方法的表达。

(4)添加关联:从一个类A的对象发消息给另外一个类B的对象,那么需要A与B之间具有关联关系。在关联关系里,每一端的类称为一个角色,在设计类图中,角色可以用方向箭头修饰,关联上的方向箭头一般解释为从源到目标类的属性可见性,它一般实现为源类中有一个指向目标类对象的属性。下列情形可以定义从A指向B的方向性修饰:

a.A的对象发送消息给B的对象。

b.A的对象创建B的对象。

c.A的对象需要保持与B的对象连接。

(5)添加依赖关系:依赖关系表明一个类的对象知道另外一个类的对象,它用虚线箭头表示,从依赖端指向被依赖端。在类图中,依赖关系可以表示非属性可见性,即参数,全局或者局部可见性。

7.5.3.3 模型优化

在设计完成后,可以对设计模型进行仔细检查,除了保证模型的一致性(交互图与类图)、模型的正确性外,还可能发现一些值得优化的地方。

(1)发现新的类的继承层次:如果在模型中发现某些类具有共同的属性和方法,可以提取出一个公共类,把共同的属性和方法放在这个类中,再定义一个继承关系;如果发现某一个类中不同的子类型有行为上的差别,那么可以去定义一些子类,通过重载、多态机制使子类具有自己的个性行为。

(2)压缩类成为属性:如果一个类中只有访问其属性或者修改其属性的方法,可以考虑把它压缩为另外相关的类的属性以简化模型。

(3)效率优化:我们可以围绕效率方面的优化进行一些特别的设计。例如,通过增加额外的关联使得访问变得直接从而提高了访问效率,也可以把计算的中间结果保存在某一属性中,这样下次就可以不计算而直接把结果取出来。但是这种效率优化有时可能会导致模型质量的下降,因此应该谨慎地使用。

7.5.3.4 对象设计的注意点

1)特殊用例的考虑

对象设计是由用例驱动的,而这些用例反映了用户的需求。任何一个系统都是需要启动和关闭的,另外,系统在运行过程中还可能发生一些意外,需要进行恢复操作。这些情形并不在通常的用例考虑范围之内。所以就需要设置一些特别的用例来反映这些情况。这些用例列举如下。

(1)启动用例(start up use case):系统在启动的时候要完成的工作。一般而言,系统在启动时要创建在系统运行过程中一直存在的对象——UI对象、控制类对象并建立相互之间的关系。

(2)结束用例(end up use case):系统在关闭时要做的工作。为了保证系统能够安全退出,需要保证数据的完整性,清理内存,清理网络连接、与数据库以及其他系统的连接。

(3)异常用例(exception use case):系统出现异常后,需要做的工作,具体要做什么工作取决于设计策略。例如有些系统异常后,需要进行数据恢复,那么就需要描述数据恢复过程。

除了针对普通用例进行设计外,最后需要再针对这些特殊用例进行设计。

2)需要遵循的软件设计基本原则

软件设计的基本原则是需要遵循的。然而,并不能教条。例如对稳定的元素和普遍的元素的高耦合一般不是一个问题。例如,一个Java J2EE应用对Java库(java.util,等等)的耦合没有问题,因为它们是稳定的,并普遍使用。

3)为可能的变化进行针对性的设计

对于初学者而言,所进行的设计可能是比较脆弱的,也就是说对于一些未来可能发生的变化没有采取一些特别的设计。对于学习到一定程度的人来说,他们在各个地方都会假想可能发生的变化并进行针对性的设计。然而,实际上,有些地方并不一定会发生这种变化,因此真正的高手只是在恰当的地方为将来可能的变化来进行针对性的设计。