谈软件巨著《人月神话》-大型软件项目最佳实践|系列二(人月神话 人件)
最近做了一个超大项目重构,其中对项目的管理也产生了非常多的问题,进行了深度项目的复盘之后,再回首去看软件巨著《人月神话》体感更加丰富收获非常多,故产生了此系列文章。
本系列文是软件巨著《人月神话》读书笔记,全系列分为三部分,会逐步介绍。
本文讲述的是作者在大型软件项目中一些重要的实践,主要有
1. 团队划分,外科手术式队伍
大型项目的经验显示:一拥而上的开发方法是高成本、速度缓慢、低效的。成本的主要组成部分是相互的沟通和交流,以及更正沟通不当所引起的不良结果(系统调试),需要协作沟通的人员的数量影响着开发成本。
不同人或者不同团队之间效率差异会有很大,书中提到Sackman,Erikson和Grant曾对有经验的程序员进行测量,发现最好的和最差的表现在生产率上有10:1的差异。
从 1963 年到 1966 年,作者项目设计、编码和文档工作花费了大约 5000 人年。如果人月可以等量置换的话,譬如 10 人队伍。 作为一个尺度, 假设他们都非常厉害,比一般的编程人员在编程和文档方面的生产率高 7 倍。 假定 OS/360 原有开发人员是一些平庸的编程人员(这与实际的情况相差很远)。 同样,假设另一个生产率的改进因子提高了 7 倍, 因为较小的队伍所需较少的沟通和交流。 那么, 5000/(10× 7× 7) = 10,他们需要 10 年来完成 5000 人年的工作。一个产品在最初设计的 10 年后才出现,还有人会对它感兴趣吗?或者它是否会随着软件开发技术的快速进步,而显得过时呢?
总之,团队的规模太大,带来的沟通成本也随之升高,总体效率随之降低;团队规模太小,可能单位效率提升了,但是总体进度太慢了,无法及时完成真正有用的产品。
如何解决上面两难的问题呢,Harlan Mills 的提议提供了一个崭新的、创造性的解决方案 。 Mills 建议大型项目的每一个部分由一个团队解决,但是该队伍以类似外科手术的方式组建,而并非一拥而上,我们叫做“外科手术队伍”
外科手术队伍核心设计:同每个成员截取问题某个部分的做法相反, 由一个人来进行问题的分解, 其他人给予他所需要的支持,以提高效率和生产力。
团队阵容以一个主程序员为核心,配合9名其他专业人员形成一个团队,如下图所示:
- 首席程序员:决策一切并实际动手编程,亲自定义功能和性能技术说明书,设计程序,编制源代码,测试以及书写技术文档,首席程序员需要极高的天分、 十年的经验和应用数学、业务数据处理或其他方面的大量系统和应用知识
- 副手:作为后备,能完成任何一部分工作,但是相对具有较少的经验。他的主要作用是作为设计的思考者、 讨论者和评估人员。他需要详细了解所有的代码, 研究设计策略的备选方案。 显然, 他充当外科医生的保险机制。 他甚至可能编制代码,但针对代码的任何部分,不承担具体的开发职责。
- 管理员:外科医生是老板,他必须在人员、加薪等方面具有决定权,但他决不能在这些事务上浪费任何时间。 因而, 他需要一个控制财务、 人员、 工作地点安排和机器的专业管理人员, 该管理员充当与组织中其他管理机构的接口。
- 编辑:外科医生负责产生文档——出于最大清晰度的考虑,他必须书写文档。对内部描述和外部描述都是如此。 而编辑根据外科医生的草稿或者口述的手稿, 进行分析和重新组织,提供各种参考信息和书目,对多个版本进行维护以及监督文档生成的机制。
- 两个文秘:管理员和编辑每个人需要一个秘书,配合管理员和编辑共同工作。
- 程序职员:他负责维护编程产品库中所有团队的技术记录。该职员接受秘书性质的培训,承担机器码文件和可读文件的相关管理责任
- 工具维护人员:保证所有基本服务的可靠性, 以及承担团队成员所需要的特殊工具(特别是交互式计算机服务) 的构建、 维护和升级责任。
- 测试人员:负责编写测试用例,搭建测试环境并测试。
- 语言专家:寻找一种简洁、 有效的使用语言的方法来解决复杂、 晦涩或者棘手的问题。
传统的团队:将工作进行划分,每人负责一部分工作的设计和实现;
外科手术团队:实际设计完全由外科医生完成,其他人负责填充实现细节与提供必要的帮助。
外科手术式队伍的好处在于:
- 减少沟通的成本,在传统的队伍中,大家是平等的,出现观点差异的时候,不可避免的需要讨论和妥协,在外科式队伍中,不存在利益的差别,也不存在因为意见不合带来的策略和系统接口上的不一致,观点不一致由外科医生单方面来统一。
- 保证概念完整性,系统的设计和程序代码都由外科医生主要负责,保障了从设计到实现上的概念完整性。
如果整个工作能控制在范围之内, 10 人的团队无论如何组织, 总是比较高效的。但是, 当我们需要面对几百人参与的大型任务时, 如何应用外科手术团队的概念呢?
首先整个系统必须具备概念上的完整性, 要有一个系统结构师从上至下地进行所有的设计。 要使工作易于管理, 必须清晰地划分体系结构设计和实现之间的界线, 系统结构师必须一丝不苟地专注于体系结构。
然后需要使用分解的技术,分解到一个个独立部分,每个部分由一个外科手术团队保障概念完整性。项目整体的沟通只需要再外科医生之间进行沟通和讨论,大大降低成本。
2. 保持概念完整性,贵族专制
绝大多数欧洲的大教堂中,由不同时代、不同建筑师所建造的各个部分之间,在设计或结构风格上都存在着许多差异。 后来的建筑师总是试图在原有建筑师的基础上有所“ 提高”,以反映他们在设计风格和个人品味上的改变。所以, 在雄伟的哥特式的教堂上,依附着祥和的诺曼第风格十字架,它在显示上帝荣耀的同时,展示了同样属于建筑师的骄傲。
与之对应的是, 法国城市兰斯(Reims) 在建筑风格上的一致性和上面所说的哥特式大教堂形成了鲜明的对比。 设计的一致性和那些独到之处一样, 同样让人们赞叹和喜悦。 如同旅游指南所述, 风格的一致和完整性来自 8 代拥有自我约束和牺牲精神的建筑师们, 他们每一个人牺牲了自己的一些创意, 以获得纯粹的设计。 同样, 这不仅显示了上帝的荣耀, 同时也体现了他拯救那些沉醉在自我骄傲中的人们的力量。
对于计算机系统而言,尽管它们通常没有花费几个世纪的时间来构建,但绝大多数系统体现出的概念差异和不一致性远远超过欧洲的大教堂。 这通常并不是因为它由不同的设计师们开发,而是由于设计被分成了由若干人完成的若干任务。
作者主张在系统设计中,概念完整性应该是最重要的考虑因素。也就是说为了反映一系列连贯的设计思路,宁可省略一些不规则的特性和改进,也不提倡独立和无法整合的系统,哪怕它们其实包含着许多很好的设计。
短小精干的队伍带来了新的问题,如何对大项目进行合理的分割?
这依赖于一个很重要的前提:概念完整性。系统设计中,概念完整性是最重要的考虑因素。概念的完整性的确要求系统只反映唯一的设计理念,用户所见的技术说明来自少数人的思想。
如何获得系统的完整性? 答案是贵族专制,设计必须由一个人或非常少数互有默契的人员来实现并理清边界,然后据此分割执行。对于非常大型的项目, 将设计方法、 体系结构方面的工作与具体实现相分离是获得概念完整性的强有力方法。” [同样适用于小型项目 ]
3. 避免画蛇添足,要自律
在开发第一个系统时,结构师倾向于精炼和简洁。他知道自己对正在进行的任务不够了解,所以他会谨慎仔细地工作。同时他对会不断产生的装饰和润色功能,把这些功能都搁置在一边,作为下一个项目的内容。第一个项目迟早会结束,而此时的结构师,对这类系统充满了十足的信心,熟练掌握了相应的知识,并且时刻准备开发第二个系统。
第二个系统是设计师们所设计的最危险的系统,一种普通倾向是过分地设计第二个系统,向系统添加更多修饰功能和想法,它们曾在第一个系统中被小心谨慎地推迟了。如果如同ovid所述,是一个”大馅饼“。例如,后来被嵌入到7090的IBM709系统,709是非常成功和简洁的704系统进行升级的二次开发项目,709的操作集合被设计的如此丰富和充沛,以至于只有一般操作被常规使用。
所以书中提到系统设计师们,在系统设计时,需要对项目有节制,有些东西可以不添加,不要过度设计,不要过度自信,保持警觉,确保初始的概念和目标的充分体现,而不是让一些次要功能喧宾夺主。
4. 概念完整性贯彻,一些方法
概念的完整性不仅仅要在专制的贵族和系统设计师这一层面上充分理解并传达,在系统的实现人员中,也要充分传达。在不理解业务背景的情况下,开发者也很难写出优秀的代码。
作者在这一章介绍了很多方法达到之一目的,包括规格说明手册,形式化定义(如原型图)、会议和大会、多重实现、电话日志、产品测试。当然这些方法在如今可能已经过时了或有更好的方式,但是通过种种方式保证软件开发的进度不偏离最初的概念和目标,是实现项目成功的重要保障。实际上在当代的软件开发中,也有一些措施来实现这一目标,比如各种项目管理工具,版本化文档工具等。
5. 沟通,巴比伦塔失败的教训
据《创世纪》记载,巴比伦塔是人类继诺亚方舟之后的第二大工程壮举,但巴比伦塔同时也是第一个彻底失败的工程。
文中以巴比伦塔建设失败的神话故事引出了”沟通“的重要性。当上帝消除了统一的语言,人们失去了交流的手段,即使其他条件都能满足,巴比伦塔也以失败告终。
文中他们分析了必要的有利条件都已经具备,但还缺乏两个方面——交流, 以及交流的结果——组织。他们无法相互交谈, 从而无法合作。 当合作无法进行时, 工作陷入了停顿。 通过史书的字里行间, 我们推测交流的缺乏导致了争辩、 沮丧和群体猜忌。很快,部落开始分裂——大家选择了孤立,而不是互相争吵。
沟通时确保团队思想统一、目标一致的最重要手段。那么,如何进行有效的沟通呢:
- 时常保持非正式的交流沟通。比如面对面口头的交流或者电话等过远程交流,不论哪种形式,交流的内容都应形成文字记录。
- 常规性项目会议。关键成员定期组织项目例会,讨论主题顺明确,结论有效,会议内容形成文字记录。
- 项目必须拥有共享的工作手册。这是一套文档的集合,在在项目初期就确定各个文档的结构,然后在项目过程中不断的填写细化完善,每个版本应该拥有明确的时间记录和变更说明。
- 文档其实是高效沟通的一种有效方式。准备文档时,需要要仔细的核对实际情况,谨慎的措辞;写文档时,各种问题和讨论都会浮出水面,必须进行决定;写完文档后,所有的想法和决定都都以文字的方式确定并记录,对每个人的思想进行统一。文档能够使各项计划和决定在整个团队范围内得到交流。
项目经理的主要日常工作就是沟通,而不是做出决定。他的职责是使每个人都向着相同的方向前进。
即使再三强调沟通的重要性,但是由于主观或者客观的原因,在实际项目过程中还是会发现沟通不足导致的问题。书中也提出了几种方式如何减少沟通也能达到有效协作的目标:
- 每个人不是必须关注所有的内容,各个部分应该只暴露对外表现,而内部实现应该封装不对外暴露。这段内容体现到代码上就是:面向接口编程,不要依赖实现逻辑。
- 小而精的团队搭配,保持一个首席程序员完成所有的设计和代码偏写工作,其他人进行辅助配合,确保概念的完整性和一致性,减少沟通。
- 团队协作就要形成组织结构,团队组织的目的是为了减少必要的交流和协作量,人力划分和职责范围必须明确并且清晰。团队中禁止双重领导,团队是树状结构,交流是网状结构,需要补充特殊组织机制来克服树状组织结构中缺乏交流的困难。
团队组织的目标是为了减少必要的交流和协作量,为了减少交流,组织结构包括了人力划分(division of labor) 和限定职责范围(specialization of function)。
传统的树状组织结构反映了权力的结构原理——不允许双重领导,组织中的交流是网状, 而不是树状结构, 因而所有的特殊组织机制(往往体现成组织结构图中的虚线部分)都是为了进行调整,以克服树状组织结构中交流缺乏的困难。
6. 胸有成竹,工作量的预估
系统编程需要花费多长的时间?需要多少工作量?如何进行估计?
先前他推荐了合理的软件进度安排应该遵循:计划进度(1/3)、编码(1/4)、构建测试(1/4)和系统测试(1/4)。仅仅通过对编码部分的估计,然后应用上述比率对整个任务进行估计是错误的。编码大约之战问题的1/6左右,编码估计或者比率的错误可能导致不合理荒谬结果。
第二,必须声明的是,构建独立小型程序的数据不适用于编程系统产品。就好像把 100 码短跑记录外推, 得出人类可以在 3分钟之内跑完 1 英里的结论一样。
作者当时编程量化方法将程序员的生产效率细化到指令/年(他们当年写的是汇编,高级语言那个时候还很少)。这个实际意义不大,但仍然是一个有趣的参考,最终的结论是: 工作量=常数*指令数量^1.5,也就是说工作时长跟代码数量是指数关系而不是线性关系,随着代码数量的增长,工作时长是指数级上升的。
然后举例说一些关于编程人员生产率的研究,提出的一些估计技术。生产率会根据任务本身复杂度和困难程度表现出显著差异。无法用代码量来预估排期,使用适当的高级语言,编程的生成效率可以提高5倍。
7. 减少程序空间,削足适履
这一章主要是讲程序大小的,包括代码大小和内存占用的大小。代码大小现在应该已经不是软件开发的重要考量了吧? 但是这一问题在如今的App开发中应该也有,虽然很多公司毫不在乎占用用户几百M的存储空间……当然在前端还存在另外一个问题,即过大的代码包会耗费过多的流量,尤其是在早年的移动端开发中,那时候流量很金贵,现在这样的顾虑逐渐变少了,但是一直都存在的顾虑是代码的加载速度,当然这一问题也可以通过分包懒加载来解决了。总而言之,当年的那些经验在前端领域适用的已经不多了,但是在客户端领域可能仍然适用。
内存当然始终是一个问题,但是应该主要考虑不要发生内存泄露的问题,自动的垃圾收集可以解决大部分问题。计算机硬件的进步真的极大地降低了软件工程的难度。
8. 项目文档的重要性,提纲挈领
这一章主要是介绍文档的重要性,对于项目经理来说,文档是很重要的,它包含了项目目标、产品的技术说明、时间、资金预算、工作空间的分配和人员的组织结构。
- 文档的重要性主要体现在: 明确的书面记录会让分歧更明朗,使混沌的状态变得清晰、明确。
- 文档降低了沟通的负担。
- 文档便于项目经理跟踪项目的进度状态。
9. 唯一不变的是变化,未雨绸缪
软件领域的名言:“唯一不变的就是变化本身”,潜台词就是“其他都会发生变化”
变化意味着不可预料,不可提前准备,准备好的可能徒劳、不可控,变化的情况有很多,例如:
- 用户不会在初期提供明确的需求,用户的实际需要和用户的感觉会随着程序的构建、测试和使用而变化。
- 软件开发和运行过程中的环境不一定一致,甚至运行环境本身也在变化。
- 设计人员在软件开发完成之后才能意识到设计上的缺陷,从而视图在下一个版本中弥补
- 团队成员由于各种因素(私人的或者公司的)变化,甚至核心成员发生变化可能直接导致软件推翻重来。
系统必然会面临各种变化,你开发的软件必然会在修修补补中变得面目全非,最初的设计必须在各种妥协中打上各种丑陋的补丁。无论是多么良好设计的系统,都会走向混乱,区别只是这个过程的快慢而已。因此,好的设计会让这个过程尽可能的慢,尽可能地不那么痛苦,我们能做的就是眼光尽量放长远,让我们的代码尽可能地具有高可扩展性并且易于维护。而且,在面对不得不进行重构时,做好心里准备。
一些Bug有趣的数据,对于一个广泛使用的程序,其维护总成本通常是开发成本的 40%或更多。令人吃惊的是,该成本受用户数目的严重影响。用户越多,所发现的错误也越多。
起初,上一个版本中被发现和修复的 bug,在新的版本中仍会出现。 新版本中的新功能会产生新的 bug。 解决了这些问题之后, 程序会正常运行几个月。接着, 错误率会重新攀升。 Campbell 认为这是因为用户的使用到达了新的熟练水平,他们开始运用新的功能。这种高强度的考验查出了新功能中很多不易察觉的问题。
程序维护中的一个基本问题是——缺陷修复总会以(20- 50) %的机率引入新的 bug,所以整个过程是前进两步,后退一步。
系统软件开发是减少混乱度(减少熵)的过程,所以它本身是处于亚稳态的。软件维护是提高混乱度(增加熵) 的过程, 即使是最熟练的软件维护工作, 也只是放缓了系统退化到非稳态的进程。
10. 提升开发效率的工具,干将莫邪
巧匠因为他的工作而出名(工匠精神)。
好的开发团队自然要有自己一套比较完好的开发工具和开发环境,用来提供生产力和生产效率。
本章主要向我们传达的意思是: 工具对于软件产业的重要性,工具需要做到统一,需要有专人进行维护。
11. 如何让整体正常运行,整体部分
当每个功能模块开发、测试完成,开始多个模块之间进行集成测试;或者一个子系统开发测试完成,开始多个子系统进行集成测试。单个模块独立运行对最终用户根本没有意义,所有模块配合到一起形成完整的业务链路才能作为交付物。
这时的人们都乐观地认为"总算都开发完了,集成调试一下就都搞定了"。其实,这才是痛苦的开始。各种之前不相关的人和资源需要协调,这些人互相属于不同的团队,团队之间职责划分不清晰,这些都开始极度考验每个团队负责人的水平–"开始推卸责任":因为模块与模块之间的适配部分没有人负责开发实现,每个团队都理所当然的认为这些公共部分是对方负责的。
即使各个模块独立运行正确,也不一定能够集成。事实上,预先定义的集成标准并不是完美的、严格的。这就导致不同团队对标准的理解有差异,实现出来的模块总会有差异,这些差异只有在集成的过程中才能发现。每个模块都认为自己的符合标准的,对方需要修改才能按照标准进行集成。
这些技术问题相对还是容易解决的,更麻烦的是各个系统是不同的团队或者组织开发的,系统间进行集成就必须同时协调多方组织的人员。在权力不能到达的情况下,协调多方利益并不完全一致的组织简直是时间杀手。
书中也指出:"系统调试(相对于单元测试)所花费的时间会比预料的更长"。针对这个问题,我在书中没有找到针对性的解决方案,最好是早集成、早暴露、早解决。
同时我们可以在设计系统结构时精心设计,减少各个部分间的耦合,各个模块的独立性越高,系统级的bug的可能性就越低。重构过的模块在合进整体的时候仍然需要对系统整体进行测试,而不是仅仅测试重构过的模块。如自上而下的设计、构件单元调试等。
12. 进度管理和监控的方法–防止祸起萧墙
在我们的工作经验中,很多大型软件项目都会比预期延后,在事后的总结中却又并不能发现重大的失误,那么,到底是什么导致了项目的整体落后呢?
书中提到:"一天一天的进度落后比重大灾难更难以识别,不易防范,更加难以弥补"。确实是这样的,某天关键成员请假了,某天需要的环境没有到位,某天公司的网络突然换掉了,这些隐藏在日常过程中的小事,一天一天的把进度延后了。而且没有人能够及时发觉,在发觉进度延后之后,也会下意识的认为:明天就好了,加加班就赶回来了。慢性的进度偏离绝对是士气杀手。没有人总结,没有人汇报,也就没有人解决,时间就这样一天天的过去了,直到项目到期时才发觉:"时间都去哪了"?
隐藏偏离是人的本性,每个人都不希望将自己的缺点暴露在人前,软件开发人员更倾向自己闷头钻研而不善于求助他人,更希望自己将问题搞定而没有发觉进度已经延后。作为项目管理里者不能奢望团队成员都能如实的、及时的汇报工作进展。项目管理者需要通过各种方式维护一种信任、平等、合作、互助的工作氛围,逐渐的引导或者影响开发人员将实际情况表达出来。
项目经理必须使用严格的进度表来控制项目,进度表由里程碑和日期组成。每个里程碑必须是具体的、特定的、可度量的事件,能够进行清晰定义。项目经理积极的跟进每项工作进展才可能尽量准确的了解项目的真实进展,从而进行资源的调配。
项目进度滞后往往如温水煮青蛙一样让我们难以应付,最重要的就是要防微杜渐。
13. 提供面向用户的文档,另外一面
程序向用户所呈现的面貌(界面、文档、注释)与提供给机器识别的内容(代码)同样重要。不管是独立程序还是系统软件,都应该编写注释,因为人会遗忘的。
注释需要描述事情如何,还应描述为什么,注释信息应该以段落的方式向程序中插入必要的记述生文字,同时可以借助源代码的固定格式来附件更多的文档信息关提升可读性。
除了面向开发者的项目文档和说明之外,软件开发也需要编写详勿的面向用户的文档,文档需要包含软件的目的、运行环境、转输入输出范围、实现的功能和使用的算法、输入输出的格式、排操作指令、选项、运行时间、输出结果的精度和校验方式等等。
除此之外,还需要充分的测试用例来说明程序的功能和边界。