软件工程的事实与谬误 by Robert L·Glass

cover.jpg

软件工程的55个事实和5+5个谬误。
划重点:

  • 软件过程和软件产品的复杂性决定了我们在该领域的许多认识和行为。复杂性不可避免,我们不应该与之对立,而应该学会适应它。
  • 在软件领域,糟糕的估算和由此带来的时间表压力一直在迫害我们。
  • 在软件管理者和技术人员之间有隔阂
  • 鼓吹通用的观念影响了我们形成专注于有力的、明智的项目方案的能力。

管理

处于管理最底层且专长于干活的人通常比其他任何人有更大的威力。

人员

1.在软件开发中,最重要的因素不是程序员采用的工具和技术,而是程序员自身的质量。

CMM假设良好的过程会得到良好的软件。

Q:假如你的生命依靠某一软件,那么,关于该软件你最想知道什么? A:与其他事情相比,我最想知道写该软件的人,这人一定才华横溢,严谨认真,狂热追求软件完美并按照需求运作。其他对我来说都是次要的。

2.对“个体差异”的研究表明,最好的程序员要比最差的程序员强28倍之多,即使他们的报酬不同,优秀程序员也是软件业中最廉价的劳动力。

3.给延期的项目增加人手会使项目进一步延期。
Brooks法则,出自《人月神话》

4.工作环境对工作效率和产品质量具有深刻影响。
你必须找到优秀的人员并善待他们,特别是给她们提供舒适的环境。

工具和技术

5.夸大宣传是软件的瘟疫,多数软件工具对于效率和质量的提高幅度仅为5%~35%,但总有人反复说提高幅度是“数量级”的。

6.在学习新工具或者新技术的初期,程序员的工作效率和产品质量都会下降。只有克服了学习曲线之后,才可能得到实质性的收益。只有满足下面两个条件,采用新工具或者新技术才有意义:(a)新东西确实有用;(b)要想获得真正的收益,必须耐心等待。

7.软件开发者对于工具说的多,评估的少,买的多,用的少。

估算

8.项目失控的两个最主要的原因之一是糟糕的估算(另一个原因见事实23)。

估算是确定项目成本和开发时间表的过程。

依靠专家—无论专家曾经做过什么项目,无论这些项目与当前项目何等相似,专家都不可能做出合理的推断(软件项目的重要特征之一是各个项目所解决的问题差异很大。)

依靠估算算法(复杂算法,Line of Code,Function Point)—采用一个假想的项目,按照各种建议算法输入相关数据,得到的结果差异很大(2~8倍)。

9.许多估算是在软件生命周期开始时完成的。后来,我们才认识到在需求定义之前,即理解问题之前进行项目估算是不正确的;也就是说,估算时机是错误的。

10.许多软件项目都是由高层管理人员或者营销人员来估算,而不是由真正构建软件的人或者他们的主管来进行估算。因此,估算软件的人员是错误的。

高级人员和营销人员搞“政治”预测;软件人员做“理性”预测。

许多(70%)估算是由与“人事部门”相关的人员完成的,很少(4%)由项目团队进行估算。

11.软件估算很少根据项目进度进行调整。因此,这些估算通常是错误的人在错误的时间得出的错误结果。

12.因为估算的数据是如此糟糕,所以在软件项目不能达到估算目标时,不应该再考虑估算。但是无论如何,每个人都在考虑它。

极限编程建议客户或者用户先确定成本、时间表、功能和质量这四个因素中的前三个,软件开发者再确定第四个。

我要求参与者完成一个小任务。我有意增加他的工作量,使工作时间不足。我希望参与者尽力正确地完成整体工作,这样他们会因为时间不够而得出一个未完成的作品。事实并非如此,这些参与者按照不可能的时间表勉强完成了工作。他们的作品粗糙而虚假,看起来完整但根本不可用。这说明人们为了满足不可能的时间表,以至于愿意为此牺牲作品的完整性和质量。

13.在管理者和程序员之间存在隔阂。对于一个未满足估算目标的项目的调查表明:从管理者看来这是一个失败的项目,而在技术人员看来却是最成功的项目。

根本就没有做过估计的项目进展速度最快,其次是技术人员做估计得项目,最糟糕的是由管理人员做估计。

在工作效率和驾驭感之间有非常强的关联性。也就是说,如果程序员感觉到能驾驭自己的结局,那么他们的工作效率会高很多。换句话说,高度控制的管理并不一定会得到最好的或者效率最高的项目。

14.对于可行性调研的回答几乎总是“可行”。

