跳转到内容

没有人建造了它

没有人会以“十年后让系统影响范围几乎无法预测”为目标开始一个软件项目。模糊职责、跨越很远的依赖关系,或者只有少数资深开发者还能理解的区域,从来不会作为目标架构写进设计文档。

项目开始时通常有更合理的东西:一个问题、一个想法、一个客户、一段市场机会、一个 Deadline,或者一个技术难度很高的 Proof of Concept。人们只是想先让某件事情工作起来。

上一篇文章讨论了 Big Ball of Mud 到底是什么:它不是 Monolith、Legacy Software 或 Technical Debt 的同义词,而是一种结构状态——系统原本设计或表面可见的架构,越来越无法可靠解释系统实际如何组织。由此几乎自然会出现下一问:如果没有人主动追求这种状态,系统为什么还是会走到这里?

最简单的解释,是讲一个“差开发者、短视 Management、缺少架构能力”的故事。

更有意思的解释要困难得多:

Big Ball of Mud 很少是被有意识建造出来的。它可以在多年中,由大量在当时局部上下文里合理、有用甚至成功的决策逐步形成。

先做一个编辑上的说明:这一篇比系列前几篇长。不是因为答案需要人为复杂化,而是因为寻找“唯一原因”本身很容易误导。进入 Big Ball of Mud 的路径并不只有一条。有的系统在多年中因为不断叠加局部合理决策而缓慢侵蚀;有的系统一开始就以问题状态被新团队接手,之后又在完全不同的合同和组织条件下继续发展;还有的系统从一个成功 Prototype 演化成长期产品,却从未真正完成从探索性代码到长期系统的结构转换。

这些路径很不同,但它们共同指向一个问题:谁在时间维度上真正拥有整个系统的结构责任,而且这种责任是否真的有能力产生效果?

Brian Foote 和 Joseph Yoder 在描述 Big Ball of Mud 时,并非偶然从 Throwaway CodePermanent Prototype 讲起。

Prototype 的目标与长期产品不同。它要回答的是:这个想法能不能成立?算法可不可行?某个技术难题是否可解?某种 Concept 能不能演示?

为了回答这些问题,Prototype 可以做出一个正式产品不一定应该做的假设。它可以把知识直接写进代码,跳过抽象,甚至采用长期可维护性并不重要的结构。这不是错误,而是 Exploration 的目的。

真正有意思的是 Prototype 成功之后。Demo 说服了别人,客户产生兴趣,有人看到了市场机会,于是那份代码突然拥有了经济价值。里面已经沉淀了知识、开发时间和一个能够工作的解决方案。

Foote 和 Yoder 描述的危险正是在这里:Throwaway Code 没有被丢掉,因为另一条路看起来意味着“把一个已经工作的东西再做一遍”。他们并不是说 Prototype 必然产生坏产品。很多时候,在团队真正理解 Domain 之前,探索本来就是必要的。问题只发生在:从 Experiment 进入长期产品的那一刻,结构没有随系统身份一起升级。

Proof of Concept 的成功,可能恰恰成为它再也不会被丢掉的原因。 成功会保存早期决策。

我自己也经历过类似故事。起点是一项科学和技术上都非常困难的想法。一位非常优秀的科学家解决了核心难题,并证明了技术可行性,这项工作本身令人印象深刻。随后,一位企业家看到了它的商业潜力,而且这个判断也是正确的。于是 Proof of Concept 变成了产品。

在哪一刻应该说:“Big Ball of Mud 从这里开始”?

至少不是这里。

当 Prototype 变成成功产品,环境就改变了。客户在等 Feature,Incident 必须解决,合同里有日期,团队也许还没有掌握一个更合适的新技术方案。一个“真正干净”的解决方案可能要求先重构现有结构,而一个 Workaround 今天就可以完成。

在这样的条件下,Workaround 完全可能是正确决定。

软件开发不能只为假想未来优化。公司必须交付,用户需要可用产品,一个结构完美但错过市场窗口的方案,在经济上可能更差。Architectural Technical Debt 研究也反复指出这种双重性:某个架构决定在当时已知约束下完全可以成立,直到几年后才成为进一步演进的障碍。

