2025 年再读《人月神话》

软件工程的焦油坑在将来很长一段时间内会继续使人们举步维艰,无法自拔。软件系统可能是人类创造中最错综复杂的事物,只能期待人们在力所能及的或者刚刚超越力所能及的范围内进行探索和尝试。

这个复杂的行业需要:

  • 进行持续的发展;
  • 学习使用更大的要素来开发;
  • 新工具的最佳使用;
  • 经论证的工程管理方法的最佳应用;
  • 良好的自我判断以及能够使我们认识到自己的不足------谦逊的品格。

------ 《人月神话》

7月陆续读完了《人月神话-纪念典藏版》,对于这本软件工程领域的经典之作,站在当下的视角,试图对书中的观点进行分类梳理,并给出一些个人的见解。

软件研发会一帆风顺吗?

It depends.

对于 个人开发、功能单一、不需要商业化、输入可控、需求稳定无变更 的编程产品而言,软件研发有一定概率是一帆风顺的,尽管编写过程中存在各种各样的 bug、环境问题,但在前面这些限定词的条件下,解决这些只是时间和能力问题。

要留意到,这种情况下,前面加了很多限定词的,对于互联网行业的商业软件而言,当下,这些限定词会变成它们的反义词。在大型软件的开发活动中,一帆风顺是最理想的状态,但这种状态往往是凤毛麟角。

软件产品的特点

所谓特点,是指其它产品不具有或不明显,而软件产品独特具有,且关系重要的特性。我试图 从"事"与"人" 两个方面对这些特点进行整理,然而必须认识到,所谓"事"终究也是"人"提出的,所谓"人"最终也会落实在具体措施上,这两者之间并非泾渭分明,而是你中有我,我中有你。本章节内容部分来自于原书 "没有银弹" 一章。

事的特点:复杂性

规模上,软件实体比人类创造的任何其他实体都要复杂,因为没有任何两个软件、两段代码是相同的(如果相同,它们自然可以封装合并)。复杂软件系统拥有着数以千记乃至万计的状态,并且是以不可见的方式存在,这使得设计、描述和测试都非常困难。随着软件实体的扩充,复杂度以指数形式进行增长。由于复杂度,团队成员之间的沟通非常困难,导致了产品瑕疵、成本超支和进度延迟;由于复杂度,列举和理解所有可能的状态十分困难,影响了产品的可靠性;由于函数的复杂度,函数调用变得困难,导致程序难以使用;由于结构性复杂度,程序难以在不产生副作用的情况下用新函数扩充;由于结构性复杂度,造成很多安全机制状态上的不可见性。

事的特点:一致性

物理学和数学具有一致性,也就是第一性原理。而软件工程师面对的,却是随心所欲、毫无规则可言的、人为制定的惯例和系统,它们随着接口变化、随着时间迁移。很多复杂性来自于要保持接口的一致,对软件的任何设计都无法消除这种对一致性的强要求。

事的特点:指数性

世界上有这样一种工作,我们称其为 "计件工作" ,例如搬砖,假设一名工人1小时可以搬50块砖,那么一天8小时下来,他就可以搬大约400块砖,误差不会很大。

而软件开发并非如此,假设开发一个模块的时间是1人天,一个项目由4个模块组成,开发完成这4个模块的时间远远大于4人天,程序开发工作的增长量不是线性的,而是呈指数规模。一些发表的研究报告显示,指数约为1.5。这意味着,在一个具备4个模块的项目中,开发每个模块的工作量依次是1、1.5、2.25、3.375 人天,总计为 8.125 人天。这项经验来自于 IBM、Bell 实验室等多家企业和项目。

注意,这里的工作量不仅仅是指编码,同时还包含了评审、设计、撰写文档、测试、系统集成等必备环节的耗时。

事的特点:时效性

软件产品具有 时效性 ,在它们即将完成时,总面临着过时的威胁。在小步快跑、快速迭代的互联网领域更是如此。往往一个版本还未完成,下一个版本就已经开始。程序员处于 "既在解决 A 项目遗留 bug,同时在开发 B 项目,并且 C 项目的需求也开始评审和设计" 的状态,导致他无法专注设计和开发手头正在进行的 B 项目,这也为未来他开发 C 项目时,不得不抽出更多时间解决 B 项目的遗留问题埋下伏笔。

事的特点:易变性

所有的软件都会面临着变更,相比于建筑、机械等,软件是最容易发生变更的产品之一。软件产品扎根于文化的母体中,如各种应用、用户、自然及社会规律、计算机硬件等等。后者持续不断地变化着,这些变化无情地强迫着软件随之变化。

在软件开发的任何阶段,都伴随着交互设计、UI 元素的调整。人们误以为软件产品的变更是轻量和可接受的,但却忽视了,在既有设计上发生的任何修改,都倾向于破坏完整系统的架构,增加了系统的混乱程度------系统熵增。这些修改不断冲击着软件体系的内部结构边界,带来的工作量远远超过变更本身。

