《基于模型的软件开发》——1.3 宝贵教训

1.3 宝贵教训

当20世纪70年代末学术界开始对对象思想反思的时候,很多蹩脚的软件已经开发出来了,多到这些蹩脚软件中存在的通用模式已经非常明显了。很多论文和文档由此而来,例如1968年Dijkstra的经典文档:“Go To Statement Considered Harmful”。这些模式代表了从软件开发中获得的惨痛教训(其中有一些是刚刚触及)。
1.3.1 全局数据
持久状态变量用于记录系统的状态,当它们为全局可访问的时候经常会在意想不到的时间或者以意想不到的方式被修改,这是一个很基本的问题。如果写代码的时候假定了一个特定的状态,但是当代码执行的时候该假定状态没有达到,就会导致错误。
这样的问题在实践中很难诊断,因为它们往往是间歇性的。这中间还有一个维护的问题,当一些新的条件必须增加的时候,很难找齐数据被写入的所有地方,所以不相关的维护往往会导致新缺陷的引入。这个问题非常严重,以至于在20世纪70年代末期,每一个3GL的IDE都具有某种形式的查找/浏览功能,方便用户找到一个变量存在的所有位置。虽然对于开发人员来说,这样的功能提供了方便,但是该功能并不能真正有效地帮助管理状态数据。该问题非常显著,因此函数式编程范式被开发出来,与OO范式一样,很大程度上是为了解决状态数据持久性问题。
1.3.2 巨程序单元
之前有一个人问自己的同事某个特定编译器错误产生的原因。该同事之前没有见过这个信息,但是听起来很像编译器堆栈溢出的错误,于是他就去看了代码。出现错误的过程有1800行那么长,在同一个文件中,这样的过程有很多个。仅仅打印编译列表就用了半盒纸!由于编译器进行的是文件层的优化,因此问题非常难以思考和拆分。
当这位同事看到程序的长度时,他建议说最基本的解决方法就是“不要做这些”。这时,代码的作者花了10分钟痛斥这个编译器是多么糟糕,竟然不允许他用最可读的方式写代码,例如,长达十页的嵌套switch语句。最可悲的是这件事情发生在1985年,当时的人应该对程序了解更多。
大的程序模块会导致程序缺少内聚性。一个模块做了太多的事情,在模块内部,这些事情相互之间还存在紧密的关系。这会导致模块很难维护,因为一个功能的变更与模块中其他的功能之间不是隔离的。另一个常见的问题是抽象层次的混杂。这样的模块混合了高层次功能与低层次功能。这样导致高层次的处理被低层次的细节所掩盖,出现一个典型的“森林和树”的问题。
最糟糕的问题也许是模块越大,它与其他模块的交互往往越多,那么产生负作用的可能性就越大。所有开发具有可维护性代码的现代方法都主张缩小范围,范围的扩大会导致副作用。限制范围最简单的方法就是保持小模块。
1.3.3 软件结构
“架构”这个词20世纪70年代出现于软件文化当中,因为人们开始逐渐意识到软件应用与建筑或土豚一样具有骨架。关于结构的惨痛教训是,变更它通常需要大量工作。因为应用中有太多的其他内容挂在该结构之上。
有以下四种现象可以表明应用中的结构正在发生变化:
1)有很多小的变更;
2)变更横跨许多程序单元;
3)很难变更(例如:层次结构需要重新设计);
4)变更比以往更容易引入新缺陷、造成返工。
这些变更通常称为“重组猎枪”,在黑客时代存在大量这类变更。
1.3.4 缺乏内聚性
缺乏内聚性一个明显的早期现象是,当需求变更时许多模块都需要进行变更。其原因之一在于基本结构修改了,缺乏内聚性是另一个原因。功能分散到很多模块中实现,当该功能的需求变更时,所有相关的模块都会涉及。缺乏内聚性将会导致程序难以修改,因为相关的功能不能定位。因为不同的模块实现了一个给定功能的不同部分,所以这些实现还以一种特定的顺序依赖于其他的模块来实现该功能。尽管事发之后通常很容易判断出应用的内聚性很差,但是在应用开发的早期并没有一种系统化的实践能够很好地保证内聚性。
1.3.5 耦合
当前有很多书讨论耦合以及如何处理耦合,因此这个主题不在本书的实践范围之内。在执行摘要层次,耦合描述了程序元素之间密切联系的频率、性质和方向。逻辑耦合概念产生于对以下不同现象的协调:
“复杂管线”代码难以维护。
跨模块的耦合会导致它们之间的实现依赖。
如果客户端通过紧密关系了解服务器,那么当服务器变更时,客户端往往也需要随之变更。
如果服务器通过紧密关系了解客户端,那么当客户端变更时,服务器往往也需要随之变更。
对于两个元素同时进行变更的要求与它们之间的互相访问的性质成正比。
对于两个元素同时进行变更的要求与它们之间的互相访问的密切程度成正比。
双向的密切联系往往比单向的更糟糕。
耦合的概念由此而来,根据模块程序单元之间的交互和合作,来描述模块之间的依赖关系。然而,为了使程序运转,它的元素之间必须进行某种程度的交互。于是,开发人员往往会面临类似于热力学三定律的问题:1)不能取胜;2)不能暂停;3)不能
退出。
在考虑耦合的时候,区分逻辑耦合和物理耦合很重要。逻辑耦合描述了程序元素基于在问题解决方案中所扮演的角色,如何与其他程序元素建立关系。物理耦合从编译器的角度,描述了程序元素为了进行交互需要彼此了解哪些内容。在某种程度上,逻辑耦合是不可避免的,因为问题的解决依赖于此。但是逻辑耦合度可以通过良好的设计实践尽量
降低。
然而,物理耦合几乎完全来自硬件计算模型约束下实现3GL型系统的实际问题。为了使编译器能够在一个模块中对于处理过程产生正确的机器码,也就是说,数据通过过程调用从另一个模块传递过来,编译器需要知道数据如何由调用者实现(例如,整数与浮点数)。否则,编译器没有办法使用正确的ALU指令来处理它。所以编译器需要了解调用模块的实现,也就是由特定硬件确定的有关实现的物理依赖关系。这种耦合在“编译全世界”中表现得最为明显,尽管只有一个简单的变更,但是大应用中的许多模块都需要重新进行编译。
遗憾的是,人们之前关注的焦点一直是关于访问频率的。到了20世纪80年代,一些深入的思考开始担心耦合的问题。人们注意到,当模块依赖图是一个无环路有向图时,程序往往更加容易维护。人们还注意到,当编译时间和配置管理成为主要的问题时,那些非常大的应用就会变得不可维护。如何在程序元素之间减少和控制物理依赖是一项巨大的挑战。处理该问题的技术通常被称为依赖管理。
迄今为止,文献很少涉及耦合的性质。由于耦合的性质在判断一些MDB实践时扮演重要的角色,因此在此有必要确认耦合性质的基本分类方法(即,按照联系的密切程度),以下按照密切程度增加的顺序进行论述。
仅依赖于消息标识符:它是一则纯粹的消息,没有数据也没有行为。这种依赖导致搬石头砸到脚的机会是很小的,然而即使是这样朴素的形式,当消息到达的位置不对或者出现的时间不对时,仍然会造成问题。
按值传递数据:这种形式仍然是较为良好的,因为在这种形式下,没有影响消息发送方的方式,接收方对数据的处理可以全权控制。比起纯粹的消息,这种形式的缺点在于,当处理消息时,该数据在程序的上下文中可能已经不正确了。这种情况通常存在于异步或者分布式环境中:消息发送和处理有可能存在延时。在并行处理的线程应用中也有可能存在这样的问题。
按引用传递数据:这种情况下会产生新的问题,接收方能在发送方不知情的情况下修改发送方正在使用的数据。数据的完整性变成了一个严重的问题。实际上,按引用传递数据加剧了并行处理环境中的问题,因为接收方(或者发送者通过引用对其进行了数据传递的任一方)能够在发送方使用数据的时候改写它。
按值传递行为:这种奇特的现象出现于在消息中传递小程序(applet)的现代程序中。它与按引用传递数据类似,只是在这种情况下是接收方会受到意想不到的影响。原因在于applet在其所做事情中没有受到约束。当接收方调用行为的时候,不能对其进行有效的控制,也不能预测没有数量限制的潜在副作用。如果你不觉得applet是一个问题,那么去问一下处理网站安全性的那些人吧。
按引用传递行为:尽管以前有一些语言可以通过传递函数指针的方式做到这一点,但是这种做法是非常罕见的。在面向对象的编程语言中这通常可以通过对象引用而平平常常地实现。就像FORTRAN语言的GOTO指令一样,这在当时似乎是个好主意,但是后来结果很严重。今天我们意识到这绝对是最差的耦合形式,它打开了一个巨大的潘多拉盒子。除了调用行为时会出现的各类潜在的副作用之外,接收方还能够在发送方不知情的情况下改变对象的信息。由于类的整个公共接口暴露了出来,因此接收方可以自由地调用对象的任何一个方面,尽管这些方面可能会导致发送方的问题。
最糟糕的是,发送方的封装被破坏殆尽。因为那些被传递了引用的对象可能是发送方实现的一部分。通过这样的传递,发送方的实现打开了一扇窗户,这引入bug和维护的噩梦。(在OO中,这种做法将隐藏实现破坏了,而隐藏实现是OO的一种基本做法。)这意味着维护人员必须跟踪每一处使用引用的地方,并验证它所有使用方式的变更都不会造成任何破坏。即便如此,我们也不能保证在新一轮的维护中,人们不会因为对接收方的变更而导致实例访问不正确。即使一切都能够工作,由于物理耦合的关系,仍然需要重新构建接收方。
因为这是一个广泛的话题,所以本书中不再进一步讨论耦合。但是,重要的是要了解:耦合的概念形式化了一组可维护性问题,理解耦合是良好编程的基础,无论是否从事面向对象的开发。