所以,问题不是 Pragmatism。

更重要的问题是:如果局部优化长期没有足够强的结构反馈,会发生什么?一次捷径不会产生 Big Ball of Mud,一个 Workaround 也不会。甚至主动接受 Technical Debt 都可以是理性的投资决定。

真正危险的是成千上万条从未再被撤回的捷径,最终自己变成了架构。

没有被撤回的东西,最终会成为架构

Section titled “没有被撤回的东西,最终会成为架构”

第一个 Sonderfall 通常仍然很明显是 Sonderfall。也许旁边甚至写着:

TODO: Release 后整合。

但 Release 之后是下一个 Feature,再之后是下一个客户,随后又是下一个 Deadline。与此同时,另一个修改已经开始依赖这条特殊逻辑,甚至第二个团队已经把新的行为建立在它之上。

于是,“暂时这样做”逐步变成“我们这里就是这么工作的”,最后变成“我们一直都是这么做的”。

这正是 Architecture Erosion 的关键机制之一:

架构不仅由我们主动设计的东西组成,也由那些多年后从未被撤回的临时决定组成。

Foote 和 Yoder 的 Piecemeal Growth 并没有把渐进式开发描述成错误。活着的软件本来就必须一边运行,一边被一点点扩展。真正的风险在于:紧急的局部要求不断挤掉长期架构诉求。因此他们把 Expansion 与持续出现的 Consolidation Phase、Refactoring 和 Repair 放在一起讨论。

并不是每个 Feature 之后都需要“大扫除”。但长期系统必须拥有某种机制,让内部结构能够回应新学到的东西。需求会变化,团队会学习,技术平台会发展,旧假设会被证明错误,业务边界也可能移动。如果系统周围的一切都在演进,而内部结构永远只追加、不重新评价,那么它会越来越像过去的沉积物。

Deadline 不会自动摧毁架构,但它会改变某个时刻什么方案看起来合理。

在 Operational Pressure 下,团队关注的时间跨度会缩短。问题不再是“这个区域三年后应该是什么结构”,而变成“什么东西星期五之前能可靠上线”。这完全可能合理。

关于软件工程中的时间压力,研究也并没有给出“压力越大软件必然越差”这种简单公式。Kuutila、Mäntylä、Farooq 和 Claes 的系统性综述强调,压力的类型、强度和上下文都会影响结果。问题在于,当短期压力长期存在时,组织可能一直在以“特殊情况”的决策模式工作。

如果每个 Sprint 都是例外,每次 Release 都是紧急状态,那么短期优化就不再是例外,而开始成为默认架构机制。

这会让团队不断把结构性工作推迟到一个永远不会到来的“稍后”。

偶尔为了 Deadline 做妥协是 Pragmatism。长期没有任何 Consolidation,则会让 Pragmatism 变成结构。

一个软件系统跨越多年可能经历的演进:成功的 Proof of Concept 逐渐成为产品,局部方案持续响应新需求与时间压力,结构整合一再被推迟,内部结构也逐步失去清晰度。

一个 Big Ball of Mud 不一定始于一次糟糕决定。真正重要的是,大量局部决定如何随着时间共同作用。

一个系统能否持续抵抗局部优化,不只取决于开发者是否“在乎架构”。

必须有人长期代表系统结构:提出 Boundary 问题,观察 Dependency,保留技术方向,推动重要 Refactoring,并能够在“局部最快”和“长期可变更”之间产生真实影响。

这不一定是一个叫“Architect”的职位。可以是 Tech Lead、Staff Engineer、一个稳定团队,或者通过明确规则和 Review 共同承担的机制。

关键不是头衔,而是 Continuity 和 Mandate。

如果这份责任每几个月都换人,或者只有建议权、没有实际影响力,那么结构很容易在一系列局部交付中被消耗。

架构责任必须比下一张 Ticket 活得更久。

否则没人真正拥有“系统五年后还是否能被理解”这个问题。

一个领域里的卓越,并不能替代另一个领域

Section titled “一个领域里的卓越,并不能替代另一个领域”

成功产品经常来自非常强的专业能力。