事的特点:连续性

屎山代码,或称祖传代码。

你可能不会居住在一幢10年前建好的房子里,但你有可能在维护或者调用一份10年前所写下的代码。软件是具有连续性的,一些类、函数极有可能存在了五六年之久,他的原作者也许早已离开了团队,即使有幸这名程序员还留在公司,并且恰好没有因为做的太优秀被领导提拔到管理岗,让他回忆起某段代码是为了解决什么问题,仍然是一件费时费力、并且不一定有结果的事。想一想吧,我们对于自己几个月之前写的代码也许都不甚了了,又如何能想起来,多年以前的一段 fix 代码是为了解决哪一种边界条件导致的问题呢?时过境迁,也许当年引起问题的场景早已面目全非。

事的特点:不可见性

通过可视化,人们可以构建出大量高效率的工具,而软件则是不可见的和无法可视化的。

软件的客观存在不具有空间的形体特征。因此,没有已有的表达方式,就像陆地海洋有地图、硅片有模片图、计算机有电路图一样。当我们试图用图形来描述软件结构时,我们发现它不仅仅包含一个,而是很多相互关联、重叠在一起的图形。这些图形可能描绘控制流程、数据流、依赖关系、时间序列、名字空间的相互关系等等。它们通常不是有较少层次的扁平结构。实际上,在上述结构上建立概念控制的一种方法是强制将关联分割,直到可以层次化一个或多个图形。

除去软件结构上的限制和简化方面的进展,软件仍然保持着无法可视化的固有特性,从而剥夺了一些具有强大功能的概念工具的构造思路。这种缺憾不仅限制了个人的设计过程,也严重地阻碍了相互之间的交流。

人的特点:独创性

尽管在任何规模的技术团队里,人们总强调协作、配合,但不得不说代码是一种没有明确"好与坏"标准的、千人千面的产物。我们设计了种种编程规范,旨在降低(扼杀)软件的独创性,使人们在阅读彼此缩写代码时不至于一头雾水。但想必你也经历过在前人祖传代码里解 bug 的情景。如同写小说一般,每个程序员都希望自己写出世界上最完美的作品,但终究人是具有个性的,每个人眼里的"完美"总有不同。

人的特点:管理与技术难以兼得

有位管理学大师曾经说过:"人总是会被推举到他所不能胜任的岗位上"。软件行业同样如此,一名软件研发工程师,随着技术不断精进,在项目里所做出的贡献也会随之增大,当他的上级认识到这个人的能力和价值,同时在组织架构中又产生了一个管理岗位的空缺时,这名工程师有极大概率会被提拔到基层管理岗,他工作的重心不得不迁移到团队的经营和管理上。

看到没有,那些有能力、技术好的工程师会逐渐脱离一线的生产工作,那些能力和技术一般的工程师,则继续对项目代码进行编写和维护。由于得不到长期有效的维护,项目代码的腐化速度会进一步加快,直至成为大家口中的"屎山代码"。"屎山"不是一个人或者几个人造成的,而是系统性的产物。

在 35 岁职业危机论调甚嚣尘上的互联网行业,人人都削尖脑袋往管理岗上面发展,似乎年纪大了后写代码就是原罪。我认为这并不是一种健康的、可持续的发展模式,未来的软件行业必须要进行一次破除、乃至革命,以消灭这种狭隘短视的论点。

小结

以上特点,是导致软件工程复杂度的根本原因,也是软件工程学科自始至终要面对和解决的重要问题。

针对其复杂度,软件工程提出的解决方法

保持概念的完整性和设计的一致性,是软件工程问题解决的核心

组建一支"外科手术队伍(The Surgical Team)"。在外科手术中,主刀医生具备不容置疑的权威性,对应到软件开发团队,这样的角色就是架构师。优秀的程序员和较差的程序员之间,生产率可能产生10倍的差异。同时,他们所交付的代码在运行速度和空间上也具有 5:1 的惊人差别。

一个系统的开发人员,应当是尽可能少。这有利于降低相互的沟通和交流成本,以及更正沟通不当所引起的不良结果。一拥而上的开发方法是高成本的、速度缓慢的、不充分的。因此,需要组建一支 小型、精干的开发队伍 ,所谓"外科手术式",是指队伍中由一个人来进行问题的分解,其他人给予他所需要的支持,以提高效率和生产力。这样的队伍中,通常有外科医生、副手、管理员、秘书、程序职员等角色。其中最核心的角色是架构师,或者"首席程序员",将亲自定义功能和性能技术说明书,进行软件方案设计,技术选型,编写第一版的架构代码。

这种划分方式,既能获得由少数头脑产生的产品完整性,又能得到多位协助人员的总体生产率,还彻底地减少了沟通的工作量。

