在众多软件项目中,缺乏合理的时间进度是造成项目滞后的最主要原因,它比其他所有因素加起来的影响还大。
用人月
作为衡量一项工作的规模是一个危险和带有欺骗性的神话。它暗示着人员数量和时间是可以相互替换的。
人数和时间的互换仅仅适用于以下情况:某个任务可以分解给参与人员,并且他们之间不需要相互的交流。
无论多少个母亲,孕育一个生命都需要十个月。
对于软件任务的进度安排,以下是我使用了很多年的经验法则:
进度落后时,削减任务常常是唯一可行的方法。
Brooks法则:向进度落后的项目中增加人手,只会使进度更加落后。(Adding manpower to a late software project makes it later)
同样有两年经验而且在受到同样的培训的情况下,优秀的专业程序员的工作效率是较差程序员的十倍。(Sackman、Erikson和Grand)
需要协作沟通的人员的数量影响着开发成本,因为成本的主要组成部分是相互的沟通和交流,以及更正沟通不当所引起的不良结果(系统调试)
Mills建议大型项目的每一个部分由一个团队解决,但是该队伍以类似外科手术的方式组建,而并非一拥而上。由一个人来进行问题的分解,其他人给予他所需要的支持,以提高效率和生产力。
外科医生。Mills称之为首席程序员。他亲自定义功能和性能技术说明书,设计程序,编制源代码,测试以及书写技术文档。他使用例如PL/I的结构化编程语言,拥有对计算机系统的访问能力;该计算机系统不仅仅能进行测试,还存储程序的各种版本,以允许简单的文件更新,并对他的文档提供文本编辑能力。首席程序员需要极高的天分、十年的经验和应用数学、业务数据处理或其他方面的大量系统和应用知识。
主张在系统设计中,概念完整性
应该是最重要的考虑因素。也就是说为了反映一系列连贯的设计思路,宁可省略一些不规则的特性和改进,也不提倡独立和无法整合的系统,哪怕它们其实包含着许多很好的设计。
概念的完整性要求设计必须由一个人,或者非常少数互有默契的人员来实现。
对于非常大型的项目,将设计方法、体系结构方面的工作与具体实现相分离是获得概念完整性的强有力方法。
在开发第一个系统时,结构师倾向于精炼和简洁。他知道自己对正在进行的任务不够了解,所以他会谨慎仔细地工作。
在设计第一个项目时,他会面对不断产生的装饰和润色功能。这些功能都被搁置在一边,作为”下一个”项目的内容。第一个项目迟早会结束,而此时的结构师,对这类系统充满了十足的信心,熟练掌握了相应的知识,并且时刻准备开发第二个系统。
第二个系统是设计师们所设计的最危险的系统。而当他着手第三个或第四个系统时,先前的经验会相互验证,得到此类系统通用特性的判断,而且系统之间的差异会帮助他识别出经验中不够通用的部分。
一种普遍倾向是过分地设计第二个系统,向系统添加很多修饰功能和想法,“它们曾在第一个系统中被小心谨慎地推迟了。
手册、或者书面规格说明,是一个非常必要的工具
形式化定义的优点和缺点。如文中所示,形式化定义是精确的,它们倾向于更加完整;差异得更加明显,可以更快地完成。但是形式化定义的缺点是不易理解。
很多工具可以用于形式化定义,例如巴科斯范式在语言定义中很常用
周例会和年度大会–这实际上是一种非常有效的方式。
这种会议的卓有成效是由于:
1. 数月内,相同小组–结构师、用户和实现人员–每周交流一次。因此,大家对项目相关的内容比较了解,不需要安排额外时间对人员进行培训。
2. 上述小组十分睿智和敏锐,深刻理解所面对的问题,并且与产品密切相关。没有人是”顾问”的角色,每个人都要承担义务。
3. 当问题出现时,在界线的内部和外部同时寻求解决方案。
4. 正式的书面建议集中了注意力,强制了决策的制订,避免了会议草稿纪要方式的不一致。
5. 清晰地授予首席结构师决策的权力,避免了妥协和拖延。
随着时间的推移,一些决定没有很好地贯彻,一些小事情并没有被某个参与者真正地接受,其他决定造成了未曾遇到的问题。对于这些问题,有时周例会没有重新考虑,慢慢地,很多小要求、公开问题或者不愉快会堆积起来。为解决这些堆积起来的问题,我们会举行年度大会,典型的年度大会会持续两周。(如果由我重新安排,我会每六个月举行一次。)
交流和交流的结果--组织,是成功的关键
。交流和组织的技能需要管理者仔细考虑,相关经验的积累和能力的提高同软件技术本身一样重要。
实践是最好的老师,但是,如果不能从中学习,再多的实践也没有用。
Aron、Harr和OS/360的数据都证实,生产率会根据任务本身复杂度和困难程度表现出显著差异。在复杂程度估计这片”沼泽”上的指导原则是:编译器的复杂度是批处理程序的三倍,操作系统复杂度是编译器的三倍。
对常用编程语句而言。生产率似乎是固定的。这个固定的生产率包括了编程中需要注释,并可能存在错误的情况.
使用适当的高级语言,编程的生产率可以提高5倍。
创造出自精湛的技艺,精炼、充分和快速的程序也是如此。技艺改进的结果往往是战略上的突破,而不仅仅是技巧上的提高。这种战略上突破有时是一种新的算法,如快速傅立叶变换,或者是将比较算法的复杂度从n2降低到nlogn。
更普遍的是,战略上突破常来自数据或表的重新表达–这是程序的核心所在。如果提供了程序流程图,而没有表数据,我仍然会很迷惑。而给我看表数据,往往就不再需要流程图,程序结构是非常清晰的。
很容易就能找到重新表达所带来好处的例子。我记得有一个年轻人承担了为IBM650开发精细的控制台解释器的任务。他发现用户交互得很慢,并且空间很昂贵。于是,他编写了一个解释器的解释器,使得最后程序所占的空间减少“到不可思议的程度。Digitek小而优雅的Fortran编译器使用了非常密集的、专业化的代码来表达自己,以至于不再需要外部存储。
对这种表达方式解码会损失一些时间,但由于避免了输入-输出,反而得到了十倍的补偿。
由于缺乏空间而绞尽脑汁的编程人员,常常能通过从自己的代码中挣脱出来,回顾、分析实际情况,仔细思考程序的数据,最终获得非常好的结果。实际上,数据的表现形式是编程的根本
。
说明了文档的重要性。
唯一不变的就是变化本身
一旦认识到试验性的系统必须被构建和丢弃,具有变更思想的重新设计不可避免,从而直面整个变化现象是非常有用的。
为变更计划系统
如何为上述变化设计系统,是个非常著名的问题,在书本上被普遍讨论–可能讨论得比实践还要多得多。它们包括细致的模块化、可扩展的函数、精确完整的模块间接口设计、完备的文档。另外,还可能会采用包括调用队列和表驱动的一些技术。
最重要的措施是使用高级语言和自文档技术,以减少变更引起的错误。采用编译时的操作来整合标准声明,在很大程度上帮助了变化的调整。
变更的阶段化是一种必要的技术。每个产品都应该有数字版本号,每个版本都应该有自己的日程表和冻结日期,在此之后的变更属于下一个版本的范畴。
为变更计划组织架构
Cosgrove主张把所有计划、里程碑、日程安排都当作是尝试性的,以方便进行变化。这似乎有些走极端–现在软件编程小组失败的主要原因是管理控制得太少,而不是太多。
不过,他提出了一种卓越的见解。他观察到不愿意为设计书写文档的原因,不仅仅是由于惰性或者时间压力。相反,设计人员通常不愿意提交尝试性的设计决策,再为它们进行辩解。”通过设计文档化,设计人员将自己暴露在每个人的批评之下,他必须能够为他的每个结果进行辩护。如果团队架构因此受到任何形式的威胁,则没有任何东西会被文档化,除非架构是完全受到保护的。
为变更组建团队比为变更进行设计更加困难。每个人被分派的工作必须是多样的、富有拓展性的工作,从技术角度而言,整个团队可以灵活地安排。在大型的项目中,项目经理需要有两个和三个顶级程序员作为技术轻骑兵,当工作繁忙最密集的时候,他们能急驰飞奔,解决各种问题。
当系统发生变化时,管理结构也需要进行调整。这意味着,只要管理人员和技术人才的天赋允许,老板必须对他们的能力培养给予极大的关注,使管理人员和技术人才具有互换性。
程序维护中的一个基本问题是–缺陷修复总会以(20-50)%的机率引入新的bug。所以整个过程是前进两步,后退一步。
为什么缺陷不能更彻底地被修复?首先,看上去很轻微的错误,似乎仅仅是局部操作上的失败,实际上却是系统级别的问题,通常这不是很明显。修复局部问题的工作量很清晰,并且往往不大。但是,更大范围的修复工作常常会被忽视,除非软件结构很简单,或者文档书写得非常详细。其次,维护人员常常不是编写代码的开发人员,而是一些初级程序员或者新手。
显然,使用能消除、至少是能指明副作用的程序设计方法,会在维护成本上有很大的回报。同样,设计实现的人员越少、接口越少,产生的错误也就越少。
Lehman和Belady研究了大型操作系统的一系列发布版本的历史。他们发现模块数量随版本号的增加呈线性增长,但是受到影响的模块以版本号指数的级别增长。所有修改都倾向于破坏系统的架构,增加了系统的混乱程度。用在修复原有设计上瑕疵的工作量越来越少,而早期维护活动本身的漏洞所引起修复工作越来越多。随着时间的推移,系统变得越来越无序,修复工作迟早会失去根基。每一步前进都伴随着一步后退。尽管理论上系统一直可用,但实际上,整个系统已经面目全非,无法再成为下一步进展的基础。而且,机器在变化,配置在变化,用户的需求在变化,所以现实系统不可能永远可用。崭新的、对于原有系统的重新设计是完全必要的。
系统软件开发是减少混乱度(减少熵)的过程,所以它本身是处于亚稳态的。软件维护是提高混乱度(增加熵)的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程
。
工欲善其事,必先利其器
项目的关键问题是沟通,个性化的工具妨碍–而不是促进沟通。
细致的功能定义、详细的规格说明、规范化的功能描述说明以及这些方法的实施,大大减少了系统中必须查找的bug数量
自顶向下的设计
好的自顶向下设计从几个方面避免了bug。首先,清晰的结构和表达方式更容易对需求和模块功能进行精确的描述。其次,模块分割和模块独立性避免了系统级的bug。另外,细节的隐藏使结构上的缺陷更加容易识别。第四,设计在每个精化步骤的层次上是可以测试的,所以测试可以尽早开始,并且每个步骤的重点可以放在合适的级别上。
结构化编程
另外一系列减少bug数量的新方法很大程度上来自Dijkstra。Bohm和Jacopini的为其提供了理论证明。
基本上,该方法所设计程序的控制结构,仅包含语句形式的循环结构,例如DO WHILE,以及IF…THEN…ELSE的条件判断结构,而具体的条件部分在IF…THEN…ELSE后的花括号中描述。Bohm和Jacopini展示了这些结构在理论上是可以证明的。而Dijkstra认为另外一种方法,即通过GO TO不加限制的分支跳转,会产生导致自身逻辑错误的结构。
这种方法的基本理念非常优秀,但仍有人提出了一些反面的意见。一些附加的控制结构非常有效,例如,在多个条件下的多路分支(CASE、SWITCH语句),异常跳转等(GO TO ABNORMAL END)。此外,关于完全避免GO TO语句的说法显得有些教条主义,而且似乎有些吹毛求疵。
关键的地方和构建无bug程序的核心,是把系统的结构作为控制结构来考虑,而不是独立的跳转语句。这种思考方法是我们在程序设计发展史上向前迈出的一大步
。
测试的重要性。
一次添加一个构件
。这样做的好处同样是显而易见的,但是乐观主义和惰性常常诱使我们破坏这个规则。
项目进度经常以一种难以察觉,但是残酷无情的方式慢慢落后。
如何根据一个严格的进度表来控制项目?第一个步骤是制订进度表。进度表上的每一件事,被称为"里程碑"
,它们都有一个日期。
里程碑的选择只有一个原则,那就是,里程碑必须是具体的、特定的、可度量的事件,能够进行清晰定义。
文档的重要性。
流程图是被吹捧得最过分的一种程序文档。
逐一记录的详细流程图过时而且令人生厌,它只适合启蒙初学者的算法思维。
自文档化(self-documenting)的程序
数据处理的基本原理告诉我们,试图把信息放在不同的文件中,并努力维持它们之间的同步,是一种非常费力不讨好的事情。更合理的方法是:每个数据项包含两个文件都需要的所有信息,采用指定的键值来区别,并把它们组合到一个文件中。
把文档整合到源代码。这对正确维护是直接有力的推动,保证编程用户能方便、即时地得到文档资料。这种程序被称为自文档化(self-documenting)。
没有任何技术或管理上的进展,能够独立地许诺十年内使生产率、可靠性或简洁性获得数量级上的进步。
所有软件活动包括根本任务--打造由抽象软件实体构成的复杂概念结构,次要任务--使用编程语言表达这些抽象实体,在空间和时间限制内将它们映射成机器语言
。软件生产率在近年内取得的巨大进步来自对后天障碍的突破,例如硬件的限制、笨拙的编程语言、机器时间的缺乏等等。这些障碍使次要任务实施起来异常艰难,相对必要任务而言,软件工程师在次要任务上花费了多少时间和精力?除非它占了所有工作的9/10,否则即使全部次要任务的时间缩减到零,也不会给生产率带来数量级上的提高。