第一部分
面向对象开发溯源
就软件开发而言,基于模型的开发方法本质上是一种面向对象的方法。所以想全面了解这种方法,必须对面向对象方法有一个大体了解。因为面向对象的方法看起来没有传统的软件开发方法那么直观,所以我们需要先了解的是为什么面向对象的方法能够发挥作用,以及它是如何工作的。
本书这部分我们将了解面向对象方法诞生时的历史背景,这样我们就能知道传统软件开发方法有什么问题,以及面向对象方法是如何去寻求解决这些问题的。
第一章
历史观点
问题是进步的代价——查尔斯F.凯特林
软件开发相对于物理科学和工业革命来说,在人类发展过程中是新事物。物理学经过一千多年才渗透到了现代生活各个方面,工业革命也差不多用了超过一百年才达到同样水平。而计算机和软件普遍成为我们生活中的一个必要部分,不过三十年时间。是的,我们走过的路一直都不平坦。
本章提供了一个历史背景,告诉我们为什么面向对象规范得以发展。只有了解需要解决的问题,我们才能完全的理解和领会这个规范,所以还是让我们从历史谈起吧。我们将研究一些在面向对象出现之前,主流软件开发所用规范的一些缺点。最后,还将为本书的其他部分提供一个技术背景,研究一下在面向对象规范出现之前一些重要的技术进步,它们后来都成了规范的一部分。
历史
实质上,20世纪50年代,没有系统的软件开发。这是编程史上的黑暗时代。今天的开发者很难想象当时的软件开发条件。一个大型主机只有几K字节的内存,并且纸带机是当时很高科技的输入设备。西联汇款(West Union)拥有对电传打字机输入设备的有效垄断,这种设备在每次按键都需要消耗数个尺磅扭力的能量——这将对导致开发者因患腕隧道症而残废,当时医学界还没有一种名称来描述这种病症。没有浏览器、调试器、或者CRT终端。基本组件语言(Basic Assembly Language)就作为当时解决软件危机的银弹!
50年代末60年代初,一些更好的工具开始出现了:高级计算机语言,可以将1和0抽象成为标记性名称;高级运算符;块结构;抽象结构,比如说记录和数组。显然这些技术和工具简化了程序员的生活,使软件开发者生产力得以提高。但是就如何合理的使用它们,却没有一个明确的规范。所以,这次大的变革导致了骇客(Hacker)时代的诞生,那是一个个人生产力起统治作用的时代。
骇客时代从60年代早期一直延续到70年代中期。这一时期的特征是许多聪明的人编写了海量的代码——一年10万行FORTRAN语言代码,这可不一般。他们必须非常聪明,因为要花大量时间调试,他们只有精于调试技术才能够如此迅速的发布代码。他们会采用一些巧妙的解决方案来解决问题。在60年代,骇客这个词是一种恭维,意旨一个人能够编写大量代码完成不俗的工作,并且能够保证这些代码的持续运行。
到了70年代晚期,蜜月期结束了,骇客成为了一个贬义词。这是因为骇客们开始了新的项目,而把旧项目的代码交给别人来维护。随着时间流逝出现了一些特殊的情况,进而发现这些代码并不总是能够工作。并且外部世界也发生了变化,所以这些程序需要修正。大多数情况下重新撰写这些程序要比修正他们容易得多。大家这时才清楚地意识到这些代码是有某些问题的。可维护性这个单词才第一次出现在了行业词典里,相应的,不能维护的代码就被叫做骇客代码。
60年代末,在总结这些宝贵的经验教训的基础上,出现的解决方案是构建软件时采用一些更为系统化的方法。与此同时,计算机程序也变得更为庞大,认为程序的架构需要提前设计的观点也应运而生。由此,软件设计开始脱离于软件开发而变成了一项独立的活动。在骇客时代即将结束前,出现了一些方法论,将我们之前学习到的经验教训总结到一起,一体的方法论要优于某些单个方法之和。这些方法论都属于结构化开发(Structured Development)范畴。
1980年左右是一个新时期的开始,几乎在软件行业的每个角落都有令人难以置信的进步。面向对象的规范——一种特定且更规范的分析和设计方法——不过是这创新大潮中的一朵浪花。
结构化开发
毫无疑问,结构化开发是80年代之前最重要的一项进步。在软件开发方面,它提供了第一个真正的系统化方法。在60年代与3GL(第三代编程语言——译者注)相融合,使生产力得到了大幅的提高。
关于结构化开发有一个有趣的副作用在当时没有被注意。那就是应用程序变得更可靠了。没被注意的原因是,此时软件的应用已很广泛,那些非软件从业人员对软件有一些更高层次的视角。当时的软件仍然有许多缺陷,这些用户仍然认为软件是不可靠的。而事实上,1980年软件的可靠性已经从60年代早期的每千行150个缺陷下降到每千行15个缺陷。
结构化开发实际上是一个总的概念,其中囊括的软件构建方法名目繁多,但是这些方法都拥有共同约定的特征。
图形化展示
每一个尚未完全成熟的方法都有一些图形化的记号作为表现形式。背后的原理非常简单——图像比语言有更强的表现力。
功能独立
一个基本的观点认为,程序是由大量复杂度不同的算法组合而成,它们彼此协作来解决给定的问题。这个算法之间互相协作的观点对软件开发后来的发展产生了巨大影响,它出现时,正值程序开始变得庞大,对于个人来说已经无法在合理的时间内进行处理的时候。功能独立的具体实现方法诸如:可重用的函数库、子系统和应用程序分层。
应用程序接口(API)
函数是独立的,但它总是要被访问的。这导致产生一种观点,即给函数提供一种不变的接口,这样使调用程序能以相同的方式访问它,函数的实现方法被修改时,调用程序也可以不用关心。
按契约编程
这是API的逻辑延伸。API本身成为了服务端与调用端的一个契约。早期对这个概念的尝试遇到了以下问题:契约是对服务的语义描述,而API只是定义了获得这些语义的语法。当编程语言开始包含诸如对某些操作的断言作为程序单元的一部分时,这个概念才成为一种正式的契约。虽然如此,这成为一个非常优秀思想的顺理成章的开端。
从上至下的开发
最初的想法是由高层次的、抽象的用户需求开始,并且逐渐的提炼它们为更具体的需求,这些具体的需求针对不同计算机环境再变得更加细化和具体。从上至下的开发与函数分解这种方法能很好的对应,后者我们将很快会介绍。
分析和设计的出现
结构化开发认为开发活动不仅仅局限于写3GL代码。分析是一个综合体,它包含需求的提取、分析、客户领域知识的详细说明和开发领域的高层软件设计。而设计是在开发人员敲击键盘开始3GL编码前,被引进的一个正式环节,提供关于软件详细架构的图形化描述。
结构化编程使得程序的构建相比以前有了更好的可维护性。事实上,在专家和训练有素的人手里,这种方法可以使开发程序的可维护性像今天的面向对象的程序一样优秀。问题在于要想做到这一点,需要很多的训练和专业技术,这是大部分软件开发者所不具备的。所以,另外一个银弹(指结构化编程——译者注)也没打中得分区域,但是这颗最起码打到了靶纸上。虽然不尽如人意,但是值得注意的是这个方法的每一个特征,都可以在今天的面向对象开发中发现。(虽然像“从上至下设计”这些方法都有了某些限制)
函数分解
这是在每一个结构化开发方法中都应用到的核心设计技术。结构化中的“结构”指的就是这个意思。函数分解专门用来处理算法的解决方案。这个视角更接近于科学计算编程而不是信息管理系统。(第一个3GL的名称是FORTRAN,是公式(Formula)和翻译机(Translator)的缩写)。同样接近于计算机硬件模型,这个我们稍候简短的讨论。
函数分解的原则是分而治之。基本上就是将庞大、复杂的函数用传统的从上至下的方法,分解为较小的、更易管理的组件或算法。这将产生一个倒立的树的结构,树的顶端是上层函数通过调用底层的一组函数来完成任务,底层的函数又包含进一步细分的函数。树最底端的叶子是原子函数(算术运算符级别),它们最为基础,因而不能够被继续分解。图1-1展示了一个例子:
|
计算雇员的期权收益 |
|
资格验证 |
|
计算收益 |
|
更新总帐 |
|
从数据库获得入职日期 |
|
编写数据库事务 |
|
从数据库获得基本工资 |
|
从数据库获得收益表 |
图1-1 函数分解示例:将计算员工期权收益的任务分解为更易管理的小功能
函数分解的作用曾经很强大且颇有吸引力。功能强大是因为在图灵(Turing)的只有算法和计算机算机器世界里,它是管理复杂度的理想方法,特别是此时第三代编程语言(3GL)提供了方法(Procedure)作为语言的基本结构。在一个充满复杂算法的世界里,函数分解曾经是那么的直观,所以科学界对此简直是趋之若鹜。
有吸引力是因为它综合了:功能独立(比如树中的分支和细分的函数的细节)、针对契约编程——通过这种方式细分的函数为上层的调用者程序提供服务、一旦函数分解树定义好后从上至底的开发路线图、以方法签名形式体现的API、控制流上深度优先的遍历、以及在不同的程序上下文中对树中同一分支的重复调用。总之这是一种非常明智的做法,能帮助我们处理许多截然不同的问题。
在70年代末期,结构化编程出现了一些未曾预见到的新问题。这些问题来源于以下两个彼此作用的事实:
l 在科学里领域内,算法是不改变的;通常,算法要么能经得住时间的考验,要么就彻底的被新的、更高级的算法所替代。但是在商务程序领域,规则总是在变化的,产品也在不断的进化。所以贯穿整个使用周期,应用程序都需要不断被修改,有时甚至是在开发的初期也需要这么做。
l 层级式的结构难于修改。
问题在于函数分解是固有的深度优先模式,直到下面划分的所有子函数都完成后,上层函数才能完成。这就导致了控制流程是一个非常僵硬的由上至下的层级结构,当需求发生变化时很难修改。改变控制流程经常意味着完全重新组织树的分支结构。
另一个问题是冗余。原子函数集合通常是有限的,不同的分支倾向于使用很多相同的原子操作,同时这些操作也会被其他分支所用到。经常是在不同的分支上都有许多相同的原子操作序列。构建这些冗余的分支是乏味的,但是相对于做维护时,这还不是严重的缺点。如果需要在许多的分支中作相同的修改,那么一个人必须重复相同的修改许多次。这种重复增加了引入错误的机会。在70年代末,冗余代码被普遍认为是由维护引起的软件可靠性差的主要原因。
为了解决这个问题,做法是一旦捕捉到特定原子操作的组合,将其包装为高层服务,这些服务被不同的树的分支调用进而实现了重用。这样做虽然解决的冗余的问题,却带来了一个甚至更为严重的问题。在一个纯粹的函数分解树上,都是一个调用者严格对应每一个方法,所以树有着清晰的方向。分支之间的重用的难点在于,树结构变成了一个格子架结构,其中的服务既拥有许多下层子函数,同时又拥有许多上层调用者,如同图1-2所示:
(扫描问题,图中字不清晰,翻译暂略)
图1-2 函数分解树变成了格子架结构。图中阴影标记的任务是计算不同收益的基础函数,虚线指示了从一个分解的分支到另一个分支的调用,以此消除冗余。
图1-1被扩展后,增加了多种雇员收益的计算,这些收益计算都需要先完成某些相同的任务。 因为一些任务只能以一种唯一的方式实现,所以基本树对每一个收益的结构都是相同的。但是有些任务一模一样,为了避免冗余,树中的这些节点要被树中不同分支上的调用者所重用,这就导致了格子架结构。如任务“从数据库获得基本工资”就有许多调用者,它们散布在应用程序的不同部分。
格子架结构的主要缺陷在于可维护性。当某个分支上的上层函数需求发生变化时(比如,图1-2中的保险收入),可能需要修改这个函数的位于树的非常底层的子函数。但是由于重用的关系,这个子函数可能有许多位于其他分支上的调用者(比如从数据库获得基本工资)。当前所做的改动可能并不适用于其它不同分支上的调用者(比如 期权收益),因为重用子程序的每个不同的分支表示的场景可能都不尽相同。因此为了一个调用者对底层所作的修改可能损害处在不同场景下的其它调用者。
函数分解的最大难题,在于由深度优先处理方法导致的场景的不清晰。基本模式是“做这个” (Do this)。上层函数实质上只是指派底层函数去做事情的指令集合。要想发出一个指令,上层功能必须a 知道谁将去做这个事情 b 知道他们能做这个事情并且 c 在整个解决方案中知道哪一步要开始做这个事情。所有这些都某种程度上违背了功能独立原则,其中最后一条是最让人头疼的,因为这要求调用函数了解整个问题解决方案中许多高级场景。高层函数在它的实现中直接关联场景知识,所以当需求为了全局场景发生变化时,相应的实现也必须要改变。这种层级的功能依赖,以及刚才提到的诸多问题,就产生了传说中的“面条代码”。
另一种角度是通过程序的规格说明和针对契约编程(DbC)的契约。当一个函数被调用时,就与调用者有一个“做这个”的契约。这个契约体现了对这个函数所提供服务的期望。至于是函数自己提供所有服务,还是委托部分或全部功能给底层的函数,是不重要的。从调用者角度来看,与相关函数的契约是针对整个服务而言。如果相关函数是树中的上层函数,关于这个函数的规格说明就是所有下层分支的规格说明总和。由上层函数衍生出的底层函数作为上层函数的延伸,它们的个体规格说明就是上层函数规格说明的一个子集。
这意味着在格子结构中,所有上层函数衍生出的分支,构成了有上层函数为起点的复杂的依赖关系。这就是说,为了履行基于契约的对调用者的责任,一个上层函数依赖于每一个衍生出的底层函数能够按照调用者的期望正确的工作。这个依赖关系链就是产生面条代码的本质原因。改变底层函数的说明,不可能不影响到这个长链条上潜在调用者的规格说明。
浙公网安备 33010602011771号