Brooks 法则:向落后的软件项目中增加人手,只会使项目进度更加落后

人月是具有危险性质的神话,因为它暗示着人力和工作量之间是可以等比互换的。

尽管投入的人力增加了,但对项目进度并不会产生直接的帮助,反而会使状况更加恶劣。对于每个部分都必须与其他部分交互的任务而言,所增加的用于沟通的工作量可能会完全抵消对原有任务分解所产生的作用。添加更多人手实际上是延长了,而不是缩短了时间进度。

增加的成本,来自于以下三个部分:

  • 将原有任务按照新的人数分配,所造成的工作中断
  • 指导新人,讲解代码、逻辑、需求的时间
  • 不同模块之间产生的新的交流和沟通成本

强调设计和测试的重要性

创造是有趣的,而解 bug 却让人感到无比冗长和烦躁。因此,软件工程中应当对设计环节给予足够的重视。对于软件任务的进度安排,作者推荐如下基于经验的法则:

  • 1/3 计划
  • 1/6 编码
  • 1/4 构件测试和早期系统测试
  • 1/4 系统测试,所有的构件已完成

在估算工作量排期时,坚持专业的观点,避免被用户期望所左右

软件工程师常常会被产品经理对于功能紧急性的夸张描述所左右,仿佛某个功能如果不能在要求时间内上线,整个公司就会破产一样。我想说的是,尽管基于已有的 deadline 进行倒排,已经是互联网行业的常态,但软件开发负责人必须要有自己的观点,以及支撑观点、说服外界的的理由。不要因为产品认为这个功能必须在月底上线,就头脑一热盲目且乐观地接下任务。

个人建议,在需求阶段,软件工程师要尽量做到:

  • 不私下里承接需求: 由于缺失 PRD,导致无法觉察需求的细节,对开发、测试、上线等诸多环节都有影响。一旦发生事故,承接需求的开发必定背大锅,纯属好心办坏事。因此一切需求走正式的提单-审批-评估流程,相关方完成会签后,排期开发。
  • 保持技术敏锐度,避免盲目乐观: 在评估开发工作量时,如果自己对历史代码不熟悉,一定要找熟悉这一块代码的资深开发参与评估。引入一处新需求,会插入到历史的功能链路,甚至产生颠覆性的后果。在进行技术评估时,必须要具备相当的敏锐度,识别这些潜在巨大风险。同时,在给出开发评估的排期时,要辅以足够说服力的证据。例如技术架构的限制,过往项目经验等等。
  • 将程序员的时间视为有限资源: 程序员的工作并不是单纯地写代码,往往伴随着数不尽的会议、评审、紧急问题修复等等。因此,在评估工作量时,一定要适当地掺杂一些水分 。这么做并不是要留出时间偷懒,而是客观事实的限制。如果一名程序员一天的有效工作时间是 10 小时,那么,至少要留出3~4小时,用于编码外的事项。因此,在评估工作量时,就不应该以10小时作为标准。同时,在开发过程中一旦有临时需求插入,一定第一时间(至少是当天)向上汇报,临时需求会对当前手头进行中的项目产生不可避免的顺延。

开发人员交付的不仅仅是软件本身,更是用户的整体满意度

初级工程师,往往认为软件开发完成以后,,就算完成任务。但实际上并非如此,在前文中有讲过,开发时间仅仅占到总时长的 1/6。在测试阶段,软件工程师需要配合测试同事,完成专项及回归测试,对于依赖较多、链路较长的功能,开发人员必须具备指导搭建测试环境的能力,及时响应测试过程中各种边界条件的意识,更重要的是,保持良好的心态。在产品和策划、UI验收阶段,同样要考虑到验收人员的小白程度,避免不耐烦、不专业的回复。在合格上线以后,做好版本复盘、经验总结、架构内容的梳理,也至关重要,这些都会影响到相关人员、尤其是主管上级对自己的专业程度评价。

同样地,要对上述测试、验收等时间,做出足够的资源预估。

明确指明函数的副作用

这一点也许过于具体了,函数副作用带来的风险是隐蔽的,Java 语言特性决定了它可以对入参对象、类的成员变量、全局变量进行任意程度的修改。尽管全局状态变量并不是优雅的编程实践,但在特定业务场景里,它仍然是权衡成本和收益以后,最适合的实现方式。因此,有必要针对全局变量设计完备的状态管理类,同时,给予足够的日志设计,排查问题的 SOP,这也是防御式编程的一种。

采用"自顶向下、逐步细化"的软件开发方法