一位优秀科学家可以解决极难算法问题,一位优秀业务专家可以精确理解 Domain,一位非常强的 Backend 工程师可以设计高性能服务,一位资深项目经理可以让大型项目按时交付。

这些能力都很重要。

但它们不能互相自动替代。

擅长算法不自动意味着擅长长期软件架构;擅长 Delivery 不自动意味着能够判断 Dependency Structure;熟悉业务并不意味着天然会设计可演进的模型;资深 Backend 工程师进入 Frontend,也需要理解另一套 Runtime 和 Reactive Model。

组织的问题往往不是缺少聪明的人。

而是默认“某种技术卓越会自动覆盖另一种专业能力”。

如果一个系统长期缺少真正承担结构责任的人,再优秀的局部实现也可能不断积累成整体失配。

架构侵蚀的另一个来源,是系统越来越依赖自己的替代机制。

Framework 提供 Router,团队却建立另一个 Routing Layer。Forms 有自己的 State Model,团队又维护第二套 Validation State。Runtime 已经提供 Change Detection 或 Reactive Primitive,系统却通过手工同步保证界面一致。平台原本提供的能力被一层层“更可控”的内部方案覆盖。

每个 Workaround 单独看都可能有理由。

但一旦这些方案积累起来,系统真正运行的模型就越来越不再是 Framework 模型,而是组织自己多年形成的一套隐式规则。

这会让 Upgrade、Onboarding、Debugging 和未来迁移越来越贵。

一个 Framework 不是因为版本旧才会成为 Legacy。一个团队也可以在最新版本里长期与 Framework 的基本范式对抗。

长期系统有一个重要的不对称性:人的任期通常比代码短。

开发者会换团队,Product Owner 会变化,架构师会离开,公司会重组,供应商会更换。

但过去的技术决定仍然存在。

Martin Robillard 对 Turnover-Induced Knowledge Loss 的研究关注的正是这种现象:离职不仅减少人力,也会带走那些没有被代码、文档、测试或明确结构保存下来的经验知识。

如果一个系统高度依赖“知道为什么这里不能改”的历史记忆,那么人员流动会逐步把 Architecture Rationale 从显式知识变成考古学。

新团队看到的是结果,而不是当时约束。

他们可能会合理地延续旧结构,因为不知道哪些部分是偶然、哪些是必要。

或者反过来,他们会“清理”一个看起来多余的 Sonderfall,却直到 Production 才发现它承载了十年前某个真实业务规则。

代码可以比它的解释活得更久。

有时,我们直接继承一个 Big Ball of Mud

Section titled “有时,我们直接继承一个 Big Ball of Mud”

并不是每个团队都参与了系统从 Prototype 到泥球的全过程。

有时新的团队或供应商接手时,结构问题早已存在。

这会改变责任问题。

一个新团队可以很快识别大量问题,却仍然只有一个非常具体的 Delivery Auftrag:实现 Version 2、增加 Feature X、迁移到新浏览器、或者保持系统继续运行。

如果合同没有预算重建核心结构,团队很难把所有历史 Technical Debt 作为自己的“顺带任务”解决。

这并不意味着它应该继续制造新问题。但它说明:识别问题和拥有解决问题的 Mandate 是两件不同的事。

你可以继承架构问题,却并不会自动继承改变整个系统的预算和决策权。

一个项目结束了,软件却没有结束

Section titled “一个项目结束了,软件却没有结束”

软件生命周期经常比项目生命周期长。

项目被立项、预算、执行、验收并正式结束。

软件却继续存在。

一年后新的 Requirement 出现,于是出现新项目。再过两年换供应商。之后还有 Migration、监管修改、Security Update 和其他 Change Request。

从组织视角看,这些是多个独立项目。

从代码视角看,它们仍然修改同一个系统。

这形成一个深刻冲突:组织可以把责任切成项目,而软件不会自动接受这些切分。

如果没有一个跨项目持续存在的 Architecture Ownership,那么每个项目都可能局部合理地完成自己的目标,却没有人长期负责整体结构。