复用

15.小规模的复用(子程序库)开始于50多年以前,这个问题已经得到很好的解决。

16.虽然每个人都认为大规模服用(组件)非常重要、非常急需,但是这个问题至今还没有基本解决。

作者认为此问题无解,根源在于软件的多样性。

17.大规模复用最好适用于相关的系统,也就是依赖于具体应用领域,这样就限制了它的应用范围。

在一个更小的特定应用领域中采用大规模复用的方法,成功的概率就比较大。而在跨项目和跨应用领域中采用大规模复用方法的成功概率很小。(McBreen 2002)。

18.有关复用问题,有两个“三倍法则”:(a)构建可复用的组件比使用组件难三倍;(b)在将组件收录到复用库并成为通用组件之前,应该在三个不同的应用中尝试使用该组件。

19.修改复用的代码特别容易引起错误。如果一个组件中超过20%~25%的代码需要修改,那么重新实现的效率会更高。

20.设计模式复用是解决代码复用中固有问题的一种方法。

设计模式是对反复出现的问题以及该问题的解决方案的一种描述。

设计模式源于实践,而不是源于理论。

复杂性

21.问题的复杂性每增加25%,解决方案的复杂性就增加100%。这不是一个可改变的条件(即使人们都努力降低复杂性),而是客观存在的。

22.80%的软件工作是智力活动。相当大的比例是创造性的活动。很少是文书性的工作。

生命周期

生命周期(software life cycle)开始于需求的定义和开发,在这一阶段,定义和分析“什么”问题。接着是设计,在这一阶段确定如何解决问题。然后是编码,将设计转化为计算机上可运行的代码。随后,因为整个过程中极易出现错误,所以进行错误消除。最终,完成了全部测试之后,软件产品交付使用,便开始了维护.
不同的步骤形成了瀑布模型,螺旋模型等,这些模型只是将步骤的顺序调整,需求、设计、编码、错误消除和维护,这些步骤都需要完成。

需求

23.导致项目失控的两个最常见原因之一是不稳定的需求(另一个见事实8所说的项目估算失误)。

24.在产品完成时修订需求错误的代价最大,在开发早期修订需求错误的代价最小。

错误在软件的存留时间越长,修订的代价越大。

怎么做?

  • 计算机学者会坚持采用格式化的规格说明书技术;
  • 开发者则将复审放在首位;
  • 测试和质量人员要求有可测试的需求,并建立早期测试用例;
  • 系统分析员可能会要求采用建模的方法;
  • 极限编程者提倡在开发团队中吸纳一个客户代表。

25.遗漏需求是最难修订的需求错误。

最持久的软件错误是遗漏逻辑错误,它可以逃过软件测试过程,进入发布的产品中。遗漏需求会导致遗漏逻辑。

设计

26.从需求转入设计时,因为制定方案过程的复杂性,会激增出大量的衍生需求(针对一种特定设计方案的需求)。设计需求时原始需求的50倍之多。

虽然大家都认为需求追溯很有必要,但是需求扩充在一定程度上影响了需求追溯。需求追溯是指在产品的各个阶段的制品中追溯原始需求。

27.对于一个软件问题,通常不存在唯一的最佳设计方案。

在一个房间中坐满了顶级的软件设计人员,如果其中任意两个人达成一致,那就可以通过了。—Bill Curtis

28.设计是一个复杂的、迭代的过程。最初的设计方案可能是错误的,当然也不是最优的。

“从难点开始”。

设计方案可能是启发式的、试验性的。

复杂的设计过程通常不能得出最佳的结果,但是我们必须尽力寻找一个“令人满意的”方案。找到最佳设计方案不可能或者代价太高,而“令人满意的”方案(而不是最佳方案)可以满足优秀设计标准,是值得(冒险)选择的解决问题的方法。

编码

29.从设计转到编码阶段时,设计者按照自己掌握的水平,已经将问题分解为“原语”。如果编程者和设计者不是同一个人,二者的“原语”不吻合,就会出问题。

如果设计者的原语层次比编码者高,编码者无法将此设计作为起点。因此,编码者在真正编码之前需要花费时间完成额外的设计,填补中间的层次。

不要轻易将设计工作和编码工作分开。

30.COBOL是一种糟糕的语言,但是其他的(用于商业数据处理的)语言也同样糟糕。

31.错误消除是软件生命周期中最耗时的阶段。

对于许多软件产品而言,错误消除所用的时间比汇集需求、进行设计或编码都长,通常长一倍。

