
1.5.1 分解的作用
“控制复杂性的技巧我们从远古时代就知道了,即分而治之。”[19]在设计一个复杂系统时,重要的是将它分解为一些小而又小的部分,然后可以独立地处理每个部分。通过这种方式,我们适应了人类认知时的渠道能力局限:要理解某一层次的系统,只需要一次理解几个部分(而非所有部分)。实际上,正如Parnas所说的,通过分割系统的状态空间,聪明的分解直接解决软件系统内在的复杂性[20]。
1.算法分解
对于自顶向下的结构化设计,我们中的大多数人都接受过正统的训练,所以我们将分解作为一种简单的算法分解,即系统中的每个模块代表了某个总体过程的一个主要步骤。图1-3是一个结构化设计的产品的例子,结构图展示了解决方案的不同功能模块之间的关系。这张结构图展示了一个程序的部分设计,即更新一个主控文件(master file)的内容。它是利用一个专家系统工具从一个数据流图中自动生成的,该工具包含了结构化设计的规则[21]。
2.面向对象的分解
我们认为,对于同样的问题,还存在另一种可能的分解方式。如图1-4所示,我们根据问题领域中的关键抽象概念对系统进行了分解。我们没有将问题分解为“Get formatted Update(取得格式化的更新信息)”和“Add checksum(添加校验和)”这样的步骤,而是确定了“Master File”和“Checksum”这样的对象,这是直接从问题域的词汇表中得到的。

图1-3 算法分解