一套长期存在的软件贯穿三个时间上分离的项目:V1 由供应商 A 开发,V2 由供应商 B 开发,V3 由供应商 C 开发。每个项目和合同都会结束,但代码、依赖和技术决策会继续进入下一阶段。

项目和合同边界切分的是组织,而产品的技术历史会完整跨越这些边界。

公共部门的信息系统尤其容易暴露这种时间尺度差异。

采购合同有明确范围、预算、验收和供应商。可软件在合同结束后通常还会继续运行很多年。

相关研究讨论了 Requirements Specification 中的制度冲突、公共 IT Outsourcing 的知识损失,以及长期可持续软件所需要的 Ownership。

问题并不在“外部供应商”这个概念本身。

外部团队可以交付非常好的软件,长期伙伴关系也完全可以建立高质量结构。

真正困难的是,当 Contract Boundary 与 System Boundary 不一致,而且没有机制维护跨 Contract 的结构连续性。

供应商 A 对 V1 负责,供应商 B 对 V2 负责,内部项目管理负责验收,业务部门负责需求。

那么谁对“V1 到 V5 之间系统结构的长期演进”负责?

如果答案不明确,责任空隙就会直接进入代码。

这是一个经常被忽视的问题。

一个项目里可以有 Architect,一个供应商里可以有 Tech Lead,但项目结束后,这个角色对应的长期责任是否仍然存在?

如果下一次招标重新定义 Scope、团队和供应商,而旧系统结构只是“现状”,那么 Architectural Knowledge 很容易变成移交文档里的附件,而不是活着的决策能力。

这也是为什么长期 System Ownership 如此重要。

不是为了建立一个中央架构权威,而是为了确保某些问题不会每次都从零开始:哪些 Boundary 必须保持?哪些 Debt 是有意接受的?哪些 Workaround 应该消失?哪些风险被组织主动承担?

如果这些问题没有持续 Ownership,下一批人只能从代码中重新猜。

把这一切简单归因于 Outsourcing 同样不准确。

内部团队也会重组,也会丢失知识,也会在时间压力下局部优化,也会建立 Big Ball of Mud。

外部供应商并不天然缺乏质量意识;内部团队也不天然拥有长期视角。

真正关键的是激励、合同、决策权、知识转移和责任持续时间是否支持长期系统健康。

如果 Auftrag 只支付 Feature,而不支付结构工作,那么供应商很难长期免费承担系统重建。

如果每次供应商更换都伴随显著知识损失,也不能简单指望下一家公司“自己读代码就懂”。

问题不在组织边界存在。

问题在组织边界是否与系统责任匹配。

“为什么当时不顺便 Refactor?”从多年后回看很容易提出。

但 Refactoring 仍然消耗容量、风险预算和决策空间。

如果项目合同只验收 Feature,如果 Bonus 只看日期,如果 Product Roadmap 从不为结构工作留下空间,那么“不做 Refactoring”可能是系统激励下完全可预测的结果。

这不是说开发团队可以借此免除质量责任。

团队仍然应该避免不必要的 Debt,应该让结构成本透明,也应该提出更安全的方案。

但组织如果长期只为可见 Feature 付费,也会塑造最终得到的架构。

如果结构工作永远没有预算,那么结构最终会被当作免费的资源消耗。

至此可以看到两种不同的 Big-Ball-of-Mud 路径。

第一种是连续演化:同一个产品从 Prototype 开始,在不断成功和局部优化中逐步侵蚀。

第二种是责任断裂:系统跨项目、供应商或团队存活,每个参与者都只拥有局部 Scope,没有人持续拥有整体结构。

两条路径不同。

共同盲点却很接近:长期 Architecture Ownership 不足。

在第一种路径里,结构责任被短期 Delivery 持续挤压;在第二种路径里,结构责任不断在组织边界之间掉落。

结果都可能是:系统活得比任何一个负责它的人更久。

架构侵蚀还会产生一个自我强化循环。

系统越难修改,下一次 Deadline 下进行局部优化就越有吸引力。

因为“真正修好”意味着先理解更大的影响范围、承担更多 Regression、协调更多人。

于是团队更可能再次选择局部 Workaround。

这个 Workaround 又进一步扩大未来的复杂度。