“需求-分析-编码-错误消除”所占比例:20-20-20-40 或者 25-25-20-30。

测试

32.普通程序员认为已经彻底测试过的软件其实只执行了55%~60%的逻辑路径。采用覆盖分析器等自动工具,可以将上述比例提高到85%~90,几乎不可能测试软件中100%的逻辑路径。

  • 需求驱动测试(测试是否满足了所有需求)
  • 结构驱动测试(测试已构建的软件的所有组成部分是否正确运行)
  • 统计驱动测试(随机测试确定软件执行的时间和结果)
  • 风险驱动测试(测试确定是否已经消除了最主要的风险)

由于软件产品固有的复杂性,任何测试都不会是彻底的测试。因此,(a)测试工作实际上是一种折衷的活动,关键是作出适当的折衷选择;(b)许多重要软件的发行版中存在错误,这不足为奇(追求无瑕疵软件的想法是天真的)。

33.即使测试覆盖有可能达到100%,这种测试也不够。大约35%的错误是源于逻辑路径的缺失,还有40%的错误源于执行特定的路径组合。不可能实现100%的覆盖。

为了构建成功的、可靠的软件,需要综合采用多种错误消除方法,通常是越多越好。对于这个问题,没有神奇魔法。

34.没有工具就无法做好错误消除工作。人们常用调试器,很少使用覆盖分析器等其他工具。

35.自动测试很少,也就是说有些测试可以也应该自动化,但是有许多测试任务不能自动完成。

36.程序员在程序中嵌入测试代码、目标代码中的编译参数等方法,都是测试工具的重要补充。

评审和检查

37.在运行第一个测试用例之前进行严格审查可以消除软件产品中多大90%的错误。

要找到同一个错误,审查的成本低于测试的成本。

在软件生命周期各个阶段的产品都可以审查。

决定检查成败的关键不是采用形式化的过程,而是团队成员在审查过程中的严格程度和注意力集中程度。

38.虽然严格审查有很多优点,但是不能也不应该代替测试。

39.通常认为,事后评审对于了解客户的满意程度和改进过程都很重要。但是很多软件公司不开展事后评审。

软件业的智慧一直没有增长。

我们在疯狂追求新东西时,往往抛弃了许多旧东西(例如极限编程和敏捷开发等最新的软件方法倾向于拒绝老方法中积累的智慧)。

软件业一直忙于加速工作,以致于没有时间考虑如何做得更好,而不只是更快。

40.同行评审涉及技术和社会两方面问题,忽视任何一方面都会产生严重的灾难。

在评审过程中,参与者应该尽力熟知当前软件中的每一个决定和细节。

评审者必须从评审对象作者的角度来处理评审对象,而不是按照自己的方式,所以评审非常难。许多人穿上别人的鞋都寸步难行。

大多数人在软件产品中投入大量的情感和智力,因此如果被别人评头论足将会非常敏感。

禁止经理参加评审(他们倾向于评审开发者,而不是评审产品);
禁止没有准备的人参加评审(他们会使有准备着失望,还会偏离主题);
开发者不能作为评审主管(缩小可能涉及到开发者的自我因素)。

维护

41.维护开支通常占软件成本的40%~80%(平均为60%)。因此,维护可能是软件生命周期中最重要的阶段。

软件维护就是在发现错误时进行修订,并在必要时做修改。

古老的硬件会被抛弃,古老的软件每天都在使用。

42.增强功能大约占软件维护成本的60%,错误更正仅占17%。因此,软件维护的主体是在旧软件中加入新功能,而不是更正错误。

60%: 改进,增强功能
18%: 适应性维护,即在改变工作环境时保证软件正常运行,如再另一台计算机上运行、在另一种操作系统上运行、与新的软件包交互、引入新设备等。
17%: 错误更正
5%: 为了使软件更容易维护所作出的维护工作(预防性维护Preventive Maintenance,重构refactoring)

60/60规则:60%的软件成本用于软件维护,维护成本60%用于功能增强。因此,增强旧软件是个大问题。

43.维护是解决方案,而不是问题。

假设软件维护只是修正错误,那么软件维护才是个问题。

只有维护才能解决在软件中独有的一个问题,即“我们已经构建了一个东西,但是现在需要一个稍微不同的东西。”

44.比较软件开发和软件维护中的工作,除了维护中“理解现有的产品”这项工作之外,其他工作都一样。这项工作占据了大约30%的维护时间,是主要的维护活动,因此可以说维护比开发更难。

45.更好的软件工程开发导致更多而不是更少的维护。

