那种没人愿意谈论的架构
通常用不了一周。
你刚加入项目,还在熟悉 Onboarding Ticket、Build 流程、各种名字和职责,某个时候就会有人若无其事地说出第一句话:
“千万别碰 Service X。”
你点点头,记住这个名字。此时你还不知道,接下来的几个月里你会听到多少次这句话。有时候说话的人还会笑一下。有时候听起来更像警告。
几周之后,你经历第一次 Release。它也会和你在其他项目里见过的 Release 有些不同。
那一天,某些人最好保持在线。Deployment 结束后,团队不会立刻切到下一个 Ticket。大家会盯着 Log,检查几个关键流程,然后比平时多等一会儿,直到终于有人说:
“看起来没问题。”
这未必是因为大家知道一定会坏什么。
而是因为没人能非常确定地说,究竟什么东西可能会坏。
迟早你还会听到另一句话。也许来自 Management,也许来自一个在系统里待了很多年的 Specialist:
“这个我一天就做完了。”
而且他说不定是对的。
从业务上看,修改确实很小:增加一个字段,多一个条件,调整一条已有规则。对于一个已经了解这块系统多年的开发者来说,也许真的只需要几个小时。
但其他开发者可能需要几天,甚至几周。
站在外部,很容易形成一个直观解释:
系统不是问题。
开发者太慢了。
只是,这个解释和项目里所有人同时知道的事实对不上。
大家知道哪个 Service 有问题。知道哪些区域危险。Tester 知道常见的 Side Effect。Product Owner 知道哪些看似很小的 Ticket 经常失控。开发者知道哪些修改需要异常多的分析。管理层大概也已经看过不止一份关于 Technical Debt 的演示。
问题并没有被隐藏。
那为什么这种状态还能稳定存在很多年?
这正是本文要讨论的。
这里暂时还不讨论 Big Ball of Mud 在技术上应该如何精确定义。那是下一篇的主题。
我现在更感兴趣的是另一个问题:
为什么组织会逐渐发展出一种能力——持续在一个结构上明显有问题的系统里工作,而不是从根本上改变这种状态?
这不是异常状态,而是被稳定下来的状态
Section titled “这不是异常状态,而是被稳定下来的状态”一提到 Big Ball of Mud,人很容易想到混乱:系统不断故障,到处救火,Production 问题频发,整个系统仿佛随时会崩溃。
现实往往没有这么戏剧化。
日常工作依然继续。Feature 仍然上线。Release 仍然发生。Production Issue 仍然被解决。客户继续使用产品。
系统是“能工作的”。
只是为了让它继续工作,需要越来越多原本并不属于业务本身的东西:
- 更多测试;
- 更多 Review;
- 更多协调;
- Specialist Knowledge;
- 非正式规则;
- 特殊的 Release Process;
- 某些最好提前叫进来的关键人物;
- 以及大家都知道“最好别碰”的系统区域。
到了这个阶段,组织已经开始适应自己的架构。
这本身并不意味着不专业。
恰恰相反。
如果修改经常产生意外 Side Effect,那么增加测试很合理。如果一个关键区域实际上只有两个人真正理解,那么修改之前先问其中一个人,也很合理。如果 Release 风险很高,那么采用更谨慎的 Release Process 当然合理。如果某个 Service 每次修改都制造问题,那么保持谨慎也是专业行为。
问题未必出在其中某一个决定上。
问题可能恰恰出在许多局部合理决策叠加之后形成的整体结果。
因为每一次成功的补偿,都会让组织更容易继续生活在这个结构性状态里:
- 额外的测试挡住了 Bug;
- Review 发现了 Side Effect;
- Specialist 找到了 Production Error;
- Release 最终成功;
- Workaround 继续奏效。
以当天的视角看,每一项措施都是成功的。
而这正形成一个悖论:
组织越来越擅长补偿自身架构带来的后果。
与此同时,真正改变架构本身的即时压力反而可能降低。