局部优化会形成自我强化循环:系统越难改,下一次越容易继续选择局部方案。

局部解决方案会改变未来决策:系统越难修改,下一次再次局部优化的压力就越大。

过去并不完全决定未来,但它会改变未来选择的价格。

Coding Agent 首先改变的是速度。

Agent 可以以非常高的速度找到一个局部可工作的解决方案。如果 Ownership、Boundary 和架构规则清晰,这是一种巨大优势。

如果这些方向标不存在,Agent 仍然必须在每个任务里重新猜:许多可能的局部解法中,哪一个才符合系统长期结构?

Agent 可以非常快地找到好的局部方案。而“局部方案持续获胜”本来就是历史问题的一部分。

这个系列会在关于 AI 的文章中进一步展开这一点。

讲完这些故事,很自然会问:到底是谁的错?

“没人”是一个舒服的答案,但它会完全消解责任。

决策当然有责任主体。开发者负责自己的修改,技术领导负责方向,Management 负责优先级,客户和 Auftraggeber 负责需求与合同,供应商负责约定范围内的交付质量。

但“所有人都有错”同样没有帮助。

不同的人在不同时间拥有不同信息、约束、预算和时间尺度。写 Workaround 的开发者知道本周 Deadline,却不可能知道未来五年的所有需求;Product Owner 看见正在等待 Feature 的客户,却不一定看见每一条技术依赖;架构师了解长期目标,却可能没有 Mandate 对抗每一个短期优先级;新供应商看到结构问题,却可能只有下一版产品的预算。

Project Management 与 Software Architecture 也是不同能力。一个优秀项目经理可以很好完成自己的职责,却未必拥有判断复杂 Dependency Structure 或 Test Strategy 的技术深度。这不是个人失败,而是角色边界与所需 Expertise 的问题。

所以,Big Ball of Mud 的整体状态通常不能有意义地压缩到某个人或某次决策上。

这正是“责任”和“归罪”的区别:我们可以严肃分析决策,而不必把系统演化写成犯罪故事。

今天回看那些促使我写这组文章的系统,我很难找到一个明确时刻,说:“就在这里,它变成了 Big Ball of Mud。”

成功的 Proof of Concept 不是那个时刻;没有它,产品可能根本不会存在。把它产品化也可能是完全正确的经济决定。第一个 Workaround 多半解决了真实问题,技术切换可能有合理理由,被推迟的 Refactoring 也可能确实输给了当时更重要的客户需求。

对于继承来的系统,这个问题更难。公司 B 在 V1 上继续开发 V2,如果 Auftrag 本来就只要求交付 V2,它“有错”吗?公司 A 当初完成了被委托、验收并付费的质量要求,它“有错”吗?如果项目经理根本不拥有跨多个采购周期管理 Architecture Quality 的角色和能力,又该如何评价?

这就是 Big Ball of Mud 的形成如此值得研究的原因:

它不是一个错误,而是多年中大量局部逻辑决策相互作用的结果。

有时还要再加一句:

组织可以把责任切成项目,但软件系统不会自动接受这些切分。

Foote 和 Yoder 在 1990 年代末就提出:究竟是什么力量,会让优秀程序员仍然产生结构上问题严重的系统?他们描述的 Throwaway Code、Piecemeal Growth、Keep It Working、Shearing Layers、Consolidation 以及最终 Reconstruction,更像是对长期软件演化力量的描述,而不是个人错误清单。

今天关于 Architecture Erosion、Architectural Technical Debt、Outsourcing 和公共信息系统的研究又扩展了这一视角:技术因素和非技术因素共同作用,短期和长期目标竞争,交接会造成知识流失,采购制度可能产生目标冲突,而长期 System Ownership 具有重要价值。

因此,Big Ball of Mud 可以被理解为一种 Emergent Result:很多参与者在很长时间里不断修改同一个系统,而每一次修改都在回应前一次修改留下的环境。团队、合同和组织会更换,技术决定却继续存在。如果缺少持续结构反馈,最后得到的整体形态可能从未被任何人作为目标图画出来。

每个人也许贡献过一点,有些人只是继续维护已经存在的一点。

