现在的位置: 首页 > 综合 > 正文

软件固有的复杂性

2013年08月29日 ⁄ 综合 ⁄ 共 3547字 ⁄ 字号 评论关闭

软件固有的复杂性

一颗垂死的恒星正处在塌缩的边缘,一名儿童在学习如何阅读,白细胞向病毒发起进攻——这是真实事件的几个例子,它们包含着真正可怕的复杂性。软件也可能包含巨大复杂性的元素,但是这里的复杂性基本上是另一种类型。Brooks曾指出:“爱因斯坦认为自然界必定存在着简单的解释,因为上帝不是反复无常或随心所欲的。软件工程师没有这样的信仰来提供安慰。许多必须控制的复杂性是随心所欲的复杂性。”

定义软件复杂性

我们确实注意到,某些软件系统并不复杂。这些大多是可以被遗忘的应用,它们是由一个人提出、构建、维护和使用的,这个人通常是编程新手或独立工作的专业开发人员。但这并不是说所有这些系统都是拙劣和不优雅的,我们也不是想贬低它们的创造者。这些系统的目的通常很有限,生命周期也很短。我们可以扔掉它们,用全新的软件代替它们,不必尝试复用、修复它们或者扩展它们的功能。这样的应用开发起来通常难度不大,但比较乏味,因此,我们对学习设计这样的应用兴趣不大。

与之相反,我们对所谓的“工业级”的开发挑战,兴趣要大得多。我们发现,这些应用表现出非常丰富的行为,它们存在于反馈式系统中,由真实世界的事件驱动或发出驱动事件。对这些应用来说,时间和空间都是稀有资源。这些应用维护着数百万条信息记录的完整性,同时允许并发的更新和查询。这些系统发出命令并控制真实世界的实体,例如,控制空中交通和铁路交通。这样的软件系统通常具有很长的生命周期,随着时间的推移,许多用户渐渐依赖于这些软件系统的正常工作。在工业级软件的世界里,我们也会看到一些框架,它们简化了特定领域应用程序的创建,还能看到一些程序模仿了某方面的人类智能。尽管这些应用通常是研发的产品,但它们的复杂性一点也不差,因为它们是增量式开发和探索式开发的方法和基础。

工业级软件的特征是,单个开发者要理解其设计的所有方面非常困难,几乎是不可能的。武断地说,这些系统的复杂性超出了人类智能的范围。不幸的是,我们所说的这种复杂性似乎是所有大型软件系统的基本特征。从根本上来说,我们可以掌握这种复杂性,但不能消除这种复杂性。

为什么软件在本质上是复杂的

正如Brooks所指出的:“软件的复杂性是一个基本特征,而不是偶然如此”[3]。我们认为这种固有的复杂性有4个原因:问题域的复杂性、管理开发过程的困难性、通过软件可能实现的灵活性,以及刻画离散系统行为的问题。

1.问题域的复杂性

我们在软件中试图解决的问题常常涉及不可避免的复杂性,在其中我们可以发现数不清的竞争性需求,甚至是相反的需求。请考虑一下一架多引擎飞机的电子系统的需求、一个蜂窝式移动电话交换系统的需求或一个自动化机器人的需求。这些系统的基本功能已经很难理解了,现在还要加上所有的(常常是隐含的)非功能需求,如可用性、性能、成本、健壮性和可靠性。这种无限制的外部复杂性是导致Brooks所说的任意复杂性的原因之一。

这种外部复杂性通常源自于系统用户和系统开发者之间的“沟通困难”:用户常常发现很难用开发者能够理解的形式对他们的需求给出准确的表述。在某些情况下,用户只是对想要的软件系统有一个模糊的想法。这既不是系统用户的错,也不是系统开发者的错,出现这种情况是因为这些人都缺乏另一个领域的经验。用户和开发者对问题的本质有着不同的看法,并根据解决方案的本质做出了不同的假定。实际上,即使用户对他们的需求知道得很清楚,我们目前也没有什么好方法来准确地记录下这些需求。常见的描述需求的方法是用一大段文字,偶尔配有一些插图。这样的文档是难以理解的,可能产生不同的解读,而且经常包含一些设计方案,而不是基本需求。

更麻烦的是,软件系统在开发过程中经常发生需求改变,主要是因为软件开发项目本身改变了问题的规则。看到早期的产品(如设计文档和原型)之后,在安装好并使用系统之后,在强制使用所有功能之后,用户会对他们的需求有更好的理解和表述。同时,这个过程也帮助开发者了解了问题域,使他们能够问出更好的问题,从而照亮系统期望行为中的黑暗角落。

 