时间: 2024-10-24 16:53:00

《基于模型的软件开发》——1.3 宝贵教训的相关文章

《基于模型的软件开发》——1.4 技术革新

1.4 技术革新 在OO范式之前,即使是编程的"黑暗时代",也并非一片混沌.学术界一直致力于调整数学运算使其能够应用于计算环境下的实践.经过认真思考,学术界提供了一种数学通用语言作为软件开发的基础.这是一个特别适用于计算环境的理论和模型集合.今天这些数学运算仍然是软件开发时一切工作的基础. 本书的重点为软件开发的工程方法,因此不会涉及很多理论.然而,主要的相关数学元素以及它们对软件开发的影响很值得用几个小节高屋建瓴地描述.因为这些理论现在仍然存在于OO范式背后,尽管是以一种非常隐秘的方

《基于模型的软件开发》——导读

前言 软件开发是一项极其复杂的智力活动,它是一门朝气蓬勃并且仍在迅速发展的学科.软件开发还不够完善,因此迄今人们仍然在试图找出开发软件的好方法. 尽管如此,多年来软件开发方法仍然获得了大幅提升.许多设计方法学不断发展以促进软件设计的各个方面.其中之一是结构化设计方法,该方法提供了一种非常直观的方式,用以很好地匹配图灵和冯·诺依曼的硬件计算模型. 尽管结构化设计明显优于它之前的特定方法,但它存在着一个致命的弱点:当用户需求随着时间的推移改变时,软件往往很难随之修改,大型的应用尤其如此.与此同时,应