这是 Niklaus Wirth 在 1971 年的一篇论文中提到的方法,从要得到的主要结果开始,步骤如下:

  1. 勾画出能得到结果但是比较粗略的任务定义和大概的解决方法。
  2. 对该定义和方案进行细致的检查,以判断结果与期望之间的差距。
  3. 将上述步骤的解决方案,在更细的步骤中进行分解,每一项任务定义的精化变成了解决方案中算法的精化,后者可能还伴随着数据表达方式的精化。
  4. 在整个过程中,当时别处解决方案或者数据的模块(module)时,对这些模块的进一步细化可以和其他的工作独立。

在以上每个步骤中,尽可能使用级别较高的表达方法来表现概念和隐藏细节。这种自顶向下设计在以下方面避免了更多 bug 的产生:

  • 首先,清晰的结构和表达方式,更容易对需求和模块功能进行精确的描述。
  • 其次,模块分割和模块独立性避免了系统级的bug。
  • 另外,细节的隐藏使结构上的缺陷更容易识别。
  • 第四,设计在每个精化步骤的层次上,是可以测试的,所以测试可以尽早开始。

项目进度管理与风险识别

比起重大灾难,一天一天的进度落后,更难以识别、更不容易防范和更加难以弥补。因此,需要在项目启动前,就设计好一个严格的进度控制表。它由 里程碑日期 组成,里程碑必须是具体的、特定的、可度量的事件,能够对其进行清晰的定义。

甘特图是一种很好的可视化的项目进度管理工具。


书中其它一些有意思的观点

第二个系统是人们所涉及的最危险的系统

通常,在有惊无险地完成第一个系统的设计后,人们容易陷入一种盲目自大的情结,在第二个系统的设计中,会产生过分设计的倾向。一名有经验的开发人员应当具备足够冷静的心态,及时识别,避免在错误的道路上越走越远。尤其是在当下互联网瞬息万变的环境中,一个当下是重点的功能,也许下个季度就成了明日黄花。为它所做的设计,并不能起到预期的效果。

至少,在为可扩展和维护性进行深度设计之前,你应当与经理就软件未来的走向,进行一次深入的交流。由于双方所处的位置不同,所吸收到的信息也必然存在不对称的情况。开发人员往往只从技术实现难度、维护成本进行考虑,而站在经理的角度,产品方向、公司财务状况、未来人员规划,都会影响到技术方案的设计。多进行向上沟通,这是远远比埋头钻研技术更重要的事。

缺陷修复会以 (20-50)% 的概率引入新的缺陷

确实是软件开发过程中会发生的真实场景,对已经完成代码的任何改动,都会影响到最初设计的完整性和统一性。当缺陷触及到主要业务流程时,开发人员应当有意识地评估好影响面。改旧 bug 结果引入新 bug,这种情形在所难免,我们要做的就是尽可能降低它们所发生的概率。同时,在每次 bugfix 之后,必须重新运行先前所有的测试用例,从而确保系统不会以更隐蔽的方式被破坏。

尾声

《人月神话》并不是一本教你写代码的书,它更多描绘了在越来越大的软件项目体系下,人与团队如何以高效的方式协作达成目标的问题。任何需要多人协作完成的项目,都会遇到一个特别的困难,就是它必须有多人参与设计和开发,但是,在概念上,却要保持为单个人员使用的一致性。书中介绍的大部分项目管理思想和工具,都是为了这种概念的一致性、整体性而服务的。并且,对项目而言,人就是一切。项目人员的素质、人员的组织管理是比使用的工具或采用的技术方法更重要的因素。

最后,软件工程的焦油坑在可见的未来中,仍然会使程序员在内的群体举步维艰,无法自拔。软件系统也许是人类历史中所创造的最错综复杂的事物,这个行业需要持之以恒的学习和思考。发现问题,探寻方法,解决问题,保持谦卑。在不断螺旋上升的过程中,提升团队、个人的战斗力。

参考资料

  • 《人月神话 纪念典藏版》
相关推荐
PetterHillWater1 天前
AI促进软件研发管理案例
aigc·团队管理
用户6120414922134 天前
C语言做的单词背诵测试器
c语言·后端·敏捷开发
无责任此方_修行中4 天前
当“中国责任心”遇上“瑞典自由风”:一次跨国团队的破冰之旅
程序员·团队管理·午夜话题
却尘4 天前
💀 Git 考古灭迹术:让代码"从未存在过"的禁忌技法
git·github·敏捷开发
用户6120414922135 天前
C语言做的电子时钟带闹钟带倒计时
c语言·后端·敏捷开发
用户6120414922136 天前
C语言做的飞机大战游戏(控制台版)
c语言·后端·敏捷开发
老实巴交的麻匪10 天前
(三)学习、实践、理解 CI/CD 与 DevOps:声明式 API,Docker Compose 容器编排
运维·敏捷开发·自动化运维
用户61204149221313 天前
C语言做的班级投票系统
c语言·敏捷开发
老实巴交的麻匪19 天前
(一)学习、实践、理解 CICD 与 DevOps
运维·敏捷开发·自动化运维