图1-4 面向对象的分解
虽然两种设计解决的是相同的问题,但它们处理的方式相当不一样。在第二种分解中,我们把世界看成是一组自动化的代理,它们互相协作,执行某种高级的行为。所以“取得格式化的更新信息”不是存在于一个独立的算法中,而是与“File of Updates(更新文件)”对象相关联的一个操作。调用这个方法创建了另一个对象,即“Update to Card(对卡的更新)”。按照这种方式,我们的解决方案中的每个对象都有它自己的独特行为,每个对象都是真实世界中的某个对象的模型。从这个角度来看,一个对象就是一个可以触摸的实体,展示了一些定义良好的行为。对象能做一些事情,我们通过发送消息要求它们做它们能做的事情。因为我们的分解基于对象而不是算法,所以称为“面向对象的分解”。
3.算法分解与面向对象分解
对复杂系统的分解,哪一种是正确的方法?按算法分解还是按对象分解?实际上,这个问题带有欺骗性,因为正确的答案是两种观点都有其各自的重要性。算法的观点强调了事件的顺序,面向对象的观点强调了一些代理,它们要么发出动作,要么是这些操作执行的对象。
分析和设计方法的分类
我们发现,区分“方法”和“方法学”这两个术语是有意义的。一种方法是一个有规定的过程,目的是生成一组模型,利用某种定义良好的表示法,描述被开发的软件系统的各个方面。一种方法学是一组方法,适用于软件开发生命周期的各个阶段,它由过程、实践和某种一般的哲学统一起来。方法很重要,这有几个原因。首先,它们在复杂软件系统的开发中注入了纪律。其次,它们定义了一些产品,这些产品成为了开发团队中的成员进行沟通的载体。另外,方法也定义了管理层测量进度和管理风险所需的里程碑。
随着软件系统复杂度的增长,方法也在演进。在计算机技术的早期,人们不会去写大型程序,因为那时计算机的能力非常有限。构建系统的主要限制条件是硬件:计算机的内存很小,程序必须与磁鼓这样的二级存储设备的大延迟搏斗,处理器的时钟周期也是以几百毫秒来计算的。在20世纪的60年代和70年代,计算机经济开始发生了极大的变化,硬件的成本直线下降,同时计算的能力直线上升。因此,人们越来越希望对日益复杂的应用实现自动化,最终在经济上也可行了。作为重要的工具,高级程序设计语言出现了。这些语言改进了单个开发者及开发团队整体的生产效率,具有讽刺意味的是,这又迫使我们创建更为复杂的系统。
许多设计方法是在20世纪的60年代和70年代提出来的,它们要解决的问题是不断增加的复杂性。其中最有影响的方法是自顶向下的结构化设计,也称为“组合设计”。这种方法直接受到传统的高级程序设计语言的影响,如FORTRAN和COBOL。在这些语言中,基本的分解单元是子程序,这导致了程序具有树的形态,子程序通过调用其他子程序来完成工作。这就是自顶向下的结构化设计所采取的方法:设计者通过算法分解将大问题分解为较小的步骤。
自从20世纪的60年代和70年代以来,人们制造出了能力更强的计算机。结构化设计的价值没有改变,但正如Stein所说的,“当应用超过10万行代码时,结构化程序似乎就不行了”[22]。人们提出了许多设计方法,其中许多方法就是针对自顶向下的结构化设计的缺点提出来的。在Teledyne Brown Engineering的一次全面调查中,Peters[23]、Yau和Tsai[24]对比较有趣和成功的设计方法进行了分类整理。可能并不奇怪,这些方法中的大多数基本上都是类似主题的一些变奏。实际上,Sommerville指出,绝大多数方法都可以归为以下三类之一[25]:
■ 自顶向下的结构化设计
■ 数据驱动设计
■ 面向对象设计
Yourdon和Constantine[26]、Myers[27]和Page-Jones[28]等人的著作对自顶向下的结构化设计给出了例子和说明。这种方法的基础源自于Wirth [29],[30]及Dahl、Dijkstra、Hoare[31]的工作。Mills、Linger和Hevner[32]提出了结构化设计的一个重要的变化形式。这些不同的方法都采用了算法分解的方式。利用这种方法设计的软件可能比用其他方法设计的软件更多一些。但是,结构化设计不考虑数据抽象和信息隐藏的问题,它也没有提供足够的手段来处理并发。对于特别复杂的系统来说,结构化设计的可伸缩性不太好,而且这种方法与基于对象和面向对象的语言一起使用基本上不合适。
数据驱动设计是Jackson[33],[34]早期工作和Orr方法[35]的最好例证。在这种方法中,系统输入和输出之间的映射关系驱动着软件系统的结构。正如结构化设计一样,数据驱动的设计已经成功地应用于一些复杂的领域,特别是信息管理系统。这些系统涉及系统输入和输出之间的直接关系,但在考虑时间相关的事件方面要求不高。
面向对象分析的底层概念是设计者应该将系统建模为一组协作的对象,将单个对象作为类的实例,而类之间具有层次关系。面向对象的分析和设计直接反映了一些高级程序设计语言的结构,如Smalltalk、Object Pascal、C++、Common Lisp Object System(CLOS)、Ada、Eiffel、Python、Visual C#和Java。
但是,实际情况却是我们无法同时用两种方法来构建复杂系统,因为它们的观点是完全正交的。[36]开始分解系统时,我们必须要么从算法开始,要么从对象开始,然后利用得到的结构作为框架来表达其他的看法。
经验使我们首先应用面向对象的观点,因为这种方法更有助于组织软件系统的内在复杂性。它帮助我们描述复杂系统中有组织的复杂性,这些复杂系统包括计算机、行星、银河和大型社会团体等。第2章中将进一步讨论,与算法分解相比,面向对象分解具有一些非常重要的优点。面向对象分解通过复用共同的机制,得到一些较小的系统,从而提供了重要的表达经济性。面向对象系统在应对变化时也更有弹性,从而更能够随时间演变,因为它们的设计是基于稳定的中间状态的。实际上,面向对象分解极大地降低了构建复杂软件系统的风险,因为它们的思路是从我们有信心的、较小的系统开始增量式地演进。而且,通过帮助我们明智地决定对巨大的状态空间进行分离关注,面向对象的分解直接关注了软件的内在复杂性。
本书第3部分通过一些应用展示了这些好处,这些应用来自于一些不同的问题域。本小节补充材料“分析和设计方法的分类”,进一步比较了面向对象的观点和较为传统的设计方法。