1.5 从混沌到有序
当然,我们中间总是会有一些天才,这些人拥有非凡的技能,一个人就能做一群普通开发者的工作,相当于软件工程界的莱特(Frank Lloyd Wright)或达芬奇。这些人就是我们寻找作为架构师的人——他们设计创新的思想、机制和框架,其他人可以用来作为其他应用或系统的架构基础。但是,“这个世界上天才很少,没有理由相信软件工程领域拥有大量的天才”[2]。尽管我们中间确实有一些天才,但是在工业级软件开发的领域,我们不能够总是依赖个人的灵感引导我们前进。因此,必须考虑通过一些训练有素的方式来控制复杂性。
1.5.1 分解的作用
“控制复杂性的技巧我们从远古时代就知道了,即分而治之。”[16]在设计一个复杂系统时,重要的是将它分解为一些小而又小的部分,然后可以独立地处理每个部分。通过这种方式,我们适应了人类认知时的渠道能力局限:要理解某一层次的系统,只需要一次理解几个部分(而非所有部分)。实际上,正如Parnas所说的,通过分割系统的状态空间,聪明的分解直接解决软件系统内在的复杂性[17]。
1.算法分解
对于自顶向下的结构化设计,我们中的大多数人都接受过正统的训练,所以我们将分解作为一种简单的算法分解,即系统中的每个模块代表了某个总体过程的一个主要步骤。图1-3是一个结构化设计的产品的例子,结构图展示了解决方案的不同功能模块之间的关系。这张结构图展示了一个程序的部分设计,即更新一个主控文件(master file)的内容。它是利用一个专家系统工具从一个数据流图中自动生成的,该工具包含了结构化设计的规则[18]。
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万行代码时,结构化程序似乎就不行了”[19]。人们提出了许多设计方法,其中许多方法就是针对自顶向下的结构化设计的缺点提出来的。在Teledyne Brown Engineering的一次全面调查中,Peters[20]、Yau和Tsai[21]对比较有趣和成功的设计方法进行了分类整理。可能并不奇怪,这些方法中的大多数基本上都是类似主题的一些变奏。实际上,Sommerville指出,绝大多数方法都可以归为以下三类之一[23]:
■ 自顶向下的结构化设计
■ 数据驱动设计
■ 面向对象设计
Yourdon和Constantine[24]、Myers[25]和Page-Jones[26]等人的著作对自顶向下的结构化设计给出了例子和说明。这种方法的基础源自于Wirth [27, 28]及Dahl、Dijkstra、Hoare[29]的工作。Mills、Linger和Hevner [30]提出了结构化设计的一个重要的变化形式。这些不同的方法都采用了算法分解的方式。利用这种方法设计的软件可能比用其他方法设计的软件更多一些。但是,结构化设计不考虑数据抽象和信息隐藏的问题,它也没有提供足够的手段来处理并发。对于特别复杂的系统来说,结构化设计的可伸缩性不太好,而且这种方法与基于对象和面向对象的语言一起使用基本上不合适。
数据驱动设计是Jackson[31, 32]早期工作和Orr方法[33]的最好例证。在这种方法中,系统输入和输出之间的映射关系驱动着软件系统的结构。正如结构化设计一样,数据驱动的设计已经成功地应用于一些复杂的领域,特别是信息管理系统。这些系统涉及系统输入和输出之间的直接关系,但在考虑时间相关的事件方面要求不高。
面向对象分析的底层概念是设计者应该将系统建模为一组协作的对象,将单个对象作为类的实例,而类之间具有层次关系。面向对象的分析和设计直接反映了一些高级程序设计语言的结构,如Smalltalk、Object Pascal、C++、Common Lisp Object System(CLOS)、Ada、Eiffel、Python、Visual C#和Java。
但是,实际情况却是我们无法同时用两种方法来构建复杂系统,因为它们的观点是完全正交的。[3]开始分解系统时,我们必须要么从算法开始,要么从对象开始,然后利用得到的结构作为框架来表达其他的看法。
经验使我们首先应用面向对象的观点,因为这种方法更有助于组织软件系统的内在复杂性。它帮助我们描述复杂系统中有组织的复杂性,这些复杂系统包括计算机、行星、银河和大型社会团体等。第2章中将进一步讨论,与算法分解相比,面向对象分解具有一些非常重要的优点。面向对象分解通过复用共同的机制,得到一些较小的系统,从而提供了重要的表达经济性。面向对象系统在应对变化时也更有弹性,从而更能够随时间演变,因为它们的设计是基于稳定的中间状态的。实际上,面向对象分解极大地降低了构建复杂软件系统的风险,因为它们的思路是从我们有信心的、较小的系统开始增量式地演进。而且,通过帮助我们明智地决定对巨大的状态空间进行关注点分离,面向对象的分解直接关注了软件的内在复杂性。
本书第三部分通过一些应用展示了这些好处,这些应用来自于一些不同的问题域。本节补充材料“分析和设计方法的分类”,进一步比较了面向对象的观点和较为传统的设计方法。
1.5.2 抽象的作用
我们在前面提到了Miller的实验,他从这些实验中得出结论,一个人同一时刻只能理解大约7个信息,上下浮动2个。这个数字似乎与信息的内容无关。正如Miller自己说的:“绝对判断的范围和短期记忆的范围对我们能够接收、处理和记住的信息量有着很强的限制。通过将输入组织为一些不同的维度,并形成一些片段序列,我们设法打破……这种信息瓶颈”[35]。用现在的术语来说,我们把这个过程叫作“分块”或“抽象”。
正如Wulf所描述的:“我们(人类)已经形成了一种异常强大的技术来对付复杂性。我们对它进行抽象。如果不能够全面掌握一个复杂的对象,我们就选择忽略非本质的细节,转而处理这个对象的一般化的、理想化的模型”[36]。例如,研究植物中光合作用的原理,我们可以关注叶子细胞中发生的化学反应,忽略掉其他的部分,如根和茎。我们仍然受到同时可以理解的事物数量的限制,但通过抽象,我们利用了信息的分块和不断增大的语义内容。当我们采用面向对象的观点来看世界时,尤其是这样,因为对象作为真实世界中实体的抽象,代表了特定的一块密集而内聚的信息。第2章将更详细地讨论抽象的意义。
1.5.3 层次结构的作用
另一种增加单块信息的语义内容的方法,是在复杂的软件系统中显式地组织类和对象层次结构。对象结构很重要,因为它展示了不同的对象之间如何通过一些交互模式进行协作,我们把这些交互模式称为“机制”。类结构也同样重要,因为它强调了系统中的公共结构行为。因此,我们不必去研究某片植物叶子上的每个光合作用细胞,只要研究一个这样的细胞就行了,因为我们预期所有的其他细胞都会表现出相似的行为。虽然我们把一类对象的每个实例作为不同的实体,但是我们可以假定同类对象的所有其他实例都具有同样的行为。通过将对象分成具有相关抽象的组(例如,植物细胞与动物细胞),我们明确地区分了不同对象的共同属性和特有属性,这又进一步帮助我们掌握它们内在的复杂性[37]。
确定复杂软件系统中的层次关系通常并不容易,因为这要求在许多对象之中发现模式,每个对象都可能拥有大量复杂的行为。但当我们整理出这些层次结构之后,复杂系统的结构以及我们对它的理解都得到了极大的简化。第3章将详细讲解类和对象层次结构的实质,第4章将描述帮助我们确定这些模式的技术。