补偿机制确实有效——而正因为它有效,也可能帮助当前结构继续存在。
当偏差逐渐成为正常运行的一部分
Section titled “当偏差逐渐成为正常运行的一部分”Diane Vaughan 提供了一个很有意思的组织社会学视角。
Vaughan 研究了 1986 年 Challenger 灾难之前 NASA 的决策过程。她描述了技术偏差如何逐步成为组织内部所认为的“可接受运行”之一部分。
偏差出现了。
它们没有立即导致灾难。
它们被分析、解释,并被放进已有决策流程中。之后的任务仍然成功。
随着时间推移,组织内部对于“什么风险仍然可以接受”的判断可能因此发生变化。
Vaughan 为这种现象提出了 Normalization of Deviance 这一概念。
这里必须做一个重要区分。
Vaughan 研究的不是 Big Ball of Mud。我也没有找到足够可靠的实证研究,可以让我们严肃地得出:
Big Ball of Mud 就是 Normalization of Deviance 的一种形式。
这样的类比太强了。
但这个概念确实可以帮助我们观察某些组织行为。
Jeffrey Pinto 后来通过对 21 位 Project Manager 的访谈,研究了 Normalization of Deviance 如何出现在项目组织中。其中涉及 Planning、Governance,以及那些随时间逐渐变成项目“正常做法”的问题性实践。
把这个视角放回软件开发,至少可以问一个问题:
如果一个 Release 必须增加三项特殊安全措施,才能稳定完成,会发生什么?
第一次,这显然属于异常情况。
第二次 Release,团队重复同样的措施。
第三次,再重复一次。
某个时候,大家不再问:
为什么我们的 Release 需要这种特殊处理?
操作层面的问题只剩下:
这一次谁来负责额外检查?
类似地:
这个 Service 存在结构性问题。
经过几年之后,可能变成:
这个 Service 就是要小心一点。
问题仍然是已知的。
只是应对问题的方式已经被吸收到正常运行过程里。
组织学会了与它共存。
Technical Debt 会制造 Feature 本身没有包含的工作
Section titled “Technical Debt 会制造 Feature 本身没有包含的工作”对于这种额外工作量,Software Engineering 已经有更直接的研究。
Terese Besker、Antonio Martini 和 Jan Bosch 研究了 Technical Debt 如何影响开发者的日常工作。
在他们的纵向研究中,43 位开发者持续记录自己的工作,并辅以访谈以及一次独立 Replication。
开发者平均报告,自己大约有 23% 的开发时间因为 Technical Debt 而损失。
额外工作尤其经常出现在更多测试,同时也包括分析与 Refactoring。研究还发现,已有 Technical Debt 有时会迫使开发者为了继续交付,再引入新的 Technical Debt。
这 23% 并不是对 Big Ball of Mud 的测量。研究讨论的是一般意义上的 Technical Debt,而在长期存在的软件系统中,完全没有 Technical Debt 几乎并不现实。Technical Debt 甚至不必天然是错误:如果短期收益足以证明未来成本合理,那么有意识地承担技术债务完全可能是理性选择。
所以对这个系列来说,最重要的不是 23% 这个具体数字,而是 Besker 等人观察到的机制:
Technical Debt 会占用原本并不属于业务修改本身的工作时间。
在 Big Ball of Mud 中,我关心的是这种额外成本不再偶尔发生,而是开始决定相当一部分日常开发工作的状态。
继续使用债务这个比喻:问题已经不再只是“存在债务”。
利息开始吞掉真正用于产品开发的能力。
因此,我们可以提出两个看起来很相似的问题。
第一个是:
为什么我们的开发者做这个修改需要六周?
第二个是:
为什么我们的系统为了一个业务上很小的修改,需要消耗六周的人力?

