1.9.3 第2阶段:我们将如何建立对象

在这一阶段,我们必须做出设计,描述这些类和它们如何交互。确定类和交互的出色技术是类职责协同(Class-Responsibility-Collaboration, CRC)卡片。此技术的部分价值是它非常简单:只要有一组3到5英寸的空白卡片,在上面书写。每张卡片描述一个类,在卡片上写的内容是:

(1)类的名字。这很重要,因为名字体现了类行为的本质,所以有一目了然的作用。

(2)类的职责:它应当做什么。通常,它可以仅由成员函数的名字陈述(因为在好的设计中,这些名字应当是描述性的),但并不产生其他的注记。如果需要开始这个过程,请从一个懒程序员的立场看这个问题:你希望有什么样的对象魔术般地出现,把你的问题全部解决?

(3)类的协同:它与其他类有哪些交互?“交互”是非常宽泛的术语。它可以是一些已经存在的其他对象对这个类的对象提供的服务。协同还应当考虑这个类的观众。例如如果创建了Firecracker(鞭炮),那么谁将观察它,是Chemist(药剂师)还是Spectator(观众)?前者希望知道鞭炮由什么化学成分组成,后者对鞭炮爆炸后的颜色和形状有反应。

我们可能想让卡片更大一些,因为我们希望从中得到全部信息,但是它们是非常小的,这不仅能保持我们的类小,而且能防止过早地陷入过多的细节。如果一张小卡片上放不下类所需要的信息,那么这个类就太复杂了(或者是考虑过细了,或者应当创建多个类)。理想的类应当一目了然。CRC卡片的思想是帮助我们找到设计的第一印象,使得我们能得到总体概念,然后精炼我们的设计。

CRC卡片的最大的好处之一是在交流中。在一个组中,最好实时进行交流,而不是用计算机。每个人负责几个类(起初它们没有名字或其他信息)。每次只解决一个情节,决定发送什么消息给不同的对象以满足每个情节,这样就能作出一个比较形象的对问题的模拟。当我们经历了这个过程后,就会找出我们所需要的类以及它们的职责和协同,这样,我们同时也填写好这些卡片。当我们完成所有用例后,就有了一个相当完整的设计的第一印象。

在我开始用CRC卡片之前,当提出最初的设计时,我最成功的咨询经验就是站在一个没有OOP经验的项目组前,在白板上描述对象。我们讨论对象应当如何互相通信,擦除其中的一些,用其他的对象替换它们。实际上我是在白板上管理所有的“CRC卡片”。项目组(他们知道项目的目标)真正地在做这个设计,他们“拥有”这个设计,而不是获得既成的设计。我所做的所有事情就是通过提问正确的问题,提炼这些假设,并且从项目组得到反馈,修改这些假设来指导这个过程。这个过程的真正好处是项目组学习了如何做面向对象的设计,不是通过复审抽象的例子,而是通过在一个设计上工作,这对于他们是最有兴趣的。

制作了一组CRC卡片之后,我们可能希望用UML[1]创建这个设计的更形式化的描述。我们并不是非要用UML,但它可能有帮助,特别是如果我们要将一个图表挂在墙上,让大家一起思考时,这是一个很好的想法。除了UML之外的另一选择是对象及其接口的文字描述,这或许依赖于我们的程序设计语言,也就是代码本身[2]

UML还提供了另外一种图形符号来描述系统的动态模型。在一个系统或子系统的状态转换占主导地位,以至于它们需要自己的图表的情况下,这是有帮助的(例如在控制系统中)。我们可能还需要描述数据结构,因为系统或子系统中数据结构是重要因素(例如数据库)。

当已经描述了对象及其接口后,第2阶段就要完成了。这时已经知道了对象中的大多数,通常会有对象漏掉,直到第3阶段才被发现。这没问题。我们关心的是最终能找到所有的对象。在这个阶段较早地发现它们是好的。因为OOP提供了充分的结构,所以如果我们稍迟发现它们也可以。事实上,对象设计可能在程序设计全过程的五个阶段中都会发生。

1.9.3.1 对象设计的五个阶段

对象的设计生命期不仅仅限于写程序的时间。实际上,它出现在一系列阶段上。接受这种观点很有好处,因为我们不再期望设计立刻尽善尽美,而是认识到,对对象做什么和它应当像什么的理解,会随着时间的推移而呈现。这个观点也适用于不同类型程序的设计。特殊类型程序的模式是通过一次又一次地求解问题而形成的(设计模式在第2卷介绍)。同样,对象有自己的模式,通过理解、使用和重用而形成。

(1)对象发现这个阶段出现在程序的最初分析期间。对象可以通过寻找外部因素及边界、系统中重复的元素和最小概念单元而发现。如果已经有了一组类库,某些对象是很明显的。类之间的共同性(暗示着基类和继承关系),可以立刻出现或在设计过程的后期出现。

(2)对象装配当我们正在建立对象时会发现需要一些新成员,这些新成员在对象发现时期未出现过。对象的这种内部需要可能要用新类去支持它。

(3)系统构造再次指出,对对象的更多要求可能出现在以后阶段。随着不断学习,我们会改进我们的对象。与系统中其他对象通信和互相连接的需要,可以改变已有的类或要求新类。例如,我们可以发现需要辅助类,这些类如像一个链表,它们包含很少的状态信息或没有状态信息,只有帮助其他类的功能。

(4)系统扩充当我们向系统增添新的性能时,可能发现我们先前的设计不容易支持系统扩充。这时,我们可以重新构造部分系统,并很可能要增加新类或类层次。

(5)对象重用这是对类真正的强度测试。如果某些人试图在全新的情况下重用它,他们也许会发现一些缺点。当我们修改一个类以适应更新的程序时,类的一般原则将变得更清楚,直到我们有了一个真正可重用的对象。然而,不要期望从一个系统设计而来的大多数对象是可重用的,大量对象是对于特定系统的。可重用类一般共性较少,为了重用,它们必须解决更一般的问题。

1.9.3.2 对象开发准则

下述步骤提出了考虑开发类时要用到的一些准则:

1)让特定问题生成一个类,然后在解决其他问题期间让这个类生长和成熟。

2)记住,发现所需要的类(和它们的接口),是设计系统的主要内容。如果已经有了那些类,这个项目就不困难了。

3)不要强迫自己在一开始就知道每一件事情,应当不断地学习。

4)开始编程,让一些部分能够运行,这样就可以证明或否定已生成的设计。不要害怕过程型大杂烩式的代码—类的隔离性可以控制它们。坏的类不会破坏好的类。

5)尽量保持简单。具有明显用途的不太清楚的对象比很复杂的接口好。当需要下决心时,用Occam的Razor方法:选择简单的类,因为简单的类总是好一些。从小的和简单的类开始,当我们对它有了较好的理解时再扩展这个类接口,但是很难从一个类中删去元素。

[1]对于初学者,我推荐上面提到过的专著《UML Distilled》。

[2]Python(www.Python.org)常常被用做“可执行伪代码”。