1.1 编程系统产品(Programming Systems Product)开发的工作量是供个人使用的、独立开发的构件程序的九倍。
我估计软件构件产品化引起了3倍工作量,将软件构件整合成完整系统所需要的设计、集成和测试又强加了3倍的工作量,这些高成本的构件在根本上是相互独立的。
1.2 编程为什么会有趣?作为回报,它的从业者期望得到什么样的快乐?
首先,这种快乐是一种创建事物的纯粹快乐。
其次,这种快乐来自于开发对他人有用的东西。
第三,快乐来自于整个过程中体现出的一股强大的魅力。
第四,这种快乐是持续学习的快乐,它来自这项工作的非重复特性。
最后,这种快乐还来自于在易于驾驭的介质上工作。
1.3 职业的苦恼
首先,苦恼来自追求完美,这是学习编程的最困难部分
其次,苦恼来自由他人来设定目标、供给资源和提供信息。
下一个苦恼——概念性设计是有趣的,但寻找琐碎的bug却只是一项重复性的活动。
最后一个苦恼,人们通常期望项目在接近结束时,(bug、工作时间)能收敛得快一些,然而软件项目的情况却是越接近完成,收敛得越慢,而产品在即将完成时总面临着陈旧过时的威胁。
这,就是编程。一个许多人痛苦挣扎的焦油坑以及一种乐趣和苦恼共存的创造性活动。
缺乏合理的时间进度是造成项目滞后的最主要原因。
导致这种普遍性灾难的原因是什么呢?
首先,我们对估算技术缺乏有效的研究,我们总是基于一种不真实的假设——一切都将运作良好。
第二,我们采用的估算技术隐含地假设人和月可以互换,错误地将进度与工作量相互混淆。
第三,由于对自己的估算缺乏信心,软件经理通常不会有耐心持续地进行估算这项工作。
第四,对进度缺少跟踪和监督。其他工程领域中,经过验证的跟踪技术和常规监督程序,在软件工程中常常被认为是无谓的举动。
第五,当意识到进度的偏移时,下意识(以及传统)的反应是增加人力。
良好的烹饪需要时间,某些任务无法在不损害结果的情况下加快速度。
2.1 所有的编程人员都是乐观主义者:“一切都将运作良好”。所以系统编程的进度安排背后的第一个假设是:一切都将运作良好,每一项任务仅花费它所“应该”花费的时间。
计算机编程基于十分容易掌握的介质,编程人员通过非常纯粹的思维活动——概念以及灵活的表现形式来开发程序。正由于介质的易于驾驭,我们期待在实现过程中不会碰到困难,因此造成了乐观主义的弥漫。而我们的构思是有缺陷的,因此总会有bug。
2.2 我们围绕成本核算的估计技术,混淆了工作量和项目进展。用人月作为衡量一项工作的规模是一个危险和带有欺骗性的神话。它暗示着人员数量和时间是可以相互替换的。
人数和时间的互换仅仅适用于以下情况:某个任务可以分解给参与人员,并且他们之间不需要相互的交流。对于可以分解,但子任务之间需要相互沟通和交流的任务,必须在计划工作中考虑沟通的工作量。沟通所增加的负担由两个部分组成,培训和相互的交流。每个成员需要进行技术、项目目标以及总体策略上的培训。这种培训不能分解,因此这部分增加的工作量随人员的数量呈线性变化。所增加的用于沟通的工作量可能会完全抵消对原有任务分解所产生的作用。
从而,添加更多的人手,实际上是延长了,而不是缩短了时间进度。
2.3 关于进度安排,我的经验是为1/3计划、1/6编码、1/4构件测试以及1/4系统测试。
在许多重要的方面,它与传统的进度安排方法不同:
通过对传统项目进度安排的研究,我发现很少项目允许为测试分配一半的时间,但大多数项目的测试实际上是花费了进度中一半的时间。它们中的许多项目,在系统测试之前还能保持进度。或者说,除了系统测试,进度基本能保证。
2.4 作为一个学科,我们缺乏数据估计。因为我们对自己的估计技术不确定,所以在管理和客户的压力下,我们常常缺乏坚持的勇气。非阶段化方法的采用,少得可怜的数据支持,加上完全借助软件经理的直觉,这样的方式很难生产出健壮可靠和规避风险的估计。
2.5 Brook法则:向进度落后的项目中增加人手,只会使进度更加落后。(Adding manpower to a late software project makes it later)
向软件项目中增派人手从三个方面增加了项目必要的总体工作量:任务重新分配本身和所造成的工作中断;培训新人员;额外的相互沟通。
3.1 同样有两年经验而且在受到同样的培训的情况下,优秀的专业程序员的工作效率是较差程序员的十倍。(Sackman、Erikson和Grand)
需要协作沟通的人员的数量影响着开发成本,因为成本的主要组成部分是相互的沟通和交流,以及更正沟通不当所引起的不良结果(系统调试)。这一点,也暗示系统应该由尽可能少的人员来开发。小型、精干队伍是最好的——尽可能的少。
3.2 Mills建议大型项目的每一个部分由一个团队解决,但是该队伍以类似外科手术的方式组建,而并非一拥而上。即由一个人来进行问题的分解,其他人给予他所需要的支持,以提高效率和生产力。
Mills概念的真正关键是“从个人艺术到公共实践”的编程观念转换。它向所有的团队成员展现了所有计算机的运作和产物,并将所有的程序和数据看作是团队的所有物,而非私人财产。
这样的队伍应该包含:
外科医生。Mills称之为首席程序员。他亲自定义功能和性能技术说明书,设计程序,编制源代码,测试以及书写技术文档。首席程序员需要极高的天分、十年的经验和应用数学、业务数据处理或其他方面的大量系统和应用知识。
副手。他是外科医生的后备,能完成任何一部分工作,但是相对具有较少的经验。他的主要作用是作为设计的思考者、讨论者和评估人员。外科医生试图和他沟通设计,但不受到他建议的限制。
管理员。外科医生是老板,他必须在人员、加薪等方面具有决定权,但他决不能在这些事务上浪费任何时间。因而,他需要一个控制财务、人员、工作地点安排和机器的专业管理人员,该管理员充当与组织中其他管理机构的接口。
编辑。编辑根据外科医生的草稿或者口述的手稿,进行分析和重新组织,提供各种参考信息和书目,对多个版本进行维护以及监督文档生成的机制。
两个秘书。管理员和编辑每个人需要一个秘书。管理员的秘书负责项目的协作一致和非产品文件。
程序职员。他负责维护编程产品库中所有团队的技术记录。该职员接受秘书性质的培训,承担机器码文件和可读文件的相关管理责任。所有的计算机输入汇集到这个职员处。如果需要,他会对它们进行记录或者标识。
工具维护人员。保证所有基本服务的可靠性,以及承担团队成员所需要的特殊工具(特别是交互式计算机服务)的构建、维护和升级责任。
测试人员。因此,测试人员既是为外科医生的各个功能设计系统测试用例的对头,同时也是为他的日常调试设计测试数据的助手。他还负责计划测试的步骤和为测试搭建测试平台。
语言专家。语言专家寻找一种简洁、有效的使用语言的方法来解决复杂、晦涩或者棘手的问题。他通常需要对技术进行一些研究(两到三天)。通常一个语言专家可以为两个到三个外科医生服务。
3.3 传统的两人队伍与外科医生——副手队伍架构之间的区别:
首先,传统的团队将工作进行划分,每人负责一部分工作的设计和实现。在外科手术团队中,外科医生和副手都了解所有的设计和全部的代码。
第二,在传统的队伍中大家是平等的,出现观点的差异时,不可避免地需要讨论和进行相互的妥协和让步。而在外科手术团队中,不存在利益的差别,观点的不一致由外科医生单方面来统一。
另外,团队中剩余人员职能的专业化分工是高效的关键,它使成员之间采用非常简单的交流模式成为可能。
3.4 当我们需要面对几百人参与的大型任务时,如何应用外科手术团队的概念呢?
扩建过程的成功依赖于这样一个事实,即每个部分的概念完整性得到了彻底的提高——决定设计的人员是原来的七分之一或更少。
所以,可以让200人去解决问题,而仅仅需要协调20个人,即那些“外科医生”的思路。
4.1 在系统设计中,概念完整性应该是最重要的考虑因素。也就是说,为了反映一系列连贯的设计思路,宁可省略一些不规则的特性和改进,也不提倡独立和无法整合的系统,哪怕它们其实包含着许多很好的设计。
4.2 编程系统(软件)的目的是使计算机更加容易使用。
为了做到这一点,计算机装备了语言和各种工具,只有当这些功能说明节约下来的时间,比用在学习、记忆和搜索手册上的时间要少时,易用性才会得到提高。由于目标是易用性,功能与理解上复杂程度的比值才是系统设计的最终测试标准。单是功能本身或者易于使用都无法成为一个好的设计评判标准。
4.3 概念的完整性要求设计必须由一个人,或者非常少数互有默契的人员来实现。而进度压力却要求很多人员来开发系统。
有两种方法可以解决这种矛盾。第一种是仔细地区分设计方法和具体实现。第二种是前一章节中所讨论的、一种崭新的组建编程开发团队的方法。
对于非常大型的项目,将设计方法、体系结构方面的工作与具体实现相分离是获得概念完整性的强有力方法。
系统的体系结构(architecture)指的是完整和详细的用户接口说明。
对于计算机,它是编程手册;对于编译器,它是语言手册;对于控制程序,它是语言和函数调用手册;对于整个系统,它是用户要完成自己全部工作所需参考的手册的集合。体系结构同实现必须仔细地区分开来。
难道不能遵循民主的理论,从所有的员工中搜集好的创意,以得到更好的产品,而不是将技术说明工作仅限定于少数人?
就必须只能存在少数的结构师而言,答案是肯定的,他们的工作产物的生命周期比那些实现人员的产物要长,并且结构师一直处在解决用户问题,实现用户利益的核心地位。如果要得到系统概念上的完整性,那么必须控制这些概念。这实际上是一种无需任何歉意的贵族专制统治。
纪律、规则对行业是有益的。外部的体系结构规定实际上是增强,而不是限制实现小组的创造性。概念上统一的系统能更快地开发和测试。
4.4 如同Blaauw所指出的,整个创造性活动包括了三个独立的阶段:体系结构(architecture)、设计实现(implementation)、物理实现(realization)。在实际情况中,它们往往可以同时开始和并发地进行。在编程系统的开发中,这个原理同样适用。
概念的完整性的确要求系统只反映唯一的设计理念,用户所见的技术说明来自少数人的思想。实际工作被划分成体系结构、设计实现和物理实现,但这并不意味着该开发模式下的系统需要更长的时间来创建。经验显示恰恰相反,整个系统将会开发得更快,所需要的测试时间将更少。
同工作的水平分割相比,垂直划分从根本上大大减少了劳动量,结果是使交流彻底地简化,概念完整性得到大幅提高。
5.1 尽早交流和持续沟通能使结构师有较好的成本意识,以及使开发人员获得对设计的信心,并且不会混淆各自的责任分工。
面对估算过高的难题,结构师有两个选择:削减设计或者建议成本更低的实现方法——挑战估算的结果。后者是固有的主观感性反应。此时,结构师是在向开发人员的做事方式提出挑战。想要成功,结构师必须:
牢记是开发人员承担创造性和发明性的实现责任,所以结构师只能建议,而不能支配;
时刻准备着为所指定的说明建议一种实现的方法,同样准备接受其他任何能达到目标的方法;
对上述的建议保持低调和平静;
准备放弃坚持所作的改进建议;
5.2 在开发第一个系统时,结构师倾向于精炼和简洁。他知道自己对正在进行的任务不够了解,所以他会谨慎仔细地工作。
第二个系统是设计师们所设计的最危险的系统。一种普遍倾向是过分地设计第二个系统,向系统添加很多修饰功能和想法,它们曾在第一个系统中被小心谨慎地推迟了。
当设计师着手第三个或第四个系统时,先前的经验会相互验证,得到此类系统通用特性的判断,而且系统之间的差异会帮助他识别出经验中不够通用的部分。
项目经理如何避免画蛇添足(second-system effect)?
他必须坚持至少拥有两个系统以上开发经验结构师的决定。
6.1 手册、或者书面规格说明,是一个非常必要的工具,尽管光有文档是不够的。手册是产品的外部规格说明,它描述和规定了用户所见的每一个细节;同样的,它也是结构师主要的工作产物。规格说明的风格必须清晰、完整和准确。规格说明作者应该追求的精确程度:在仔细定义规定什么的同时,定义未规定什么。
6.2 手册的作者必须注意自己的思路和语言,达到所需要的精确程度。一种颇具吸引力的作法是对上述定义使用形式化标记方法。形式化定义是精确的,它们倾向于更加完整;差异得更加明显,可以更快地完成。但是形式化定义的缺点是不易理解。
在表达的精确和简明性上,目前所提出的形式化定义,具有了令人惊异的效果,增强了我们进行准确表达的信心。但是,它还需要记叙性文字的辅助,才能使内容易于领会和讲授。如果同时具有两种方式,则必须以一种作为标准,另一种作为辅助描述,并照此明确地进行划分。
最后,关于实际使用标准是形式化描述还是叙述性文字这一点而言,使用实现作为形式化定义特别容易引起混淆,特别是在程序化的仿真中。另外,当实现充当标准时,还必须防止对实现的任何修改。
6.3 对软件系统的体系结构师而言,存在一种更加可爱的方法来分发和强制定义。对于建立模块间接口语法,而非语义时,它特别有用。这项技术是设计被传递参数和共享存储器的声明,并要求编程实现在编译时的一些操作(PL/I的宏或#include)来包含这些声明。
6.4 我们把会议分成两个级别:周例会和年度大会。
周例会是每周半天的会议,由所有的结构师,加上硬件和软件实现人员代表和市场计划人员参与,由首席系统结构师主持。
会议中,任何人可以提出问题和修改意见,但是建议书通常是以书面形式,在会议之前分发。新问题通常会被讨论一些时间。重点是创新,而不仅仅是结论。周例会的决策会给出迅捷的结论,允许工作继续进行。
这种会议的卓有成效是由于:
随着时间的推移,一些决定没有很好地贯彻,一些小事情并没有被某个参与者真正地接受,其他决定造成了未曾遇到的问题。对于这些问题,有时周例会没有重新考虑,慢慢地,很多小要求、公开问题或者不愉快会堆积起来。为解决这些堆积起来的问题,我们会举行年度大会,典型的年度大会会持续两周。
年度大会的出席人员不仅仅包括体系结构小组和编程人员、实现人员的结构代表,同时包括编程经理、市场和实现人员,由System/360的项目经理主持。
议程典型地包括大约200个条目,每个不同的声音都有机会得到表达。然后,会制订出决策。每天早晨,会议参与人员会在座位上发现更新了的手册说明,记录了前一天的各项决定。
这些“收获的节日”不仅可以解决决策上的问题,而且使决策更容易被接受。每个人都在倾听,每个人都在参与,每个人对复杂约束和决策之间的相互关系有了更透彻的理解。
6.5 在大多数计算机项目中,机器和手册之间往往会在某一天出现不一致,人们通常会忽略手册。因为与机器相比,手册更容易改动,并且成本更低。
然而,当存在多重实现时,情况就不是这样。这时,如实地遵从手册更新机器所造成的延迟和成本的消耗,比根据机器调整手册要低。
6.6 一种有用的机制是由结构师保存电话日志。
日志中,他记录了每一个问题和相应的回答。每周,对若干结构师的日志进行合并,重新整理,并发布给用户和实现人员。这种机制很不正式,但非常快捷和易于理解。
6.7 项目经理最好的朋友就是他每天要面对的敌人——独立的产品测试机构/小组。
该小组根据规格说明检查机器和程序,充当麻烦的代言人,查明每一个可能的缺陷和相互矛盾的地方。每个开发机构都需要这样一个独立的技术监督部门,来保证其公正性。
在最后的分析中,用户是独立的监督人员。产品——测试小组则是顾客的代理人,专门寻找缺陷。
7.1 巴比伦塔项目的失败是因为缺乏交流,以及交流的结果——组织。他们无法相互交谈,从而无法合作。当合作无法进行时,工作陷入了停顿。
7.2 因为左手不知道右手在做什么,所以进度灾难、功能的不合理和系统缺陷纷纷出现。随着工作的进行,许多小组慢慢地修改自己程序的功能、规模和速度,他们明确或者隐含地更改了一些有效输入和输出结果用法上的约定。由于对其他人的各种假设,团队成员之间的理解开始出现偏差。
团队应该以尽可能多的方式进行相互之间的交流:非正式、常规项目会议,会上进行简要的技术陈述、共享的正式项目工作手册以及电子邮件。
7.3 项目工作手册:
是什么。项目工作手册不是独立的一篇文档,它是对项目必须产出的一系列文档进行组织的一种结构。项目所有的文档都必须是该结构的一部分。这包括目的、外部规格说明、接口说明、技术标准、内部说明和管理备忘录。
为什么。技术说明几乎是必不可少的。如果某人就硬件和软件的某部分,去查看一系列相关的用户手册。他发现的不仅仅是思路,而且还有能追溯到最早备忘录的许多文字和章节,这些备忘录对产品提出建议或者解释设计。对于技术作者而言,文章的剪裁粘贴与钢笔一样有用。使用项目手册的第二个原因是控制信息发布。控制信息发布并不是为了限制信息,而是确保信息能到达所有需要它的人的手中。
项目手册的第一步是对所有的备忘录编号,从而每个工作人员可以通过标题列表来检索是否有他所需要的信息。还有一种更好的组织方法,就是使用树状的索引结构。
处理机制。同许多其它的软件管理问题一样,随着项目规模的扩大,技术备忘录的问题以非线性趋势增长。工作手册的实时更新是非常关键的。工作手册的使用者应该将注意力集中在上次阅读后的变更,以及关于这些变更重要性的评述。
现在如何入手?在当今很多可以应用的技术中,我认为一种选择是采用可以直接访问的文件。在文件中,记录修订日期记录和标记变更标识条。每个用户可以从一个显示终端(打印机太慢了)来查阅。
7.4 团队组织的目的是减少不必要交流和合作的数量,因此良好的团队组织是解决上述交流问题的关键措施。
减少交流的方法是人力划分(division of labor)和限定职责范围(specialization of function)。当使用人力划分和职责限定时,树状管理结构所映出对详细交流的需要会相应减少。事实上,树状组织架构是作为权力和责任的结构出现。
树状编程队伍的每棵子树所必须具备的基本要素:
产品负责人的角色是什么?
他组建团队,划分工作及制订进度表。他要求,并一直要求必要的资源。这意味着他主要的工作是与团队外部,向上和水平地沟通。他建立团队内部的沟通和报告方式。最后,他确保进度目标的实现,根据环境的变化调整资源和团队的构架。
技术主管的角色是什么?
他对设计进行构思,识别系统的子部分,指明从外部看上去的样子,勾画它的内部结构。他提供整个设计的一致性和概念完整性;他控制系统的复杂程度。当某个技术问题出现时,他提供问题的解决方案,或者根据需要调整系统设计。
存在三种可能的关系,它们都在实践中得到了成功的应用:
产品负责人和技术主管是同一个人。这种方式非常容易应用在很小型的队伍中,可能是三个或六个开发人员。在大型的项目中则不容易得到应用。原因有两个:第一,同时具有管理技能和技术技能的人很难找到。思考者很少,实干家更少,思考者-实干家太少了。第二,大型项目中,每个角色都必须全职工作,甚至还要加班。对负责人来说,很难在承担全部管理责任的同时,还能抽出时间进行技术工作。对技术主管来说,很难在保证设计的概念完整性,没有任何妥协的前提下,担任管理工作。
产品负责人作为总指挥,技术主管充当其左右手。这种方法有一些困难。很难在技术主管不参与任何管理工作的同时,建立在技术决策上的权威。显然,产品负责人必须预先声明技术主管的技术权威,在即将出现的绝大部分测试用例中,他必须支持后者的技术决定。要达到这一点,产品责任人和技术主管必须在基本的技术理论上具有相似观点;他们必须在主要的技术问题出现之前,私下讨论它们;产品责任人必须对技术主管的技术才能表现出尊重。
技术主管作为总指挥,产品负责人充当其左右手。我猜测最后一种安排对小型的团队是最好的选择,如同在第3章《外科手术队伍》一文中所述。对于真正大型项目中的一些开发队伍,我认为产品负责人作为管理者是更合适的安排。
系统编程需要花费多长的时间?需要多少的工作量?如何进行估计?
编码大约只占了问题的六分之一左右,编码估计或者比率的错误可能会导致不合理的荒谬结果。仅仅通过对编码部分的估计,然后乘以任务其他部分的相对系数,是无法得出对整项工作的精确估计的。必须声明的是,构建独立小型程序的数据不适用于编程系统产品。
8.1 Charles Portman发现他的编程队伍落后进度大约1/2,每项工作花费的时间大约是估计的两倍。
日志显示事实上他的团队仅用了百分之五十的工作周,来进行实际的编程和调试,估算上的失误完全可以由该情况来解释。其余的时间包括机器的当机时间、高优先级的无关琐碎工作、会议、文字工作、公司业务、疾病、事假等等。简言之,项目估算对每个人年的技术工作时间数量做出了不现实的假设。
8.2 Joel Aron对程序员的生产率进行了研究。他根据程序员(和系统部分)之间的交互划分这些系统,得到了如下的生产率:
该人年数据未包括支持和系统测试活动,仅仅是设计和编程。
8.3 John Harr,汇报了他和其他人的经验。下图是数据最详细和最有用的。头两个任务是基本的控制程序,后两个是基本的语言翻译。生产率以经调试的指令/人年来表达。它包括了编程、构件测试和系统测试。没有包括计划、硬件机器支持、文书工作等类似活动的工作量。
4个NO.1的ESS编程工作总结:
ESS预计和实际编程速度:
ESS预计和实际调试速度:
生产率同样地被划分为两个类别,控制程序的生产率大约是600指令每人年,语言翻译大约是2200指令每人年。注意所有的四个程序都具有类似的规模——差异在于工作组的大小、时间的长短和模块的个数。控制程序确实更加复杂。除开这些不确定性,数据反映了实际的生产率——描述了在现在的编程技术下,大型系统开发的状况。
8.4 IBM OS/360的经验,还是证实上面那些结论。
就控制程序组的经验而言,生产率的范围大约是600~800(经过调试的指令)/人年。语言翻译小组所达到的生产率是2000~3000(经过调试的指令)/人年。这包括了小组的计划、代码构件测试、系统测试和一些支持性活动。就我的观点来说,它们同Harr的数据是可比的。
生产率会根据任务本身复杂度和困难程度表现出显著差异。在复杂程度估计这片“沼泽”上的指导原则是:编译器的复杂度是批处理程序的三倍,操作系统复杂度是编译器的三倍。
8.5 Corbato的MIT项目MAC报告表示在MULTICS系统上,平均生产率是1200行经调试的PL/I语句(大约在1和2百万指令之间)/人年。MULTICS包括了控制程序和语言翻译程序。但Corbato的数字是行/人年,不是指令!系统中的每个语句对应于手写代码的3至5个指令!
这意味着两个重要的结论:
对常用编程语句而言。生产率似乎是固定的。这个固定的生产率包括了编程中需要注释,并可能存在错误的情况;
使用适当的高级语言,编程的生产率可以提高5倍。
9.1 程序有多大?除了运行时间以外,它所占据的空间也是主要开销。
当系统设计者认为对用户而言,常驻程序内存的形式比加法器、磁盘等更加有用时,他会将硬件实现中的一部分移到内存上。相反的,其他的做法是非常不负责任的。
由于规模是软件系统产品用户成本中如此大的一个组成部分,开发人员必须设置规模的目标,控制规模,考虑减小规模的方法,就像硬件开发人员会设立元器件数量目标,控制元器件的数量,想出一些减少零件的方法。同任何开销一样,规模本身不是坏事,但不必要的规模是不可取的。
9.2 对项目经理而言,规模控制既是技术工作的一部分,也是管理工作的一部分。
他必须研究用户和他们的应用,以设置将开发系统的规模。接着,把这些系统划分成若干部分,并设定每个部分的规模目标。
首先,仅对核心程序设定规模目标是不够的,必须把所有的方面都编入预算。
在为每个单元设立核心规模的同时,我们没有同时设置访问的目标。
项目规模本身很大,缺乏管理和沟通,以至于每个团队成员认为自己是争取小红花的学生,而不是构建系统软件产品的人员。为了满足目标,每个人都在局部优化自己的程序,很少会有人停下来,考虑一下对客户的整体影响。
9.3 空间预算的多少和控制并不能使程序规模减小,为实现这一目标,它还需要一些创造性和技能。
其中的一个技巧是用功能交换尺寸:
在内存大小一定的情况下进行系统设计时,会出现另外一个基本问题。内存受限的后果是即使最小的功能模块,它的适用范围也难以得到推广。临时空间的尺寸,以及每次磁盘访问的程序数量是很关键的决策,因为性能是规模的非线性函数。
第二个技能是考虑空间——时间的折衷:
对于给定的功能,空间越多,速度越快。这一点在很大的范围内都适用。也正是这一点使空间预算成为可能。
项目经理可以做两件事来帮助他的团队取得良好的空间——时间折衷。一是确保他们在编程技能上得到培训,而不仅仅是依赖他们自己掌握的知识和先前的经验。另外一种方法是认识到编程需要技术积累,需要开发很多公共单元构件库。库中的每个组件需要有两个版本,运行速度较快和短小精炼的。
9.4 精炼、充分和快速的程序,往往是战略性突破的结果,而不仅仅技巧上的提高。这种突破常常是一种新型算法。
更普遍的是,战略上突破常来自于数据或表的重新表达。实际上,数据的表现形式是编程的根本。
在一片文件的汪洋中,少数文档形成了关键的枢纽,每件项目管理的工作都围绕着它们运转。它们是经理们的主要个人工具。
10.1 计算机产品的文档
目标:
定义待满足的目标和需要,定义迫切需要的资源、约束和优先级。
技术说明:
计算机手册和性能规格说明。它是在计划新产品时第一个产生,并且最后完成的文档。
进度、时间表:
预算:预算不仅仅是约束。对管理人员来说,它还是最有用的文档之一。预算的存在会迫使技术决策的制订,否则,技术决策很容易被忽略。更重要的是,它促使和澄清了策略上的一些决定。
组织机构图:
报价、预测、价格:这三个因素互相牵制,决定了项目的成败。
10.2 大学科系的文档
目标
课程描述
学位要求
研究报告(申请基金时,还要求计划)
课程表和课程的安排
预算
教室分配
教师和研究生助手的分配
10.3 软件项目的文档
做什么:目标。定义了待完成的目标、迫切需要的资源、约束和优先级。
做什么:产品技术说明。以建议书开始,以用户手册和内部文档结束。速度和空间说明是关键的部分。
时间:进度表
资金:预算
地点:工作空间分配
人员:组织图。它与接口说明是相互依存的,如果系统设计能自由地变化,则项目组织架构必须为变化做准备。
10.4 为什么要有正式的文档?
首先,书面记录决策是必要的。
第二,文档能够作为同其他人的沟通渠道。
最后,项目经理的文档可以作为数据基础和检查列表。
只有一小部分管理人员的时间——可能只有20%——用来从自己头脑外部获取信息。其他的工作是沟通:倾听、报告、讲授、规劝、讨论、鼓励。不过,对于基于数据的部分,少数关键的文档是至关重要的,它们可以满足绝大多数需要。
项目经理的任务是制订计划,并根据计划实现。但是只有书面计划是精确和可以沟通的。计划中包括了时间、地点、人物、做什么、资金。这些少量的关键文档封装了一些项目经理的工作。
不变只是愿望,变化才是永恒。——SWIFT。
普遍的做法是,选择一种方法,试试看;如果失败了,没关系,再试试别的。不管怎么样,重要的是先去尝试。——富兰克林 D. 罗斯福。
11.1 化学工程师很早就认识到,在实验室可以进行的反应过程,并不能在工厂中一步实现。一个被称为“实验性工厂(pilot planet)”的中间步骤是非常必要的。软件系统的构建人员也面临类似的问题。
对于大多数项目,第一个开发的系统并不合用。要解决所有的问题,除了重新开始以外,没有其他的办法——即开发一个更灵巧或者更好的系统。系统的丢弃和重新设计可以一步完成,也可以一块块地实现。所有大型系统的经验都显示,这是必须完成的步骤。
因此,管理上的问题不再是“是否构建一个试验性的系统,然后抛弃它?”。现在的问题是“是否预先计划抛弃原型的开发,或者是否将该原型发布给用户?”。将原型发布给用户,可以获得时间,但是它的代价高昂——对于用户,使用极度痛苦;对于重新开发的人员,分散了精力;对于产品,影响了声誉,即使最好的再设计也难以挽回名声。因此,为舍弃而计划,无论如何,你一定要这样做。
11.2 一旦认识到试验性的系统必须被构建和丢弃,具有变更思想的重新设计不可避免,从而直面整个变化现象是非常有用的。
我从不建议顾客目标和需求的所有变更必须、能够、或者应该整合到设计中。项目开始时建立的基准,肯定会随着开发的进行越来越高,甚至开发不出任何产品。抛弃原型概念本身就是对事实的接受——随着学习的过程更改设计。
11.3 如何为上述变化设计系统,是个非常著名的问题。它们包括细致的模块化、可扩展的函数、精确完整的模块间接口设计、完备的文档。另外,还可能会采用包括调用队列和表驱动的一些技术。
最重要的措施是使用高级语言和自文档技术,以减少变更引起的错误。采用编译时的操作来整合标准声明,在很大程度上帮助了变化的调整。
变更的阶段化是一种必要的技术。每个产品都应该有数字版本号,每个版本都应该有自己的日程表和冻结日期,在此之后的变更属于下一个版本的范畴。
程序员不愿意为设计书写文档的原因,不仅仅是由于惰性。更多的是源于设计人员的踌躇——要为自己尝试性的设计决策进行辩解。
11.4 当系统发生变化时,管理结构也需要进行调整。这意味着,只要管理人员和技术人才的天赋允许,老板必须对他们的能力培养给予极大的关注,使管理人员和技术人才具有互换性。
这其中的障碍是社会性的,人们必须同顽固的戒心做斗争。首先,管理人员自己常常认为高级人员太“有价值”,而舍不得让他们从事实际的编程工作;其次,管理人员拥有更高的威信。为了克服这个问题,如Bell Labs的一些实验室,废除了所有的职位头衔。每个专业人士都是“技术人员中的一员”。而IBM的另外一些实验室,保持了两条职位晋升线,如图11.1所示。
管理人员需要参与技术课程,高级技术人才需要进行管理培训。项目目标、进展、管理问题必须在高级人员整体中得到共享。只要能力允许,高层人员必须时刻做好技术和情感上的准备,以管理团队或者亲自参与开发工作。这是件工作量很大的任务,但显然很值得!
组建外科手术队伍式的软件开发团队,这整个观念是对上述问题的彻底冲击。其结果是当高级人才编程和开发时,不会感到自降身份。这种方法试图清除那些会剥夺创造性乐趣的社会障碍。
上述组织架构的设计是为了最小化成员间的接口。同样的,它使系统在最大程度上易于修改。当组织构架必须变化时,为整个“外科手术队伍”重新安排不同的软件开发任务,会变得相对容易一些。
11.5 在程序发布给顾客使用之后,它不会停止变化。发布后的变更被称为“程序维护”,但是软件的维护过程不同于硬件维护。
计算机系统的硬件维护包括了三项活动——替换损坏的器件、清洁和润滑、修改设计上的缺陷。
软件维护不包括清洁、润滑和对损坏器件的修复。它主要包含对设计缺陷的修复。对于一个广泛使用的程序,其维护总成本通常是开发成本的40%或更多。令人吃惊的是,该成本受用户数目的严重影响。用户越多,所发现的错误也越多。
程序维护中的一个基本问题是——缺陷修复总会以(20-50)%的机率引入新的bug。所以整个过程是前进两步,后退一步。
为什么缺陷不能更彻底地被修复?
首先,看上去很轻微的错误,似乎仅仅是局部操作上的失败,实际上却是系统级别的问题。修复局部问题的工作量很清晰,并且往往不大。但是,更大范围的修复工作常常会被忽视,除非软件结构很简单,或者文档书写得非常详细
其次,维护人员常常不是编写代码的开发人员,而是一些初级程序员或者新手。作为引入新bug的一个后果,程序每条语句的维护需要的系统测试比其他编程要多。理论上,在每次修复之后,必须重新运行先前所有的测试用例,从而确保系统不会以更隐蔽的方式被破坏,所以它的成本非常高。
11.6 Lehman和Belady研究了大型操作系统的一系列发布版本的历史。他们发现模块数量随版本号的增加呈线性增长,但是受到影响的模块以版本号指数的级别增长。所有修改都倾向于破坏系统的架构,增加了系统的混乱程度。用在修复原有设计上瑕疵的工作量越来越少,而早期维护活动本身的漏洞所引起修复工作越来越多。随着时间的推移,系统变得越来越无序,修复工作迟早会失去根基。每一步前进都伴随着一步后退。
实际上,整个系统已经面目全非,无法再成为下一步进展的基础。而且,机器在变化,配置在变化,用户的需求在变化,所以现实系统不可能永远可用。崭新的、基于原有系统的重新设计是完全必要的。
系统软件开发是减少混乱度(减少熵)的过程,所以它本身是处于亚稳态的。软件维护是提高混乱度(增加熵)的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程。
就工具而言,即使是现在,很多软件项目仍然像一家五金店。每个骨干人员都仔细地保管自己工作生涯中搜集的一套工具集,这些工具成为个人技能的直观证明。正是如此,每个编程人员也保留着编辑器、排序、内存信息转储、磁盘实用程序等工具。
这种方法对软件项目来说是愚蠢的:
首先,项目的关键问题是沟通,个性化的工具妨碍——而不是促进沟通。
其次,当机器和语言发生变化时,技术也会随之变化,所有工具的生命周期是很短的。毫无疑问,开发和维护公共的通用编程工具的效率更高。
不过,仅有通用工具是不够的。专业需要和个人偏好同样需要很多专业工具。所以在前面关于软件开发队伍的讨论中,我建议为每个团队配备一名工具管理人员。这个角色管理所有通用工具,能指导他的客户——老板使用工具。同时,他还能编制老板需要的专业工具。
项目经理必须考虑、计划、组织的工具到底有哪些呢?
首先是计算机设施。它需要硬件和使用安排策略;它需要操作系统,提供服务的方式必须明了;它需要语言,语言的使用方针必须明确;然后是实用程序、调试辅助程序、测试用例生成工具和处理文档的字处理系统。
12.1 机器支持可以有效地划分成目标机器和辅助机器。目标机器是软件所服务的对象,程序必须在该机器上进行最后测试。辅助机器是那些在开发系统中提供服务的机器。
目标机器的类型有哪些?
团队开发的监督程序或其他系统核心软件当然需要它们自己的机器。目标机器系统会需要若干操作员和一两个系统编程人员,以保证机器上的标准支持是即时更新和实时可用的。
另外,还需要配备调试机器或者软件。这样,在调试过程中,所有类型的程序参数可以被自动计数和测量。
计划安排:
目标机器时间需求具有特别的增长曲线。当目标机器刚刚被研制,或者当它的第一个操作系统被开发时,机器时间是非常匮乏的,时间的调度安排成了主要问题。
我们开始把机器时间分配成连续的块。例如,整个从事排序工作的15人小组,会得到系统4至6小时的使用时间块,由他们自己决定如何使用。即使没有安排,其他人也不能使用机器资源。这种方式,是一种更好的分配和安排方法。尽管机器的利用程度可能会有些降低(常常不是这样),生产率却提高了。
12.2 仿真装置。如果目标机器是新产品,则需要一个目标机器的逻辑仿真装置。这样,在生产出新机器之前,就有辅助的调试平台可供使用。即使在新机器出现之后,仿真装置仍然可以提供可靠的调试平台。
可靠并不等于精确。在某些方面,仿真机器肯定无法精确地达到与新型机器一致的实现。但是至少在一段时间内,它的实现是稳定的,新硬件就不会。
实验室研制和试制的模型产品和早期硬件不会像定义的那样运行,不会稳定工作,甚至每天都不会一样。。所以,一套运行在稳定平台上的可靠仿真装置,提供了远大于我们所期望的功用。
编译器和汇编平台。出于同样的原因,编译器和汇编软件需要运行在可靠的辅助平台上,为目标机器编译目标代码。接着,可以在仿真器上立刻开始后续的调试。
高级语言的编程开发中,在目标机器上开始全面测试目标代码之前,编译器可以在辅助机器上完成很多目标代码的调试和测试工作。
程序库和管理。在OS/360开发中,一个非常成功的重要辅助机器应用是维护程序库。
这个库实际上划分成不同访问规则下的子库。
首先,每个组或者编程人员分配了一个区域,用来存放他的程序拷贝、测试用例以及单元测试需要的测试辅助例程和数据。在这个开发库(playpen)中,不存在任何限制开发人员的规定。他可以自由处置自己的程序,他是它们的拥有者。
当开发人员准备将软件单元集成到更大的部分时,他向集成经理提交一份拷贝,后者将拷贝放置在系统集成子库中。此时,原作者不可以再改变代码,除非得到了集成经理的批准。当系统合并在一起时,集成经理开始进行所有的系统测试工作,识别和修补bug。
有时,系统的一个版本可能会被广泛应用,它被提升到当前版本子库。此时,这个拷贝是不可更改的,除非有重大缺陷。该版本可以用于所有新模块的集成和测试。
这有两个重要的理念。首先是受控,即程序的拷贝属于经理,他可以独立地授权程序的变更。其次是使发布的进展变得正式,以及开发库(playpen)与集成、发布的正式分离。
编程工具。随着调试技术的出现,旧方法的使用减少了,但并没有消失。因此,还是需要内存转储、源文件编辑、快照转储、甚至跟踪等工具。
文档系统。在所有的工具中,最能节省劳动力的,可能是运行在可靠平台上的、计算机化的文本编辑系统。
性能仿真装置。最好有一个。正如我们将在下章讨论到的,彻底地开发一个。使用相同的自顶向下设计方法,来实现性能仿真器、逻辑仿真装置和产品。
11.3 高级语言。使用高级语言的主要原因是生产率和调试速度。我们在前面已讨论过生产率的问题(第8章)。调试上的改进来自下列事实——存在更少的bug,而且更容易查找。
bug更少的原因,是因为它避免在错误面前暴露所有级别的工作,这样不但会造成语法上的错误,还会产生语义上的问题,如不当使用寄存器等。编译器的诊断机制可以帮助找出这些类似的错误,更重要的是,它非常容易插入调试的快照。
系统编程需要什么样的高级语言呢?
现在可供合理选择的语言是PL/I6。它提供完整的功能集;它与操作系统环境相吻合;它有各种各样的编译器,一些是交互式的,一些速度很快,一些诊断性很好,另一些能产生优化程度很高的代码。我自己觉得使用APL来解决算法更快一些,然后,将它们翻译成某个系统环境下的PL/I语言。
交互式编程。在那些系统编程所关注的方面,Multics(以及后续系统,IBM的TSS)和其他交互式计算机系统在概念上有很大的不同:多个级别上数据和程序的共享和保护,可延伸的库管理,以及协助终端用户共同开发的设施。
批处理和交互式编程生产率的对比:
Harr的数据表明了系统软件开发中,交互式编程的生产率至少是原来的两倍。
13.1 防范bug的定义。系统各个组成部分的开发者都会做出一些假设,而这些假设之间的不匹配,是大多数致命和难以察觉的bug的主要来源。
第4、5、6章所讨论的获取概念完整性的途径,就是直接面对这些问题。简言之,产品的概念完整性在使它易于使用的同时,也使开发更容易进行以及bug更不容易产生。
测试规格说明。在编写任何代码之前,规格说明必须提交给测试小组,以详细地检查说明的完整性和明确性。
自顶向下的设计。在1971年的一篇论文中,Niklaus Wirth将程序开发划分成体系结构设计、设计实现和物理编码实现,每个步骤可以使用自顶向下的方法很好地实现。
Wirth的流程将设计看成一系列精化步骤。开始是勾画出能得到主要结果的,但比较粗略的任务定义和大概的解决方案。然后,对该定义和方案进行细致的检查,以判断结果与期望之间的差距。同时,将上述步骤的解决方案,在更细的步骤中进行分解,每一项任务定义的精化变成了解决方案中算法的精化,后者还可能伴随着数据表达方式的精化。
好的自顶向下设计从几个方面避免了bug:
首先,清晰的结构和表达方式更容易对需求和模块功能进行精确的描述。
其次,模块分割和模块独立性避免了系统级的bug。
另外,细节的隐藏使结构上的缺陷更加容易识别。
第四,设计在每个精化步骤的层次上是可以测试的,所以测试可以尽早开始,并且每个步骤的重点可以放在合适的级别上。
结构化编程。该方法所设计程序的控制结构,仅包含语句形式的循环结构,例如DO WHILE,以及IF…THEN…ELSE的条件判断结构,而具体的条件部分在IF…THEN…ELSE后的花括号中描述。
Bohm和Jacopini展示了这些结构在理论上是可以证明的。而Dijkstra认为另外一种方法,即通过GO TO不加限制的分支跳转,会产生导致自身逻辑错误的结构。
关键的地方和构建无bug程序的核心,是把系统的结构作为控制结构来考虑,而不是独立的跳转语句。
13.2 整个调试过程有四个步骤,跟随这个过程来检验每个步骤各自的动机是一件很有趣的事情。
本机调试:
早期的机器的输入和输出设备很差,延迟也很长。因此,在一次机器交互会话中会尽可能多地包含试验性操作。在那种情况下,程序员仔细地设计他的调试过程——计划停止的地点,检验内存的位置,需要检查的东西以及如果没有预期结果时的对策。花费在编写调试程序上的时间,可能是程序编制时间的一半。
内存转储:
本机调试非常有效。在两小时的交互过程中可能会发现一打问题,但是计算机的资源非常匮乏,成本很高。因此,当使用在线高速打印机时,测试技术发生了变化。某人持续地运行程序,直到某个检测失败,这时所有的内存都被转储。接着,他将开始艰苦的桌面工作,考虑每个内存位置的内容。总之,整个过程的设计是为了减少计算机的使用时间,从而尽可能满足更多的用户。
快照:
采用内存转储技术的机器往往配有2000~4000个字(word双字节),或者8K~16K字节的内存。但是,随着内存的规模不断增长,对整个内存都进行转储变得不大可能。因此,人们开发了有选择的转储、选择性跟踪和将快照插入程序的技术。
交互式调试:
计算机将多个程序载入到内存中准备运行,被调试的程序和一个只能由程序控制的终端相关联,由监督调度程序控制调试过程。当终端前的编程人员停止程序,检查进展情况或者进行修改时,监督程序可以运行其他程序,从而保证了机器的使用率。
交互式调试拥有和本机调试一样的操作实时性,但前者并没有象后者要求的那样,在调试过程中要预先进行计划。
Gold实验得到一个有趣的结果,这个结果显示在每次调试会话中,第一次交互取得的工作进展是后续交互的三倍。这强烈地暗示着,由于缺乏对调试会话的计划,我们没有发掘交互式调试的潜力,原有本机调试技术中那段高效率的时间消失了。
测试用例:
关于实际调试过程和测试用例的设计,Grunberger提出了特别好的对策,在其他的文章中,也有较为简便的方法。
13.3 软件系统开发过程中出乎意料的困难部分是系统集成测试。其中需要再次确认的两件事是:系统调试花费的时间会比预料的更长,需要一种完备系统化和可计划的方法来降低它的困难程度。
使用经过调试的构件单元。尽管并不是普遍的实际情况——不过通常的看法是——系统集成调试要求在每个部分都能正常运行之后开始。(这既不同于为了查出接口bug所采取“合在一起尝试” 的方法;也不同于在所有构件单元的bug已知,但未修复的情况下,即开始系统调试的做法。)
搭建充分的测试平台。这里所说的辅助测试平台,指的是供调试使用的所有程序和数据,它们不会整合到最终产品中。测试平台可能会有相当于测试对象一半的代码量,但这是合乎情理的。一种测试辅助的形式是伪构件(dummy component),它仅仅由接口和可能的伪数据或者一些小的测试用例组成。另一种形式是微缩文件(miniature file)。微缩文件的特例是伪文件(dummy file),实际上并不常见。不过OS/360任务控制语言提供了这种功能,对于构件单元调试非常有用。
还有一种方式是辅助程序(auxiliary program)。用来测试数据发生器、特殊的打印输出、交叉引用表分析等,这些都是需要另外开发的专用辅助工具的例子。
控制变更。必须有人对变更进行控制和文档化,团队成员应使用开发库的各种受控拷贝来工作。
一次添加一个构件。系统测试期间,一次只添加一个构件。
阶段(量子)化、定期变更。随着项目的推进,系统构件的开发者会不时出现在我们面前,带着他们工作的最新版本——更快、更卓越、更完整,或者公认bug更少的版本。将使用中的构件替换成新版本,仍然需要进行和构件添加一样的系统化测试流程。这个时候通常已经具备了更完整有效的测试用例,因此测试时间往往会减少很多。
Lehman和Belady出示了证据,变更的阶段(量子)要么很大,间隔很宽;要么小和频繁。后者很容易变得不稳定。
项目是怎样延迟了整整一年的时间?…一次一天。一天一天的进度落后是难以识别、不容易防范和难以弥补的。
14.1 如何根据一个严格的进度表来控制项目?
第一个步骤是制订进度表。进度表上的每一件事,被称为“里程碑”,它们都有一个日期。里程碑的选择只有一个原则,那就是,里程碑必须是具体的、特定的、可度量的事件,能够进行清晰定义。
里程碑有明显边界和没有歧义,比它容易被老板核实更为重要。如果里程碑定义得非常明确,以致于无法自欺欺人时,很少有人会就里程碑的进展弄虚作假。但是如果里程碑很模糊,老板就常常会得到一份与实际情况不符的报告。
对于大型开发项目中的估计行为,政府的承包商所做的研究显示:
每两周进行仔细修订的活动时间估计,随着开始时间的临近不会有太大的变化;期间内对时间长短的过高估计,会随着活动的进行持续下降;过低估计直到计划的结束日期之前大约三周左右,才有所变化。
慢性进度偏离是士气杀手。
14.2 进取对于杰出的软件开发团队,同优秀的棒球队伍一样,是不可缺少的必要品德。
14.3 当一线经理发现自己的队伍出现了计划偏离时,他肯定不会马上赶到老板那里去汇报这个令人沮丧的消息。团队可以弥补进度偏差,他可以想出应对方法或者重新安排进度以解决问题,为什么要去麻烦老板呢?因此,所有的污垢都被隐藏在地毯之下。
但是每个老板都需要两种信息:需要采取行动的计划方面的问题,用来进行分析的状态数据。出于这个目的,他需要了解所有开发队伍的情况,但得到状态的真相是很困难的。
一线经理的利益和老板的利益是内在冲突的。一线经理担心如果汇报了问题,老板会采取行动,这些行动会取代经理的作用,降低自己的威信,搞乱了其他计划。所以,只要项目经理认为自己可以独立解决问题,他就不会告诉老板。
有两种掀开毯子把污垢展现在老板面前的方法:
一种是减少角色冲突和鼓励状态共享,另一种是猛地拉开地毯。
减少角色的冲突。首先老板必须区别行动信息和状态信息。他必须规范自己,不对项目经理可以解决的问题做出反应,并且决不在检查状态报告的时候做安排。当项目经理了解到老板收到项目报告之后不会惊慌,或者不会越俎代庖时,他就逐渐会提交真实的评估结果。
猛地拉开地毯。不论协作与否,拥有能了解状态真相的评审机制是必要的。PERT图以及频繁的里程碑是这种评审的基础。大型项目中,可能需要每周对某些部分进行评审,大约一个月左右进行整体评审。
PERT图的准备工作是老板和要向他进行汇报的经理们的职责。PERT的准备工作是PERT图使用中最有价值的部分。它包括了整个网状结构的展开、任务之间依赖关系的识别、各个任务链的估计。这些都要求在项目早期进行非常专业的计划。PERT图为前面那个泄气的借口,“其他的部分反正会落后”,提供了答案。
对于大型项目,一个对里程碑报告进行维护的计划和控制(Plan and Control)小组是非常可贵的。
不了解,就无法真正拥有。——歌德
计算机程序是从人传递到机器的一些信息。为了将人的意图清晰地传达给不会说话的机器,程序采用了严格的语法和严谨的定义。
15.1 不同用户需要不同级别的文档。
使用程序。每个用户都需要一段对程序进行描述的文字。为了得到一份有用的文字描述,就必须放慢脚步,稳妥地进行。
验证程序。除了程序的使用方法,还必须附带一些程序正确运行的证明,即测试用例。每一份发布的程序拷贝应该包括一些可以例行运行的小测试用例,为用户提供信心——他拥有了一份可信赖的拷贝,并且正确地安装到了机器上。
然后,需要得到更加全面的测试用例,这些用例可以根据输入数据的范围划分成三个部分:
修改程序。调整程序或者修复程序需要更多的信息。那么这份概述的组成部分是什么呢?
15.2 流程图是被吹捧得最过分的一种程序文档。流程图显示了程序的流程判断结构,它仅仅是程序结构的一个方面。详细逐一记录的流程图是一件令人生厌的事情,而且高级语言的出现使它显得陈旧过时。(流程图是图形化的高级语言。)
15.3 数据处理的基本原理告诉我们,试图把信息放在不同的文件中,并努力维持它们之间的同步,是一种非常费力不讨好的事情。
更合理的方法是:每个数据项包含两个文件都需要的所有信息,采用指定的键值来区别,并把它们组合到一个文件中。
不过,我们在程序文档编制的实践中却违反了我们自己的原则。典型的,我们试图维护一份机器可读的程序,以及一系列包含记叙性文字和流程图的文档。
我认为相应的解决方案是“合并文件”,即把文档整合到源代码。这对正确维护是直接有力的推动,保证编程用户能方便、即时地得到文档资料。这种程序被称为自文档化(self-documenting)。
方法:
第一个想法是借助那些出于语言的要求而必须存在的语句,来附加尽可能多的“文档”信息。因此,标签、声明语句、符号名称均可以作为工具,用来向读者表达尽可能多的意思。
第二个方法是尽可能地使用空格和一致的格式提高程序的可读性,表现从属和嵌套关系。
第三,以段落注释的形式,向程序中插入必要的记叙性文字。段落注释能提供总体把握和真正加深读者对整件事情的理解。
自文档化方法激发了高级语言的使用,特别是用于在线系统的高级语言——无论是对批处理还是交互式,它都表现出最强的功效和应用的理由。如同我曾经提到的,上述语言和系统强有力地帮助了编程人员。因为是机器为人服务,而不是人为机器服务。因此从各个方面而言,无论是从经济上还是从以人为本的角度来说,它们的应用都是非常合情合理的。
没有任何技术或管理上的进展,能够独立地许诺十年内使生产率、可靠性或简洁性获得数量级上的进步。
16.1 所有的软件活动包括根本任务——打造构成抽象软件实体的复杂概念结构,次要任务——使用编程语言表达这些抽象实体,在空间和实践限制内将它们映射成机器语言。
相对必要任务而言,软件工程师在次要任务上花费了多少时间和精力?除非次要任务占了所有工作的9/10,否则即使全部次要任务的时间缩减到零,也不会带来生产率数量级上的提高。
16.2 在所有恐怖民间传说的妖怪中,最可怕的是人狼,因为它们可以完全出乎意料地从熟悉的面孔变成可怕的怪物。为了对付人狼,我们在寻找可以消灭它们的银弹。
大家熟悉的软件项目具有一些人狼的特性(至少在非技术经理看来),常常看似简单明了的东西,却有可能变成一个落后进度、超出预算、存在大量缺陷的怪物。
但是,我们看看近十年来的情况,没有银弹的踪迹。没有任何技术或管理上的进展,能够独立地许诺在生产率、可靠性或简洁性上取得数量级的提高。
解决管理灾难的第一步是将大块的“巨无霸理论”替换成“微生物理论”,它的每一步——希望的诞生,本身就是对一蹴而就型解决方案的冲击。
16.3 不仅仅是在目力所及的范围内,没有发现银弹,而且软件的特性本身也导致了不大可能有任何的发明创新——能够像计算机硬件工业中的微电子器件、晶体管、大规模集成一样——提高软件的生产率、可靠性和简洁程度。
首先,我们必须看到这样的畸形并不是由于软件发展得太慢,而是因为计算机硬件发展得太快。
其次,让我们通过观察预期的软件技术产业发展速度,来了解中间的困难。我将它们分成根本的——软件特性中固有的困难,次要的——出现在目前生产上的,但并非那些与生俱来的困难。
我认为软件开发中困难的部分是规格化、设计和测试这些概念上的结构,而不是对概念进行表达和对实现逼真程度进行验证。如果这是事实,那么软件开发总是非常困难的。天生就没有银弹。
现代软件系统中这些无法规避的内在特性:复杂度、一致性、可变性和不可见性。
复杂度。规模上,软件实体可能比任何由人类创造的其他实体要复杂,因为没有任何两个软件部分是相同的(至少是在语句的级别)。软件的复杂度是必要属性,不是次要因素。
一致性。软件工程师必须控制的很多复杂度是随心所欲、毫无规则可言的,来自若干必须遵循的人为惯例和系统。它们随接口的不同而改变,随时间的推移而变化,而且,这些变化不是必需的,仅仅由于它们是不同的人——而非上帝——设计的结果。某些情况下,因为是开发最新的软件,所以它必须遵循各种接口。另一些情况下,软件的开发目标就是兼容性。在上述的所有情况中,很多复杂性来自保持与其他接口的一致,对软件的任何再设计,都无法简化这些复杂特性。
可变性。软件实体经常会遭受到持续的变更压力。其中部分的原因是因为系统中的软件包含了很多功能,而功能是最容易感受变更压力的部分。另外的原因是因为软件可以很容易地进行修改——它是纯粹思维活动的产物,可以无限扩展。
不可见性。软件是不可见的和无法可视化的。软件的客观存在不具有空间的形体特征。因此,没有已有的表达方式,就像陆地海洋有地图、硅片有膜片图、计算机有电路图一样。除去软件结构上的限制和简化方面的进展,软件仍然保持着无法可视化的固有特性,从而剥夺了一些具有强大功能的概念工具的构造思路。这种缺憾不仅限制了个人的设计过程,也严重地阻碍了相互之间的交流。
16.4 如果回顾一下软件领域中取得的最富有成效的三次进步,我们会发现每一次都是解决了软件构建上的巨大困难,但是这些困难不是本质属性,也不是主要困难。
高级语言。勿庸置疑,软件生产率、可靠性和简洁性上最有力的突破是使用高级语言编程。大多数观察者相信开发生产率至少提高了五倍,同时可靠性、简洁性和理解程度也大为提高。
高级语言取得了哪些进展呢?
首先,它减轻了一些次要的软件复杂度。
高级语言最可能实现的是提供所有编程人员在抽象程序中能想到的要素。
分时。大多数观察者相信分时提高了程序员的生产率和产品的质量,尽管它带来的进步不如高级语言。分时保证了及时性,从而使我们能维持对复杂程度的一个总体把握。
统一编程环境。第一个集成开发环境——Unix和Interlisp现在已经得到了广泛应用,并且使生产率提高了5倍。
16.5 让我们来讨论一下当今可能作为潜在银弹的最先进的技术进步。
Ada和其他高级编程语言。Ada仍然不是消灭软件生产率怪兽的银弹。毕竟,它只是另一种高级语言,这类语言出现最大的回报来自出现时的冲击,它通过使用更加抽象的语句来开发,降低了机器的次要复杂度。
面向对象编程。两个概念::抽象数据类型和层次化类型,后者也被称为类(class)。抽象数据类型的概念是指对象类型应该通过一个名称、一系列合适的值和操作来定义,而不是理应被隐藏的存储结构。层次化类型,是允许定义可以被后续子类型精化的通用接口。
人工智能。很多人期望人工智能上的进展可以给软件生产率和质量带来数量级上的增长,但我不这样认为。
专家系统。人工智能领域最先进的、被最大范围使用的部分,是开发专家系统的技术。专家系统是包含归纳推论引擎和规则基础的程序,它接收输入数据和假设条件,通过从基础规则推导逻辑结果,提出结论和建议,向用户展示前因后果,并解释最终的结果。推论引擎除了处理推理逻辑以外,通常还包括复杂逻辑或者概率数据和规则。
“自动”编程。近四十年中,人们一直在预言和编写有关“自动编程”的文字,从问题的一段陈述说明自动产生解决问题的程序。现在,仍有一些人期望这样的技术能够成为下一个突破点。Parnas暗示这是一个用于魔咒的术语,声称它本身是语义不完整的。一句话,自动编程总是成为一种热情,使用现在并不可用的更高级语言编程的热情。
图形化编程。这种方法的推测部分来自VLSI芯片设计的类比,计算机图形化在设计中扮演了高生产力的角色。部分源于——人们将流程图作为一种理想的设计介质,并为绘制它们提供了很多功能强大的实用程序——这证实了图形化的可行性。这个方法中至今还没有出现任何令人信服和激动的进步。
程序验证。现代编程的许多工作是测试和修复bug。是否有可能出现银弹,能够在系统设计级别、源代码级别消除bug呢?是否可以在大量工作被投入到实现和测试之前,通过采用证实设计正确性的“深奥”策略,彻底提高软件的生产率和产品的可靠性?我并不认为在这里能找到魔法。
环境和工具。向更好的编程开发环境开发中投入,我们可以期待得到多少回报呢?人们的本能反应是首先着手解决高回报的问题:层次化文件系统,统一文件格式以获得一致的编程接口和通用工具等。特定语言的智能化编辑器在现实中还没有得到广泛应用,不过它们最有希望实现的是消除语法错误和简单的语义错误。
工作站。随着工作站的处理能力和内存容量的稳固和快速提高,我们能期望在软件领域取得多大的收获呢?现在的运算速度已经可以完全满足程序编制和文档书写的需要。编译还需要一些提高,不过一旦机器运算速度提高十倍,那么程序开发人员的思考活动将成为日常工作的主要活动。实际上,这已经是现在的情况。我们当然欢迎更加强大的工作站,但是不能期望有魔术般的提高。
16.6 所有针对软件开发过程中次要困难的技术工作基本上能表达成以下的生产率公式:
任务时间 = Σ(频率)i×(时间)
工作的创造性部分占据了大部分时间,那么那些仅仅是表达概念的活动并不能在很大程度上影响生产率。
购买和自行开发。
构建软件最可能的彻底解决方案是不开发任何软件。越来越多的软件提供商,为各种眼花缭乱的应用程序提供了质量更好、数量更多的软件产品。对于这些软件,购买都要比重新开发要低廉一些。
软件成本一直是开发的成本,而不是复制的成本。所以,即使只在少数使用者之间实现共享,也能在很大程度上减少成本。
需求精炼和快速原型。
开发软件系统的过程中,最困难的部分是确切地决定搭建什么样的系统。概念性工作中,没有其他任何一个部分比确定详细的技术需求更加困难,详细的需求包括了所有的人机界面、与机器和其他软件系统的接口。需求工作对系统的影响比其他任何一个部分的失误都大,当然纠正需求的困难也比其他任何一个部分要大。
增量开发——增长,而非搭建系统。很多年前,Harlan Mill建议所有软件系统都应该以增量的方式开发。即,首先系统应该能够运行,即使未完成任何有用功能,只能正确调用一系列伪子系统。接着,系统一点一点被充实,子系统轮流被开发,或者是在更低的层次调用程序、模块、子系统的占位符(伪程序)等。
这种开发模式对士气的推动是令人震惊的。当一个可运行系统——即使是非常简单的系统出现时,开发人员的热情就迸发了出来。
卓越的设计人员。关键的问题是如何提高软件行业的核心,一如既往的是——人员。
17.1《没有银弹》中声称和断定,在近十年内,没有任何单独的软件工程进展可以使软件生产率有数量级的提高。因此,必须着手解决开发的根本问题。
17.2 重用和交互的构件开发是解决软件根本困难的一种方法。
17.3 现实问题。对我而言(尽管不是所有人),关键论点的正确与否归结为一个现实问题:整个软件开发工作中的哪些部分与概念性结构的精确和有序表达相关,哪些部分是创造那些结构的思维活动?
根据缺陷是概念性的(例如未能识别某些异常),或者是表达上的问题(例如指针错误或者内存分配错误)等,可以将这些缺陷的寻找和修复工作进行相应的划分。在我看来,开发的次要或者表达部分现在已经下降到整个工作的一半或一半以下。
17.4 Harel认识到《没有银弹》中的消极来自三个主题:
根本和次要问题的清晰划分
独立地评价每个候选银弹
仅仅预言了十年,而不是足够长的时间“出现任何重大的进步。”
17.5 。Jone提出,“不。关注质量,生产率自然会随着提高。”他认为,很多代价高昂的后续项目投入了大量的时间和精力来寻找和修复规格说明中、设计和实现上的错误。
Coqui也提出相似的主张:系统化软件开发方法的发展是为了解决质量问题(特别是避免大型的灾难),而不是出于生产率方面的考虑。
17.6 生产率数据。生产率的数据非常难以定义、测量和寻找。
17.7 面向对象编程——这颗铜质子弹可以吗?
面向对象编程的第一个特征是,它强制的模块化和清晰的接口。其次,它强调了封装,即外界无法看到组件的内部结构;它还强调了继承和层次化类结构以及虚函数。面向对象还强调了强抽象数据类型化,它确保某种特定的数据类型只能由它自身的相应函数来操作。
面向对象技术包含了很多方法学上的进步。面向对象技术的前期投入很多——主要是培训程序员用很新的方法思考,同时还要把函数打造成通用的类。我认为它的好处是客观实在的,并非仅仅是推测。面向对象应用在整个开发周期中,但是真正的获益只有在后续开发、扩展和维护活动中才能体现出来。
解决软件构建根本困难的最佳方法是不进行任何开发。软件包只是达到上述目标的方法之一,另外的方法是程序重用。实际上,类的容易重用和通过继承方便地定制是面向对象技术最吸引人的地方。
17.8 大多数有丰富经验的程序员拥有自己的私人开发库,可以使他们使用大约30%的重用代码来开发软件。公司级别的重用能提供70%的重用代码量,它需要特殊的开发库和管理支持。公司级别的重用代码也意味着需要对项目中的变更进行统计和度量,从而提高重用的可信程度。
17.9 高级语言有更广泛的词汇量、更复杂的语法以及更加丰富的语义。对于重用,词汇学习并不是思维障碍中的一小部分。
如果想获得所有潜在的重用,任何使用类库编程的人员必须学习其成员的语法(外部接口)和语义(详细的功能行为)。越复杂的功能,学习成本越高。高级语言比机器语言强大,但是也更加复杂。而需要重用一个模块,则需要学习相应模块的成本。
17.10 复杂性是我们这个行业的属性,而且复杂性是我们主要的限制。软件开发是一件棘手的事情,前方并不会有魔术般的解决方案。现在,有可能,我们可以在软件生产率上取得逐步的进展,而不是等待不大可能到来的突破。
只能根据过去判断将来。——帕特里克·亨利
然而永远无法根据过去规划将来。——埃德蒙·伯克
19.2 概念完整性。一个整洁、优雅的编程产品必须向它的每位用户提供一个条理分明的模型概念,这个模型描述了应用、实现应用的方法以及用来指明操作和各种参数的用户界面使用策略。用户所感受到的产品概念完整性是易用性中最重要的因素。
结构师。结构师开发用于向用户解释使用的产品概念模型,概念模型包括所有功能的详细说明以及调用和控制的方法。结构师是这些模型的所有者,同时也是用户的代理。在不可避免地对功能、性能、规模、成本和进度进行平衡时,卓有成效地体现用户的利益。结构师就像电影的导演,而经理类似于制片人。
将体系结构和设计实现、物理实现相分离。为了使结构师的关键任务更加可行,有必要将用户所感知的产品定义——体系结构,与它的实现相分离。
体系结构的递归。对于大型系统,即使所有实现方面的内容都被分离出去,一个人也无法完成所有的体系结构工作。所以,有必要由一位主结构师把系统分解成子系统,系统边界应该划分在使子系统间接口最小化和最容易严格定义的地方。显然,这个过程可以根据需要重复递归地进行。
概念完整性是产品质量的核心。拥有一位结构师是迈向概念完整性最重要的一步。
19.3 开发第二个系统所引起的后果:盲目的功能和频率猜测
盲目的功能(Featuritis)。对于如电子表格或字处理等通用工具的结构师,一个不断困扰他们的诱惑是以性能甚至是可用性的代价,过多地向产品添加边界实用功能。
定义用户群。用户群越大和越不确定,就越有必要明确地定义用户群,以获得概念完整性。
把用户群的属性记录下来,包括:
他们是谁
他们需要(need)什么
他们认为自己需要(need)什么
他们想要(want)的是什么
频率。对于软件产品,任何用户群属性实际上都是一种概率分布,每个属性具有若干可能的值,每个值有自己的发生频率。为了得到完整、明确和共有的用户群描述,结构师应该猜测(guess),或者假设(postulate)完整的一系列属性和频率值。
19.4 通过类比获得的概念完整性。WIMP是一个充分体现了概念完整性的用户界面例子,完整性的获得是通过采用大家非常熟悉的概念模型——对桌面的比喻,以及一致、细致的扩展,后者充分发挥了计算机的图形化实现能力。
19.5 在《未雨绸缪》一章体现的概念中,最大的错误是它隐含地假设了使用传统的顺序或者瀑布开发模型。
瀑布模型的基本谬误是它假设项目只经历一次过程,而且体系结构出色并易于使用,设计是合理可靠的,随着测试的进行,编码实现时可以修改和调整的。换句话说,瀑布模型假设所有错误发生在编码实现阶段,因此它们的修复可以很顺畅地穿插在单元和系统测试中。
瀑布模型的第二个谬误是它假设整个系统一次性地被构建,在所有的设计、大部分编码、部分单元测试完成之后,才为闭环的系统测试合并各个部分。
在把任何东西实现成代码之前,可能要往复迭代两个或更多的体系结构——设计——实现循环。
19.6 构建闭环的框架系统
从事实时系统开发的Harlan Mills,早期曾提倡我们首先应该构建实时系统的基本轮询回路,为每个功能都提供子函数调用(占位符),但仅仅是空的子函数。对它进行编译、测试,使它可以不断运行。它不直接完成任何事情,但至少是正常运行的。
接着,我们添加(可能是基本的)输入模块和输出模块。
然后,一个功能接一个功能,我们逐渐开发和增加相应模块。在每个阶段,我们都拥有一个可运行的系统。
在每个功能基本可以运行之后,我们一个接一个地精化或者重写每个模块——增量地开发(growing)整个系统。不过,我们有时的确需要修改原有的驱动回路,或者甚至是回路的模块接口。
因为我们在所有时刻都拥有一个可运行的系统,所以我们可以很早开始用户测试,以及我们可以采用按预算开发的策略,来彻底保证不会出现进度或者预算的超支(以允许的功能牺牲作为代价)。
Parnas产品族:
Parnas力劝设计人员对产品的后期扩展和后续版本进行预测,定义它们在功能或者平台上的差异,从而搭建一棵相关产品的家族树。设计类似一棵树的技巧是将那些变化可能性较小的设计决策放置在树的根部。
Microsoft的“每晚重建”方法:
在我们第一次产品发布之后,我们会继续发布后续版本,向已有的可运行系统添加更多的功能。从我们第一个里程碑开始[第一次发布有三个里程碑],我们每晚重建开发中的系统[以及运行测试用例]。该构建周期成了项目的“心跳”。每天,一个和多个程序员-测试员队伍提交若干具有新功能的模块。在每次重建之后,我们会获得一个可运行的系统。如果重建失败,我们将停下整个过程,直到找到问题所在并进行修复。在任何时间,团队中的每个人都了解项目的状态。
增量式开发和快速原型:
增量开发过程能使真正的用户较早地参与测试,那么它与快速原型之间的区别是什么呢?我认为它们既是互相关联,又是相互独立的。各自可以不依赖对方而存在。
19.7 现在,我确信信息隐藏——现在常常内建于面向对象的编程中——是唯一提高软件设计水平的途径。
Parnas的模块信息隐藏定义是研究项目中的第一步,它是面向对象编程的鼻祖。Parnas把模块定义成拥有自身数据模型和自身操作集的软件实体。它的数据仅仅能通过它自己的操作来访问。
第二步是把Parnas模块提升到抽象数据类型,从中可以派生出很多对象。抽象数据类型提供了一种思考和指明模块接口的统一方式,以及容易保证实施的类型规范化访问方法。
第三步,面向对象编程引入了一个强有力的概念——继承,即类(数据)默认获得类继承层次中祖先的属性。我们希望从面向对象编程中得到的最大收获实际上来自第一步,模块隐藏,以及预先建成的、为了重用而设计和测试的模块或者类库。
19.8 很多年来,人们对软件生产率和影响它的因素进行了大量的量化研究,特别是在项目人员配备和进度之间的平衡方面。
最充分的一项研究是Barry Boehm对63个项目的调查,他的结果充分地吻合了《人月神话》的结论,即人力(人)和时间(月)之间的平衡远不是线性关系,使用人月作为生产率的衡量标准实际是一个神话。
第一次发布的成本最优进度时间。即,月单位的最优时间是估计工作量(人月)的立方根,估计工作量则由规模估计和模型中的其他因子导出。最优人员配备曲线是由推导得出的。
当计划进度比最优进度长时,成本曲线会缓慢攀升。时间越充裕,花的时间也越长。
当计划进度比最优进度短时,成本曲线急剧升高。
无论安排多少人手,几乎没有任何项目能够在少于3/4的最优时间内获得成功!当高级经理向项目经理要求不可能的进度担保时,这段结论可以充分地作为项目经理的理论依据。
Brooks法则:向进度落后的软件项目中添加人手只会使进度更加落后。
最棒的研究发表在Abdel-Hamid和Madnick在1991年出版的一本颇有价值的书《软件项目动力学:一条完整的路》中。书中提出了项目动态特性的量化模型。他们得出结论“向进度落后的项目中添加人手总会增加项目的成本,但并不一定会使项目更加落后。”特别的,由于新成员总会立刻带来需要数周来弥补的负面效应,所以在项目早期添加额外的人力比在后期加入更加安全一些。
Stutzke为了进行相似的研究,开发了一个更简单的模型,得出了类似的结果。特别值得注意的是,他建议开发项目后期增加的开发人员,必须作为团队成员,愿意在过程中努力投入和工作,而不是企图改变或者改进过程本身!Stutzke认为更大型的项目中,增加的沟通负担是次要作用,没有对它建模。
19.9 人就是一切。
人件。近年来,软件工程领域的一个重大贡献是DeMarco和Lister在1987年出版的数据,《人件:高生产率的项目和团队》所表达的观点是“我们行业的主要问题实质上更侧重于社会学(sociological)而不是科学技术(technological)。”
DeMarco和Lister从他们的Coding War Games项目中提供的数据,显示了相同组织中开发人员的表现之间,和工作空间和生产率以及缺陷水平之间令人吃惊的关联。
19.10 如果人们认同我在文中多处提到的观点——创造力来自于个人,而不是组织架构或者开发过程,那么项目管理面对的中心问题是如何设计架构和流程,来提高而不是压制主动性和创造力。
公司通过将权利下放到具体的团队,事实上使得组织机构变得更加融洽和繁荣。
19.11 微型计算机革命改变了每个人使用计算机的方式。
微型计算机革命改变了每个人开发软件的方式。70年代的软件过程本身被微处理器革命和它所带来的科学技术进步所改变。很多软件开发过程的次要困难被消除。快速的个人计算机现在是软件开发者的常规工具,从而周转时间的概念几乎成为了历史。
19.12 塑料薄膜包装的成品软件产业。对于这个产业的开发者,面对的是与传统产业完全不同的经济学:软件成本是开发成本与数量的比值,包装和市场成本非常高。在传统内部的应用开发产业,进度和功能细节是可以协商的,开发成本则可能不行;而在竞争激烈的开发市场面前,进度和功能支配了开发成本。
传统产业倾向于被大型公司以已指定的管理风格和企业文化所支配。另一方面,始于数百家创业公司的成品软件产业,行事自由,更加关注结果,而不是流程。在这种趋势下,那些天才的个人程序员更容易获得认可,这隐含了“卓越的设计来自于杰出的设计人员”的观点。创业文化能够对那些杰出人员,根据他们的贡献进行奖励。而在传统软件产业中,公司的社会化因素和薪资管理计划总会使上述做法难以实施。
19.13 彻底提高软件健壮性和生产率的唯一途径,是提升一个级别,使用模块或者对象组合来进行程序的开发。一个特别有希望的趋势是使用大众市场的软件包作为平台,在上面开发更丰富和更专业化的产品。
元编程。Hypercard Stack、Excel模板、Minicard函数的开发有时被称为元编程(metaprograming),为部分软件包用户进行功能定制的过程。元编程并不是新概念,仅仅是重新被提出和重新命名。
当然,存在的困难是成品软件是作为独立实体来设计,元程序员无法改变它的功能和接口。
我们可以识别出四个层次的软件用成品户:
直接使用用户。他们以简便直接的方式来操作,对设计者提供的功能和接口感到满意。
元程序员。在单个应用程序的基础上,使用已提供的接口来开发模板或者函数,主要为最终用户节省工作量。
外部功能作者,向应用程序中添加自行编制的功能。这些功能本质上是新应用语言原语,调用通用语言编写的独立模块。这往往需要命令中断、回调或者重载函数技术,向原接口添加新功能。
元程序员,使用一个和多个特殊的应用程序,作为更大型系统的构件。他们是需求并没有得到满足的用户群。同时,这也是能在构建新应用程序方面获得较大收获的用法。
对于成品软件,最后一种类型的用户还需要额外的文档化接口,即元编程接口(metaprogramming interface,MPI)。
19.14 今天,软件工程的一些特殊问题正如第1章中所提出的:
如何把一系列程序设计和构建成系统
如何把程序或者系统设计成健壮的、经过测试的、文档化的、可支持的产品
如何维持对大量的复杂性的控制
软件工程的焦油坑在将来很长一段时间内会继续地使人们举步维艰,无法自拔。软件系统可能是人类创造中最错综复杂的事物,只能期待人们在力所能及的或者刚刚超越力所能及的范围内进行探索和尝试。这个复杂的行业需要:进行持续的发展;学习使用更大的要素来开发;新工具的最佳使用;经论证的管理方法的最佳应用;良好判断的自由发挥;以及能够使我们认识到自己不足和容易犯错的——上帝所赐予的谦卑。