因为大型的软件系统是一项投资,所以我们不能够忍受在每次需求发生变化时,就抛弃掉原有的系统。不管是否有计划,系统都会随时间的推移而演化,这种情况常常被错误地称为软件维护。准确地说,在我们修正错误时,这是维护;在我们应对改变的需求时,这是演化;当我们使用一些极端的手段来保持古老而陈腐的软件继续工作时,这是保护。不幸的是,事实表明相当一部分软件开发资源被用在了软件保护上。

2.管理开发过程的困难性

软件开发团队的基本任务就是制造简单的假象——让用户与大量的、通常是任意的外部复杂性隔离开来。当然,在软件系统中规模大并不是太好的事情。我们追求的是通过发明一些聪明的、强大的方法来实现少写代码,从而给我们一种简单的假象,另外也复用已有的设计和代码的框架。然而,即便是系统需求的数量有时候也是无法逃避的,这迫使我们要么编写大量的新软件,要么以创新的方式复用已有的软件。就在几十年前,只有几千行的汇编程序就已经是软件工程能力的极限了。但在今天,常常看到交付系统的代码规模达到几十万甚至几百万行(而且这些都是用高级语言写的)。没有哪个人能完全理解这样一个系统。即使以有意义的方式对我们的实现进行分解,也会得到数百个或数千个独立的模块。这样的工作量要求我们启用开发团队,而且理想情况下团队越小越好。但是,不论团队的规模有多大,团队开发总会面临一些重要的挑战。有更多的开发人员就意味着更复杂的沟通,因此更难以协调,特别是当开发团队的地理位置分散的时候,而实际情况又常常如此。对于开发团队来说,主要的管理挑战总是维持设计的一致性和完整性。

3.软件中随处可能出现的灵活性

一家造房屋的公司通常不会自己经营林场,砍伐树木以获取原木。我们也很少看见一家建筑公司建造一个现场的钢铁厂,为新的建筑提供定制的大梁。但在软件行业,这种情况却经常发生。软件提供了非常大的灵活性,所以开发者几乎有可能表达任何形式的抽象。但是,这种灵活性变成了一种难以置信的诱人的属性,因为它也迫使开发者打造几乎所有的初级构建模块,高层的抽象将建立在这些初级构建模块之上。建筑行业对原材料的品质有着统一的编码和标准,但软件行业却很少有这种标准。结果,软件行业还是一种劳动密集型的产业。

4.描述离散系统行为的问题

如果向空中抛出一个球,我们可以肯定地预测出它的路径,因为我们知道在正常的情况下,某些物理定律会起作用。如果因为我们在抛球时用的力大了一些,结果它就在飞行到一半的时候突然停下来,然后直接往上冲,那么我们将感到非常惊奇。[1]但是在一个调试得不太好的模拟球的运动的软件中,类似这样的行为却很容易发生。

在大型应用中,可能有成百上千个变量以及多个控制线程。系统中的这些变量、它们当前的值、当前的地址和每个过程的调用栈一起构成了应用当前的状态。因为我们是在数字计算机上执行软件,所以我们的系统具有离散的状态。与此形成对比的是,像抛球运动这样的模拟系统是连续的系统。Parnas指出:“当我们说系统是由连续函数描述的时候,我们是说它不会包含任何隐含的惊奇。输入中的小变化总是会导致输出中相应的小变化”[4]。而在另一方面,离散系统从本质上来说具有有限数量的可能状态。在大的系统中,由于组合的缘故,导致可能状态的数目变得非常大。我们试图以分离关注的方式来设计我们的系统,这样,系统某部分的行为对其他部分行为的影响就能降至最低。但是有一个事实仍未改变,即离散系统中的状态转换不能够用连续函数来建模。软件系统之外的每个事件都有可能让系统进入一个新的状态,而且,状态与状态之间的转换关系并非总是确定的。在最坏的情况下,外部的事件可能会破坏系统的状态,因为它的设计者没有考虑到事件之间的相互作用。如果一艘船的推进系统由于计算溢出而失效,其原因是某人在维护系统中输入了错误的数据(一次真正的事故),我们就理解了这个问题的严重性。在地铁、汽车、卫星、航空交通控制、仓库等系统中,与软件相关的系统故障大量上升。在连续系统中,这类行为不太可能发生,但在离散系统中,所有外部事件都有可能影响系统内部状态的任何部分。当然,这是对系统进行大量测试的主要原因,但除了那些极其微不足道的系统之外,穷尽所有可能的测试是无法做到的。既然数学工具和我们的智能都不能够对大型离散系统的完整行为进行建模,对于系统的正确性,我们必须满足于可接受的信心级别。

 

 

本文节选自《面向对象分析与设计(第3版)》一书

()
布奇(Booch,G.) 等著

王海鹏,潘加宇译

电子工业出版社出版

图书详细信息:http://blog.csdn.net/broadview2006/article/details/7896226



 

抱歉!评论已关闭.