把软件问题分成“根本的”和“次要的”
水平边界以下,程序变成编程产品(Programming Product)。这是可以被任何人运行、测试、修复和扩展的程序。
要将程序提升为程序产品,还需要有完备的文档,每个人都可以加以使用、修复和扩展。
垂直边界的右边,程序变成编程系统(Programming System)中的一个构件单元。。它是在功能上能相互协作的程序集合,具有规范的格式,可以进行交互,并可以来组装和搭建整个系统。
因为一些意想不到的交互会产生许多不易察觉的 bug,测试工作将会非常耗时,因此相同功能的编程系统构件的成本至少是独立程序的三倍。如果系统有大量的组成单元,成本还会更高。
图 1.1 的右下部分代表编程系统产品(Programming Systems Product)。和以上的所有的情况都不同的是,它的成本高达九倍。然而,只有它才是真正有用的产品,是大多数系统开发的目标。
编程为什么有趣?
1. 首先是一种创建事物的纯粹快乐。
2. 其次,快乐来自于开发对其他人有用的东西。
3. 第三是整个过程体现出魔术般的力量——将相互啮合的零部件组装在一起,看到它们精妙地运行,得到预先所希望的结果。
4. 第四是学习的乐趣,来自于这项工作的非重复特性。
5. 最后,乐趣还来自于工作在如此易于驾驭的介质上。
职业的苦恼?
1. 首先,必须追求完美。
2. 其次,是由他人来设定目标,供给资源,提供信息。
3. 概念性设计是有趣的,但寻找琐碎的 bug 却只是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的劳动。
4. 当投入了大量辛苦的劳动,产品在即将完成或者终于完成的时候,却已显得陈旧过时。
这,就是编程。一个许多人痛苦挣扎的焦油坑以及一种乐趣和苦恼共存的创造性活动。
导致项目滞后这种普遍性灾难的原因是什么呢?
1. 首先,我们对估算技术缺乏有效的研究,更加严肃地说,它反映了一种悄无声息,但并不真实的假设——一切都将运作良好。
2. 第二,我们采用的估算技术隐含地假设人和月可以互换,错误地将进度与工作量相互混淆。
3. 第三,由于对自己的估算缺乏信心,软件经理通常不会有耐心持续地进行估算这项工作。
4. 第四,对进度缺少跟踪和监督。
5. 第五,当意识到进度的偏移时,下意识(以及传统)的反应是增加人力。这就像使用汽油灭火一样,只会使事情更糟。越来越大的火势需要更多的汽油,从而进入了一场注定会导致灾难的循环。
Dorothy Sayers 在她的“The Mind of the Maker”一书中,将创造性活动分为三个阶段:构思、实现和交流。
对于创造者,只有在实现的过程中,才能发现我们构思的不完整性和不一致性。
我们的乐观主义并不应该是理所应当的。
人数和时间的互换仅仅适用于以下情况:某个任务可以分解给参与人员,并且他们之间不需要相互的交流。
沟通所增加的负担由两个部分组成,培训和相互的交流。
因为软件开发本质上是一项系统工作——错综复杂关系下的一种实践——沟通、交流的工作量非常大,它很快会消耗任务分解所节省下来的个人时间。从而,添加更多的人手,实际上是延长了,而不是缩短了时间进度。
系统测试进度的安排常常是编程中最不合理的部分。
对于软件任务的进度安排,以下是我使用了很多年的经验法则:
1/3 计划
1/6 编码
1/4 构件测试和早期系统测试
1/4 系统测试,所有的构件已完成
不为系统测试安排足够的时间简直就是一场灾难。
非阶段化方法的采用,少得可怜的数据支持,加上完全借助软件经理的直觉,这样的方式很难生产出健壮可靠和规避风险的估计。
Brooks 法则:
向进度落后的项目中增加人手,只会使进度更加落后。(Adding manpower to a late software project makes it later)
简言之,$20,000/年的程序员的生产率可能是$10,000/年程序员的 10 倍。
绝大多数大型编程系统的经验显示出,一拥而上的开发方法是高成本的、速度缓慢的、不充分的,开发出的是无法在概念上进行集成的产品。
Mills 建议大型项目的每一个部分由一个团队解决,但是该队伍以类似外科手术的方式组建,而并非一拥而上。
外科手术的组建方式:
* 外科医生。Mills 称之为首席程序员。他亲自定义功能和性能技术说明书,设计程序,编制源代码,测试以及书写技术文档。
* 副手。他是外科医生的后备,能完成任何一部分工作,但是相对具有较少的经验。他的主要作用是作为设计的思考者、讨论者和评估人员。外科医生试图和他沟通设计,但不受到他建议的限制。
* 管理员。外科医生是老板,他必须在人员、加薪等方面具有决定权,但他决不能在这些事务上浪费任何时间。因而,他需要一个控制财务、人员、工作地点安排和机器的专业管理人员,该管理员充当与组织中其他管理机构的接口。
* 编辑。外科医生负责产生文档——出于最大清晰度的考虑,他必须书写文档。对内部描述和外部描述都是如此。而编辑根据外科医生的草稿或者口述的手稿,进行分析和重新组织,提供各种参考信息和书目,对多个版本进行维护以及监督文档生成的机制。
* 两个秘书。管理员和编辑每个人需要一个秘书。管理员的秘书负责项目的协作一致和非产品文件。
* 程序职员。他负责维护编程产品库中所有团队的技术记录。该职员接受秘书性质的培训,承担机器码文件和可读文件的相关管理责任。
* 工具维护人员。现在已经有很多文件编辑、文本编辑和交互式调试等工具,因此团队很少再需要自己的机器和机器操作人员。
* 测试人员。外科医生需要大量合适的测试用例,用来对他所编写的工作片段,以及对整个工作进行测试。。因此,测试人员既是为他的各个功能设计系统测试用例的对头,同时也是为他的日常调试设计测试数据的助手。他还负责计划测试的步骤和为测试搭建测试平台。
* 语言专家。随着 Algol 语言的出现,人们开始认识到大多数计算机项目中,总有一两个乐于掌握复杂编程语言的人。这些专家非常有帮助,很快大家会向他咨询。
以上就是如何参照外科手术队伍,以及如何对 10 人的编程队伍进行专业化的角色分工。
在外科手术团队中,外科医生和副手都了解所有的设计和全部的代码。
这里,可以认为整个系统必须具备概念上的完整性,要有一个系统结构师从上至下地进行所有的设计。
如同旅游指南所述,风格的一致和完整性来自 8 代拥有自我约束和牺牲精神的建筑师们,他们每一个人牺牲了自己的一些创意,以获得纯粹的设计。
我主张在系统设计中,概念完整性应该是最重要的考虑因素。也就是说为了反映一系列连贯的设计思路,宁可省略一些不规则的特性和改进,也不提倡独立和无法整合的系统,哪怕它们其实包含着许多很好的设计。
编程系统(软件)的目的是使计算机更加容易使用。
只有当这些功能说明节约下来的时间,比用在学习、记忆和搜索手册上的时间要少时,易用性才会得到提高。
由于目标是易用性,功能与理解上复杂程度的比值才是系统设计的最终测试标准。
简洁和直白来自概念的完整性。每个部分必须反映相同的原理、原则和一致的折衷机制。
概念的完整性要求设计必须由一个人,或者非常少数互有默契的人员来实现。
系统的体系结构(architecture)指的是完整和详细的用户接口说明。 对于计算机,它是编程手册;对于编译器,它是语言手册;对于控制程序,它是语言和函数调用手册;对于整个系统,它是用户要完成自己全部工作所需参考的手册的集合。
体系结构同实现必须仔细地区分开来。如同 Blaauw 所说的,“体系结构陈述的是发生了什么,而实现描述的是如何实现 “。
如果出现了很多非常重要但不兼容的构想,就应该抛弃原来的设计,对不同基本概念进行合并,在合并后的系统上重新开始。
在毫无限制的实现小组中,在进行结构上的决策时,会出现大量的想法和争议,对具体实现的关注反而会比较少。
概念完整性的缺乏导致系统开发和修改上要付出更昂贵的代价,我估计至少增加了一年的调试时间。
实现同样是一项高级别的创造性活动。具体实现中创造和发明的机会,并不会因为指定了外部技术说明而大为减少,相反创造性活动会因为规范化而得到增强,整个产品也一样。
整 个 创 造 性 活 动 包 括 了 三 个 独 立 的 阶 段 : 体 系 结 构(architecture)、设计实现(implementation)、物理实现(realization)在实际情况中,它们往往可以同时开始和并发地进行。
同工作的水平分割相比,垂直划分从根本上大大减少了劳动量,结果是使交流彻底地简化,概念完整性得到大幅提高。
第二个系统是设计师们所设计的最危险的系统。
结 构 师 如 何 避 免 画 蛇 添 足 — — 开 发 第 二 个 系 统 所 引 起 的 后 果 ( second-system effect)?是的,他无法跳过二次系统。但他可以有意识关注那些系统的特殊危险,运用特别的自我约束准则,来避免那些功能上的修饰;根据系统基本理念及目的变更,舍弃一些功能。
手册不但要描述包括所有界面在内的用户可见的一切,它同时还要避免描述用户看不见的事物。
如果想保持文字和产品之间的一致性,则必须由一个或两个人来完成将其结论转换成书面规格说明的工作。
一句古老的格言警告说:“决不要携带两个时钟出海,带一个或三个。”同样的原则也适用于形式化和记叙性定义。如果同时具有两种方式,则必须以一种作为标准,另一种作为辅助描述,并照此明确地进行划分。
周例会是每周半天的会议,由所有的结构师,加上硬件和软件实现人员代表和市场计划人员参与,由首席系统结构师主持。
周例会卓有成效是由于:
1. 数月内,相同小组——结构师、用户和实现人员——每周交流一次。
2. 没有人是“顾问”的角色,每个人都要承担义务。
3. 当问题出现时,在界线的内部和外部同时寻求解决方案。
4. 正式的书面建议集中了注意力,强制了决策的制订,避免了会议草稿纪要方式的不一致。
5. 清晰地授予首席结构师决策的权力,避免了妥协和拖延。
随着实现的推进,无论规格说明已经多么精确,还是会出现无数结构理解和解释方面的问题。一种有用的机制是由结构师保存电话日志。日志中,他记录了每一个问题和相应的回答。每周,对若干结构师的日志进行合并,重新整理,并发布给用户和实现人员。这种机制很不正式,但非常快捷和易于理解。
团队如何进行相互之间的交流沟通呢?通过所有可能的途径。
* 非正式途径:清晰定义小组内部的相互关系和充分利用电话,能鼓励大量的电话沟通,从而达到对所书写文档的共同理解。
* 会议
* 工作手册:在项目的开始阶段,应该准备正式的项目工作手册。理所应当,我们专门用一节来讨论它。
让我们考虑一下树状编程队伍,以及要使它行之有效,每棵子树所必须具备的基本要素。它们是:
1. 任务(a mission)
2. 产品负责人(a producer)
3. 技术主管和结构师(a technical director or architect)
4. 进度(a schedule)
5. 人力的划分(a division of labor)
6. 各部分之间的接口定义(interface definitions among the parts)
产品负责人的角色是什么?他组建团队,划分工作及制订进度表。
技术主管的角色是什么?他对设计进行构思,识别系统的子部分,指明从外部看上去的样子,勾画它的内部结构。,他是“攻坚小组中的独行侠”(inside-man at the skunk works.)。他的沟通交流在团队中是首要的。
另外,还有一些技巧。例如,产品责任人可以通过一些微妙状态特征暗示来(如,办公室的大小、地毯、装修、复印机等等)体现技术主管的威信,尽管决策权力的源泉来自管理。
对于真正大型项目中的一些开发队伍,我认为产品负责人作为管理者是更合适的安排。
项目时间的评估计划、编制文档、测试、系统集成和培训的时间必须被考虑在内。
简言之,项目估算对每个人年的技术工作时间数量做出了不现实的假设。
同任何开销一样,规模本身不是坏事,但不必要的规模是不可取的。
对项目经理而言,规模控制既是技术工作的一部分,也是管理工作的一部分。他必须研究用户和他们的应用,以设置将开发系统的规模。
OS/360 项目的教训:
1. 首先,仅对核心程序设定规模目标是不够的,必须把所有的方面都编入预算。
2. 在每个模块分配功能之前,已编制了空间的预算。第二个道理也很清晰:在指明模块有多大的同时,确切定义模块的功能。
3. 培养开发人员从系统整体出发、面向用户的态度是软件编程管理人员最重要的职能。
项目经理可以做两件事来帮助他的团队取得良好的空间-时间折衷:
1. 一是确保他们在编程技能上得到培训,而不仅仅是依赖他们自己掌握的知识和先前的经验。
2. 另外一种方法是认识到编程需要技术积累,需要开发很多公共单元构件。
更普遍的是,战略上突破常来自数据或表的重新表达——这是程序的核心所在。
实际上,数据的表现形式是编程的根本。
如果系统设计能自由地变化,则项目组织架构必须为变化做准备。
为什么要有正式的文档?
1. 首先,书面记录决策是必要的。
2. 第二,文档能够作为同其他人的沟通渠道。
3. 最后,项目经理的文档可以作为数据基础和检查列表。
不变只是愿望,变化才是永恒。普遍的做法是,选择一种方法,试试看;如果失败了,没关系,再试试别的。不管怎么样,重要的是先去尝试。
对于大多数项目,第一个开发的系统并不合用。它可能太慢、太大,而且难以使用,或者三者兼而有之。要解决所有的问题,除了重新开始以外,没有其他的办法——即开发一个更灵巧或者更好的系统。系统的丢弃和重新设计可以一步完成,也可以一块块地实现。所有大型系统的经验都显示,这是必须完成的步骤 。
因此,为舍弃而计划,无论如何,你一定要这样做。
抛弃原型概念本身就是对事实的接受——随着学习的过程更改设计。
最重要的措施是使用高级语言和自文档技术,以减少变更引起的错误。
在大型的项目中,项目经理需要有两个和三个顶级程序员作为技术轻骑兵,当工作繁忙最密集的时候,他们能急驰飞奔,解决各种问题。
只要能力允许,高层人员必须时刻做好技术和情感上的准备,以管理团队或者亲自参与开发工作。
对于一个广泛使用的程序,其维护总成本通常是开发成本的 40%或更多。
整个过程是前进两步,后退一步。
系统软件开发是减少混乱度(减少熵)的过程,所以它本身是处于亚稳态的。软件维护是提高混乱度(增加熵)的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程。
开发和维护公共的通用编程工具的效率更高。
这有两个重要的理念。首先是受控,即程序的拷贝属于经理,他可以独立地授权程序的变更。其次是使发布的进展变得正式,以及开发库(playpen)与集成、发布的正式分离。
产品的概念完整性在使它易于使用的同时,也使开发更容易进行以及 bug 更不容易产生。 关键的工作是产品定义。
Niklaus Wirth将程序开发划分成体系结构设计、设计实现和物理编码实现,每个步骤可以使用自顶向下的方法很好地实现。
好的自顶向下设计从几个方面避免了 bug。
首先,清晰的结构和表达方式更容易对需求和模块功能进行精确的描述。
其次,模块分割和模块独立性避免了系统级的 bug。
另外,细节的隐藏使结构上的缺陷更加容易识别。
第四,设计在每个精化步骤的层次上是可以测试的,所以测试可以尽早开始,并且每个步骤的重点可以放在合适的级别上。
现在的技术中最有希望的,并且解决了软件的根本而非次要问题的技术,是开发作为迭代需求过程的一部分——快速原型化系统的方法和工具。 这种开发模式对士气的推动是令人震惊的。当一个可运行系统——即使是非常简单的
系统出现时,开发人员的热情就迸发了出来。
卓越设计来自卓越的设计人员。软件开发是一个创造性的过程。完备的方法学可以培养和释放创造性的思维,但它无法孕育或激发创造性的过程。
杰出的设计人员和卓越的管理人员一样重要,他们应该得到相同的培养和回报。
如何培养杰出的设计人员?限于篇幅,不允许进行较长的介绍,但有些步骤是显而易见的:
* 尽可能早地、有系统地识别顶级的设计人员。最好的通常不是那些最有经验的人员。
* 为设计人员指派一位职业导师,负责他们技术方面的成长,仔细地为他们规划职业生涯。
* 为每个方面制订和维护一份职业计划,包括与设计大师的、经过仔细挑选的学习过程、正式的高级教育和以及短期的课程——所有这些都穿插在设计和技术领导能力的培养安排中。
* 为成长中的设计人员提供相互交流和学习的机会。
就我的经验而言,在系统工作中所遇到的大多数困难是组织结构上的一些失误征兆。试图为这些现实建模,建立同等复杂的程序,实际上是隐藏,而不是解决这些杂乱无章的情况。
《没有银弹》提出了全力解决复杂性问题的方法,这种方法可以在现实中取得十分乐观的进展。它倡导向软件系统增加必要的复杂性:
* 层次化,通过分层的模块或者对象。
* 增量化,从而系统可以持续地运行。
Coqui 也提出相似的主张:系统化软件开发方法的发展是为了解决质量问题(特别是避免大型的灾难),而不是出于生产率方面的考虑。
我们可以用任何工具写出优质或低劣的代码。除非我们给人们讲解如何设计,否则语言所起的作用非常小。结果是人们使用这种语言做出不好的设计,没有从中获得什么价值。而一旦获得的价值少,它就不会流行。
面向对象应用在整个开发周期中,但是真正的获益只有在后续开发、扩展和维护活动中才能体现出来。
W.Huang 建议用责任专家的矩阵管理来组织软件工厂,从而培养重用自身代码的日常工作习惯。
Parnas 写道: 重用是一件说起来容易,做起来难的事情。它同时需要良好的设计和文档。
对软件重用问题,我们需要研究适当的学问,了解人们如何拥有语言。一些经验教训是显而易见的:
* 人们在上下文中学习,所以我们需要出版一些复合产品的例子,而不仅仅是零部件的库。
* 人们只会记忆背诵单词。语法和语义是在上下文中,通过使用逐渐地学习。
* 人们根据语义上的分类对词汇组合规则进行分组,而不是通过比较对象子集。
现在,有可能,我们可以在软件生产率上取得逐步的进展,而不是等待不大可能到来的突破。
我们理解的也好,不理解的也好,描述都应该简短精练。
软件项目管理并不像大多数程序员起初所认为的那样,而更加类似于其他类型的管理。某种程度上,《人月神话》是关于人与团队的书。
用户所感受到的产品概念完整性是易用性中最重要的因素。
结构师负责产品所有方面的概念完整性,这些是用户能实际感受到的。结构师就像电影的导演,而经理类似于制片人。
今天,我比以往更加确信。概念完整性是产品质量的核心。拥有一位结构师是迈向概念完整性的最重要一步。
有必要使设计队伍共享一幅相同的用户图像。这需要把用户群的属性记录下来,包括:
* 他们是谁
* 他们需要(need)什么
* 他们认为自己需要(need)什么
* 他们想要(want)的是什么
软件结构师所面临的最困难问题是如何确切地平衡用户功能和易用性。
Mac 设计人员把界面固化到只读内存中,从而开发者使用这些界面比开发自己的特殊界面更容易和快速。
现在,我建议应该一块块地丢弃和重新设计系统,而不是一次性地完成替换。
瀑布模型的第二个谬误是它假设整个系统一次性地被构建,在所有的设计、大部分编码、部分单元测试完成之后,才为闭环的系统测试合并各个部分。
在开发过程“下游”的经验和想法必须跃行而上,有时会超过一个阶段,来影响“上游”的活动。 所以,在把任何东西实现成代码之前,可能要往复迭代两个或更多的体系结构-设计-实现循环。
在每个功能基本可以运行之后,我们一个接一个地精化或者重写每个模块——增量地开发(growing)整个系统。不过,我们有时的确需要修改原有的驱动回路,或者甚至是回路的模块接口。
设计类似一棵树的技巧是将那些变化可能性较小的设计决策放置在树的根部。
Harel 将原型精彩地定义成: 仅仅反映了概念模型准备过程中所做的设计决策[的一个程序版本],它并未反映受实现考虑所驱使的设计决策 。
现在,我确信信息隐藏——现在常常内建于面向对象的编程中——是唯一提高软件设计水平的途径。
彻底的进展将来自对根本困难的处理——打造和组装复杂概念性结构要素。
我们希望从面向对象编程中得到的最大收获实际上来自第一步,模块隐藏,以及预先建成的、为了重用而设计和测试的模块或者类库。
许多人希望大规模重用,但不付出构建产品级质量(通用、健壮、经过测试和文档化的)模块所需要的初始代价——这种期望是徒劳的。
警告经理们避免对进度落后的项目采取的盲目、本能的修补措施。
即对于项目的成功而言,项目人员的素质、人员的组织管理是比使用的工具或采用的技术方法更重要的因素。
“我们行业的主要问题实质上更侧重于社会学(sociological)而不是科学技术(technological)。”
如果人们认同我在文中多处提到的观点——创造力来自于个人,而不是组织架构或者开发过程,那么项目管理面对的中心问题是如何设计架构和流程,来提高而不是压制主动性和创造力。
通过权力委派,中心的权威实际上是得到了加强;从整体而言,组织机构实际上更加融洽和繁荣。
彻底提高软件健壮性和生产率的唯一途径,是提升一个级别,使用模块或者对象组合来进行程序的开发。
我们可以识别出四个层次的软件成品用户:
* 直接使用用户。
* 元程序员。在单个应用程序的基础上,使用已提供的接口来开发模板或者函数,主要为最终用户节省工作量。
* 外部功能作者,向应用程序中添加自行编制的功能。
* 元程序员,使用一个和多个特殊的应用程序,作为更大型系统的构件。
拥有能控制应用程序集合之间交互的脚本语言是非常强有力的。Unix 首先使用管道和标准的 ASCII 字符串格式提供了这种功能。今天,AppleScript 是一个非常优秀的例子。
这个复杂的行业需要:进行持续的发展;学习使用更大的要素来开发;新工具的最佳使用;经论证的管理方法的最佳应用;良好判断的自由发挥;以及能够使我们认识到自己不足和容易犯错的——上帝所赐予的谦卑。
(收起)