《基于模型的软件开发》——2.1 基本理念

2.1 基本理念 OO范式较之以前的软件开发方法更加复杂精密.从硬件计算的角度我们并没有直观感受,因此需要一种独特的思想.该范式也是由很多不同的.独立的概念整合而成.在这里我们将明确这些与第二部分和第三部分密切相关的基本概念.2.1.1 可维护性 在20世纪70年代,关于软件开发的实证研究很多,从中可以归纳出以下两个惊人的结论: 大部分软件公司70%的工作量用于维护. 用于修正现有特性的工作量是最初开发工作量用的5-10倍. 这中间显然存在问题.20世纪70年代后期这一现象演变成了软件危机,软件

《基于模型的软件开发》——2.2 广度优先处理(又称对等协作)

2.2 广度优先处理(又称对等协作) 第1章讨论了SD的深度优先,分层的功能分解以及与此相关的问题.这次同样是对象的问题.由于抽象.封装和逻辑不可分性,对象具有自包含实现,因此它们允许广度优先通信.这是因为你让对象做什么它就做什么,并能够完成,这完全依赖于它的逻辑不可分性和自包含性. 从客户角度看它是原子的,一个对象就是单个可标识的实体.因此,应用中重要的控制流是按照对象的交互(OO中的协作)进行描述的.这在一定程度上大大提高了控制流的抽象层次.事实上,在本书后面的章节中你将看到,当定义协作时,