与构建糟糕的系统相比,构建良好的系统更容易实现功能增强,这样人们对他们的修改更多,导致这些系统的维护时间更长。

如果我们把维护活动视为一种方案,那么维护越多越好。假如我们执意认为维护是一个问题,那么就无法将维护活动的增加视为一件好事。

质量

质量

46.质量是一组属性的组合。

  1. 可移植性是指生成易于在不同平台之间移植的软件产品。
  2. 可靠性是指软件产品满足预期的要求,值得信赖。
  3. 效率是指软件产品在运行时间和空间消耗上的经济性。
  4. 人类工程学(又称为可用性)是指软件产品用起来既容易又舒服。
  5. 可测试性是指软件产品易于测试。
  6. 易理解性是指维护者易于理解软件产品。
  7. 可修改性是指维护者易于修改软件产品。

47.软件质量不是用户满意、满足需求、满足成本和时间表目标,或者可靠性。

用户满意 = 满足需求 + 按时提交 + 适当的成本 + 产品质量

可靠性

  1. 绝大多数程序员都会犯某些错误。

49.错误通畅聚集在一起。

半数的错误发现在15%的模块中。

50.没有唯一最好的消除软件错误的方法。

51.总会有残存的错误。我们的目标应该是消除严重错误,或者使之最少。

一项2002年的研究:两个采用完全不同软件开发方法(一个团队使用传统方法,处于CMM4;另一个团队采用前卫的形式化方法)的团队都不能构建一个可靠性达到98%的简单产品。

关于残存错误:

  • 有经验的个人实践可以使错误的发生减少75%
  • 大约40%~50%的用户程序包含着较显著的缺陷。
  • 你不会发现所有的bug
    关于残存的严重错误:
  • 小于10%的错误导致90%故障的发生。

效率

52.效率主要来自于优秀的设计,而不是优秀的编码。

数据结构是增加逻辑复杂性和改善数据访问效率之间的折衷。因此在设计阶段应认真选择正确的数据结构、文件结构或者数据库访问方法。

在设计阶段对效率的细微考虑比漂亮的编码更有意义。

53.高级语言(High-order language,HOL)代码配合适当的编译器优化,大约可以达到汇编语言90%的效率。对于一些复杂的现代体系结构,效率更高。

54.在空间和时间之前存在折衷。通常,改进一方面会降低另一方面。

三角函数:函数值列表,空间换时间。
Java: 编译后的字节码比机器码紧凑,但是时间效率很低;JIT(Just-In-Time)编译器可以将字节码编译为机器码。

研究

55.许多软件研究者不是做调查,而是鼓吹。因此,(a)有些概念比鼓吹的糟糕、更少;(b)缺少有助于确定这些概念真实价值的评估性研究。

谬误

1.你不能管理自己无法度量的东西。

优秀的知识管理者趋向于定性度量,而非定量度量。

实际情况是度量对于软件管理非常重要,谬误存在于具体进行度量的手段和过程中。

2.可以管理软件产品的质量。

所有的质量属性都有很深刻的技术内容,只有技术人员才能处理这些技术内容。

3.可以,也应该“忘我”地编程。

我们不可能为了满足别人的需求来抑制自己的需求,我们也同样不太可能为了团队的利益而抑制自我。一个有效运行的系统必须承认人的个性,也必须在这些个性的范围内运作。

4.工具和技术是通用(one site fits all)的。

5.软件需要更多的方法论。

6.要估算成本和时间表,应首先估算代码行数。

7.随机测试输入是优化测试的好方法。

8.如果有了足够多的关注,所有的bug都显而易见。

  • 错误的深浅与查找错误的人数没有关系。
  • 有关审查的研究表明:增加审查人数,发现错误数量的增加幅度会迅速减少。
  • 没有数据表明这句话的正确性。

9.估计将来的维护成本和做出产品更新的决策需要参考过去的成本数据。

10.教别人编程的方法是教别人写程序。

就像写作一样,要先读后写。

  • 要教授读代码,我们必须选择所读的范例。
  • 要教授读代码,需要指导性的教科书,但是却没有。都是关于如何写代码的。
  • 标准课程教授先写后读,形成了制度。
  • 我们在软件中需要读代码的唯一时机是维护。维护很不受欢迎,理由之一是读代码是一项非常难的活动。发挥你的创造力写新代码比读别人的老代码有意思得多。

成为程序员最好的方法是写程序,研究别人所写的优秀的程序。。。我到计算机科学中心的垃圾桶里找到了他们操作系统的列表。 —Bill Gates