没人画过最终结果,也没人某天决定“从今天开始取消边界”。

但仍然有很多人——通常出于完全可以理解的原因——各自贡献了其中一小块。

Architecture Erosion 不是事件。

它是过程。

而组织能够做出的最重要对策之一,可能非常朴素:

确保架构责任活得比下一个 Feature、下一个项目、下一个团队和下一份合同更久。

  • Brian Foote, Joseph W. Yoder: Big Ball of Mud. Technical Report WUCS-97-34, Department of Computer Science, Washington University, ursprünglich vorgestellt auf der Fourth Conference on Pattern Languages of Programs (PLoP ’97), 1997; später als Kapitel 29 in Neil Harrison, Brian Foote, Hans Rohnert (Hrsg.): Pattern Languages of Program Design 4. Addison-Wesley, 2000.
  • Ruiyin Li, Peng Liang, Mohamed Soliman, Paris Avgeriou: Understanding Software Architecture Erosion: A Systematic Mapping Study. Journal of Software: Evolution and Process 34(3), 2022, e2423. DOI: 10.1002/smr.2423.
  • Antonio Martini, Jan Bosch, Michel Chaudron: Investigating Architectural Technical Debt Accumulation and Refactoring over Time: A Multiple-Case Study. Information and Software Technology 67, 2015, S. 237–253. DOI: 10.1016/j.infsof.2015.07.005.
  • Roberto Verdecchia, Philippe Kruchten, Patricia Lago, Ivano Malavolta: Building and Evaluating a Theory of Architectural Technical Debt in Software-Intensive Systems. Journal of Systems and Software 176, 2021, 110925. DOI: 10.1016/j.jss.2021.110925.
  • Miikka Kuutila, Mika Mäntylä, Umar Farooq, Maëlick Claes: Time Pressure in Software Engineering: A Systematic Review. Information and Software Technology 121, 2020, 106257. DOI: 10.1016/j.infsof.2020.106257.
  • Rodrigo Rebouças de Almeida, Christoph Treude, Uirá Kulesza: What’s Behind Tight Deadlines? Business Causes of Technical Debt. IEEE/ACM International Conference on Cooperative and Human Aspects of Software Engineering (CHASE), 2023.
  • Robert Ramač et al.: Prevalence, Common Causes and Effects of Technical Debt: Results from a Family of Surveys with the IT Industry. Journal of Systems and Software 184, 2022, 111114. DOI: 10.1016/j.jss.2021.111114.
  • Martin P. Robillard: Turnover-Induced Knowledge Loss in Practice. ESEC/FSE ’21, 2021, S. 1292–1302. DOI: 10.1145/3468264.3473923.
  • Maria Alaranta, Sirkka L. Jarvenpaa: Changing IT Providers in Public Sector Outsourcing: Managing the Loss of Experiential Knowledge. 43rd Hawaii International Conference on System Sciences (HICSS), 2010. DOI: 10.1109/HICSS.2010.101.
  • Reetta Ghezzi, Aapo Koski, Janne Lautanala, Mikko Lehtisalo, Manu Setälä, Tommi Mikkonen: Towards Sustainable Software for Public Sector Information Systems. IEEE/ACM International Conference on Software and System Processes (ICSSP), 2023, S. 86–91. DOI: 10.1109/ICSSP59042.2023.00019.
  • Carl Erik Moe, Michael Newman, Maung Kyaw Sein: The Public Procurement of Information Systems: Dialectics in Requirements Specification. European Journal of Information Systems 26(2), 2017, S. 143–163. DOI: 10.1057/s41303-017-0035-4.
  • Josep M. Marco-Simó, Joan A. Pastor-Collado: IT Outsourcing in the Public Sector: A Descriptive Framework from a Literature Review. Journal of Global Information Technology Management 23(1), 2020, S. 25–52. DOI: 10.1080/1097198X.2019.1701357.
  • Casper Lassenius, Parastoo Mohagheghi, Jefferson Seide Molléri: Bringing it Home: Successful Backsourcing of Software Development in the Public Sector. Empirical Software Engineering 30, 170, 2025. DOI: 10.1007/s10664-025-10722-1.