《基于模型的软件开发》——1.2 结构化开发

1.2 结构化开发 结构化开发无疑是20世纪80年代之前最重要的单个技术进步之一.它第一次为软件开发提供了真正意义上的系统化方法.与20世纪60年代的3GL结合,结构化开发能够使生产力取得重大进步.结构化开发具有一种很有趣的边界效益,这在当时未被注意到.应用更加可靠,当时人们没有留意这一点,因为软件应用得越来越广泛,对非软件的用户的可见性越来越高:软件依然存在很多缺陷,用户仍然认为软件是不可靠的.事实上,可靠性从20世纪60年代初的150个缺陷/千行降低到了20世纪80年代的15个缺陷/千行.结

《基于模型的软件开发》——第1部分 面向对象开发的根本

第1部分 面向对象开发的根本 基于模型的软件开发方法本质上是一种面向对象的方法.因此,为了充分了解这种方法,有必要大致理解面向对象的开发.由于面向对象的方法不如传统软件开发方法那样直观,因此我们需要理解面向对象方法的工作方式.本书这一部分着眼于面向对象方法诞生的历史背景,使我们能够了解传统方法存在的问题,也即面向对象的方法寻求解决的问题.

《基于模型的软件开发》——1.1 历史

1.1 历史 20世纪50年代基本上不存在系统化的开发,那是编程的"黑暗时代".现在的开发人员几乎很难想象当时的软件开发环境.当时大型主机的内存只有几KB(kilobyte,千字节),纸带就是高科技输入系统.西联曾经有效垄断了电传输入设备,该设备每按下一个键都需要做几英尺-磅的功,这些设备导致程序员患上腕管综合征,该症状甚至成了医学界的一个专有名词.没有浏览器.调试器或者CRT终端.基本汇编语言(Basic Assembly Language,BAL)是解决软件危机的银弹.20世纪50

《基于模型的软件开发》——第1章 历史的视角

第1章 历史的视角 问题是进步的代价.--Charles F.?Kettering和物理科学.工业革命相比,软件开发在人类发展史上相对较新.成为现代生活中无处不在的角色,物理科学花了一千多年.工业革命花了一个多世纪,而计算机和软件只花了30年不到的时间.当然,这中间走过的路是很艰辛的.本章提供了OO范式产生的历史背景.为了全面理解并评价这种范式,首先要了解它要解决的问题.因此我们从历史讲起,然后回顾一下在OO范式出现之前主流的软件开发方法存在的一些弱点,最后考察一些被OO范式采纳的重要技术进步,

《基于模型的软件开发》——3.4 泛型

3.4 泛型 泛型是我们都在实际使用但却没有意识到自己在使用的一种方法.泛型通过参数化替代不同的行为.从本质上讲,我们所获得的不同结果,取决于单个行为职责的输入参数值--这种想法在"汇编宏"方法使用时就已经存在了,因此我们在此不多做讨论.实际上,它的另外一个名字--参数多态,表明它是多态的一种特例. 任何具有以下特征的方法都可以在技术上视为泛型的实例,在这些方法中,不同的行为依赖于该方法的参数值.因此,任何一个对参数值进行if判断的方法都可以视为泛型的实例.然而,大部分OO人员通常从存