一个修改在业务上有多小,并不能告诉你一个难以修改的系统会把它放大成多少工作。
第一个问题很容易把原因放在人身上:经验、动力、Productivity、Seniorität。
第二个问题看的是系统。
也许根本没人真的写了六周 Feature Code。时间花在分析影响、寻找隐式依赖、请其他同事参与、扩大 Regression Test、重建历史 Sonderfall、检查那些理论上根本不该受到影响的区域,以及为 Release 增加更多安全保障。
这些都需要时间。
而在一个修改风险很高的系统中,其中很多其实都是完全专业的工作。
因此,开发流程“很慢”甚至可能是一个悖论性的结果:开发者正在非常认真地、负责任地对待一个高风险系统。
它依然昂贵。
只是原因与“开发者效率低”完全不同。
系统会制造自己的专家
Section titled “系统会制造自己的专家”一个复杂系统存在得越久,经验就越有价值。
这本来很正常。
软件开发是 Knowledge Work。并不是所有重要知识都写在文档里,也不可能全部从 Code 中还原。
真正危险的是,经验知识开始越来越多地替代系统本该拥有的结构可理解性。
这时,一个人知道的就不只是某个业务领域的知识。
他可能还知道:
- 修改 X 之后,哪个看似无关的 Component 必须额外测试;
- 某个老 Sonderfall 只会在什么特殊数据组合下出现;
- 为什么某行看起来很奇怪的代码最好别删;
- 哪个 Service 名义上负责某件事,但实际一直被绕开;
- 哪条错误信息真正表示的是完全不同的问题。
这时,一部分架构已经不只存在于 Repository 中。
它存在于人的脑子里。
Software Engineering 研究尝试通过 Truck Factor 或 Bus Factor 来衡量这种知识集中风险。
Avelino、Passos、Hora 和 Valente 分析了 133 个流行 GitHub 项目。在其中 65% 的项目中,估算出的 Truck Factor 不超过两个人。作者还通过 67 个项目中的实际开发者,对自动计算结果进行了额外验证。
这并不意味着 Truck Factor 很低的项目就一定是 Big Ball of Mud。
结构非常优秀的系统同样可能把关键知识集中在少数人身上。
但它至少说明:
软件项目依赖少数知识持有者,是一种真实并且可以测量的风险。
而在一个本身就难以理解的系统里,这种人脑中的知识甚至可能成为最重要的补偿机制,让系统看起来仍然“可控”。
英雄,以及它发出的错误信号
Section titled “英雄,以及它发出的错误信号”掌握这些知识的 Specialist 并不自动是故事里的反派。
很可能正好相反。
也许这个人多年来一直承担责任:别人查了一天的 Production Error,他二十分钟找到;关键 Release 时他始终在线;复杂修改他主动接手;同事遇到问题他总会帮忙。
一次又一次。
所以当他说:
“这个我一天就做完了。”
从事实层面完全可能是真的。
但由此不能自动推出:
这个修改只需要一天。
它也许真正意味着:
某一个拥有多年经验和大量隐式知识的人,可以在一天内完成这个修改。
这是完全不同的结论。
组织未必拥有“一天内完成该修改”的一般能力。
它只是拥有了一个可以做到的人。
于是,一个 Specialist 的个人强项可能向组织发出误导性信号。
系统看起来仍然可以掌控。
毕竟,一次次都有人把它掌控住了。Incident 被解决。Feature 最终完成。Release 被救回来。
而组织、团队和 Specialist 本人可能同时学到三件事:
- 组织:“只要情况危险,就把 Person X 叫进来。”
- 团队:“这块没有 Person X 最好别动。”
- Person X:“如果我不介入,事情可能会出问题。”
这些行为完全不需要 Ego、权力欲或者恶意。
它们完全可能来自经验、责任感和乐于帮助别人。
正因为如此,这种动态才格外难打破。
少数人的异常强大表现可以在短期补偿系统结构上的缺陷。
而这又会让这些缺陷对组织来说没有那么立即痛苦。
这种依赖最终会对关键人物自己以及团队造成什么影响,值得单独讨论。
在这里首先要记住的是:
系统可以依靠人的专业能力补偿结构缺陷,因此显得比真实情况更加稳定。
不会出现在任何发票上的成本
Section titled “不会出现在任何发票上的成本”Technical Debt 还有一个很不舒服的特征。
它的成本很少以单独一行出现。
通常不会有一张账单写着:
架构侵蚀:417,000 €
成本会被拆散:
- 一个开发者多分析了一天;
- 两位同事额外参加一次会议;
- 又增加一个 Regression Test;
- Release 前再多一次 Review;
- Specialist 被从另一个主题临时拉走;
- 为了安全,一个修改被拆成两个 Release;
- Onboarding 更久;
- Refactoring 再次被推迟。
每一项单独看都有合理理由。
而且每一项都可能是正确动作。
只有把它们加在一起,经济影响才真正显现。
Besker、Martini 和 Bosch 在关于 Architectural Technical Debt 的研究中,也不只是把它理解为“代码质量差”,而是视为一种会长期损害维护与演进能力的因素。
这些成本之所以在组织层面重要,恰恰是因为它们不显眼。
新 Feature 有 Ticket。
项目有 Budget。
Migration 有 Business Case。
但今天因为一个旧架构边界已经消失,而多出来的两小时分析,通常什么都没有。
它直接消失在“正常开发工作”里。
当这种成本在系统各处一点点消失时,一个结构性问题可以很长时间看起来只是许多独立的 Productivity 问题。
为什么不能简单地“把债还掉”
Section titled “为什么不能简单地“把债还掉””走到这里,很容易把整个故事归结成“Management 太短视”。
现实没有这么简单。
Technical Debt 可以是有意识承担的。一种务实的技术实现,如果能让重要产品更快进入市场,在当时完全可能是经济上正确的选择。
而且即使系统已经严重侵蚀,继续运行它的替代方案也绝不是免费。
系统已经有客户。有数据。其他系统依赖它。它承载业务流程。可能需要满足监管要求。Migration 期间它还必须继续运行。而且没有人能保证 Rewrite 一定更快、更便宜,或者结果更好。
所以真实决策很少是:
糟糕架构还是优秀架构?
它更像:
接下来的几个月,我们把资源投入到一个客户价值清晰的 Feature,还是投入到一项短期价值很难直接展示的结构改进?
这是一个真实的经济决策。
Freire 等人调查了来自六个国家的 653 名 Software Practitioner,研究为什么 Technical-Debt Item 没有被偿还。
原因经常并不纯粹属于技术层面。作者识别出多类障碍,其中很多“不偿还”的理由与 Planning 和 Management 有关。
这并不意味着:
Management 阻止了好软件。
它说明的是:Technical Debt 不会仅仅通过“开发者写得更好”自动消失。
它必须和 Planning、资源以及其他组织目标竞争。
于是,一个技术问题变成经济问题。
这一点在本系列后面的文章里还会变得更重要。
有时候,组织会继续投入
Section titled “有时候,组织会继续投入”关于为什么组织继续沿着一条已经明显有问题的路径投入资源,Escalation of Commitment 研究提供了另一个解释框架。
Mark Keil、Joan Mann 和 Arun Rai 早在 2000 年就研究了 Software / Information System Project 中,即使发展已经存在明显问题,组织仍继续增加资源投入的情况。
他们没有找到一个单一、简单的解释机制。多种理论模型共同帮助区分“持续升级投入”的项目与没有升级的项目。
这里同样需要谨慎。
一个运行几十年的 Productive System 不是“失败的软件项目”。把 Legacy System 继续运行仅仅解释成 Sunk Cost,也过于简单。
系统存在真实的切换成本、Modernization Risk、Dependency,有时候甚至根本没有现实替代方案。
这类研究真正有意思的是另一个结论:
一条路径已经变得有问题,并不自动意味着离开这条路径就是更理性的决定。
所以组织可以在一个没人认为理想的状态里停留非常久。
未必因为没人理解问题。
而是因为所有可用替代方案本身也带着成本和风险。
这正是 Big Ball of Mud 在经济上如此难处理的原因。
问题不只是:
继续运行这个系统有多贵?
还包括:
停止继续运行它,又有多贵、多危险?
所有人都知道,却再也没人愿意说
Section titled “所有人都知道,却再也没人愿意说”还剩另一个问题。
如果问题早就已经被大家知道,会发生什么?
也许 Service X 已经被讨论过五次。也许有架构图、Ticket、Workshop 和 Refactoring Proposal。也许它曾经几次被排进优先级,又几次被推迟。
到了某个阶段,问题不再是人们有没有看见它。
而是人们还会不会继续说出来。
组织心理学在 Employee Voice 和 Employee Silence 研究中讨论的正是这类问题。
Elizabeth Morrison 将 Voice 概括为:员工主动向更高组织层级的人表达建议、担忧或者有关问题的信息。Silence 则是有意识地保留这些信息。
这里有一个非常重要的点:
沉默并不意味着没有知识。
人可以非常准确地知道一个问题存在,同时决定不再重复提出。
员工是否会发声,部分取决于他们是否相信“说出来是安全的”,以及“说出来有可能产生效果”。
这同样不是针对 Big Ball of Mud 的专门研究。
但它给出了一个很有帮助的解释视角。
在有问题的项目里,一件事经常经历这样的语言变化。
开始时:
“这个必须从根本上解决。”
之后变成:
“我们真的应该为这个安排时间。”
再之后:
“这个已经在 Backlog 里了。”
最后:
“说了也没用。”
问题没有消失。
变化的也许只是大家对“再次提出问题是否还会产生作用”的预期。
一个技术认知可能因此变成组织内部沉默的共同知识。
每个人都知道 Service X。
也正因为每个人都知道,甚至没人再需要解释为什么 Service X 是问题。
“我们有 Technical Debt”是更容易说出口的话
Section titled ““我们有 Technical Debt”是更容易说出口的话”Technical Debt 今天已经是一个完全主流的术语。
几乎所有大型软件系统都有 Technical Debt。
我们可以识别它、排序它、记录它、把它写进 Backlog,甚至尝试测量它。
这些都很有价值。
但在严重侵蚀的系统中,有一个语言上的区别值得注意。
说:
“我们有很多 Technical Debt。”
相对容易。
说:
“修改我们系统的一个核心部分时,我们已经无法可靠预测会产生哪些影响。”
就难受得多。
因为第二句话已经不只是在谈 Code Quality。
它谈的是控制、Planability、Risk,以及一个组织是否还有能力主动、定向地改变自己的产品。
技术问题因此变成 Governance 问题。
我没有找到可靠研究证明组织会有意识使用 “Technical Debt” 这个词,来语言上掩盖这种控制能力的流失。
我也不想做这样的断言。
但这两个概念必须区分。
一个系统完全可以拥有 Technical Debt,同时依然非常可控、非常容易理解。
所以在 Big Ball of Mud 中,我关心的是更进一步的问题:
组织还能多有把握地说清楚,一次修改会带来什么后果?
如果这种可预测性消失了,“我们有 Technical Debt”最终就不再足以描述真实状态。
因为系统已经失去一部分结构控制。
而承认这一点,在组织层面远比在 Technical-Debt Backlog 里再加一个 Ticket 更不舒服。
成功补偿的悖论
Section titled “成功补偿的悖论”于是我们回到文章开头。
修改 Service X 之前,团队先问了 Specialist。额外 Review 做了。Regression Test 跑了。Specialist 在 Deployment 时在线。Workaround 继续有效。Release 成功。
从那一天的角度看,这是成功。
而且它确实就是成功。
客户继续工作,公司继续交付,一个真实 Risk 被控制住了。
正因为如此,我不喜欢用简单的 Schuldzuweisung 去解释这种系统。
开发者可以非常专业。Architect 可以很早、很明确地发出警告。Specialist 可以非常愿意帮助别人。Product Owner 可以追求完全合理的客户价值。Management 也可能在经济上做出完全可理解的优先级选择。
但这些局部合理的决定叠加起来,仍然可能稳定住一个任何人都不会主动选择的整体状态。
也许这正是 Big Ball of Mud 最有意思的特征之一:
它未必因为没人看见问题才活下来。
它也可能因为组织越来越擅长在这些问题存在的情况下继续工作,而长期存活。
补偿机制确实有效:
- 更多测试降低 Risk;
- Expert Knowledge 加快关键修改;
- Review 阻止错误;
- Workaround 保持业务运行;
- Release Ritual 提供安全感;
- 更多协调弥补缺失的结构边界。
每一项措施都可能是正确的。
但每一次成功补偿,也可能同时减轻一点原本会迫使组织做根本改变的即时痛苦。
这不是自然规律。
不是每个组织都会发展出这种动态。不是每种补偿都会加剧 Technical Debt。也不是每个大型、老旧或者复杂系统都属于 Big Ball of Mud。
但多个相互独立的研究方向,至少让这套动态中的部分机制变得可信:
- Technical-Debt 研究显示了额外开发成本,以及偿还 Technical Debt 的困难;
- Knowledge-Concentration 研究显示了项目对少数知识持有者的真实依赖;
- 组织研究说明了问题性实践如何逐渐被 Normalization;
- Employee Voice / Silence 研究说明,已知问题并不意味着人们会一直重复提出;
- Escalation of Commitment 研究说明,组织可能因为心理、组织与经济上的多重原因,继续沿着已经存在问题的技术路径投入。
在自己的职业经历里,我多次同时看到这些模式。
我的经验不是证据。
但它解释了为什么我对背后的问题感兴趣。
不是:
谁把系统弄坏了?
而是:
一个大家都看得出有问题的系统,为什么还能运行这么多年?
一个可能的答案是:
正因为人们已经学会每天成功补偿它的问题。
但在讨论如何与这样的系统共处、甚至如何离开这种状态之前,我们首先必须更精确地确定自己到底在讨论什么。
因为一个大系统并不等于 Big Ball of Mud。
一个老系统也不等于。
Monolith 更不自动等于。
所以下一篇会讨论:
Big Ball of Mud 到底是什么?
Avelino, Guilherme Amaral; Passos, Leonardo; Hora, André; Valente, Marco Tulio: A Novel Approach for Estimating Truck Factors. 24th IEEE International Conference on Program Comprehension (ICPC), 2016. DOI: 10.1109/ICPC.2016.7503718.
Besker, Terese; Martini, Antonio; Bosch, Jan: Managing Architectural Technical Debt: A Unified Model and Systematic Literature Review. Journal of Systems and Software 135, 2018, pp. 1–16. DOI: 10.1016/j.jss.2017.09.025.
Besker, Terese; Martini, Antonio; Bosch, Jan: Software Developer Productivity Loss Due to Technical Debt – A Replication and Extension Study Examining Developers’ Development Work. Journal of Systems and Software 156, 2019, pp. 41–61. DOI: 10.1016/j.jss.2019.06.004.
Freire, Sávio; Rios, Nicolli; Pérez, Boris; Castellanos, Camilo; Correal, Darío; Ramač, Robert; Mandić, Vladimir; Taušan, Nebojša; López Herrera, Gustavo; Pacheco Hernández, Alexia; Mendonça, Manoel; Falessi, Davide; Izurieta, Clemente; Seaman, Carolyn; Spínola, Rodrigo: Software Practitioners’ Point of View on Technical Debt Payment. Journal of Systems and Software 196, 2023, 111554. DOI: 10.1016/j.jss.2022.111554.
Keil, Mark; Mann, Joan; Rai, Arun: Why Software Projects Escalate: An Empirical Analysis and Test of Four Theoretical Models. MIS Quarterly 24(4), 2000, pp. 631–664. DOI: 10.2307/3250950.
Morrison, Elizabeth W.: Employee Voice and Silence. Annual Review of Organizational Psychology and Organizational Behavior 1, 2014, pp. 173–197. DOI: 10.1146/annurev-orgpsych-031413-091328.
Pinto, Jeffrey K.: Project Management, Governance, and the Normalization of Deviance. International Journal of Project Management 32(3), 2014, pp. 376–387. DOI: 10.1016/j.ijproman.2013.06.004.
Vaughan, Diane: The Challenger Launch Decision: Risky Technology, Culture, and Deviance at NASA. University of Chicago Press, Chicago, 1996.