跳转到内容

那种没人愿意谈论的架构

通常用不了一周。

你刚加入项目,还在熟悉 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 继续奏效。

以当天的视角看,每一项措施都是成功的。

而这正形成一个悖论:

组织越来越擅长补偿自身架构带来的后果。

与此同时,真正改变架构本身的即时压力反而可能降低。

成功补偿的循环:结构性问题带来高风险修改,组织通过额外测试、Review、专家知识和 Workaround 进行补偿。Release 一旦继续成功,立即改变架构的压力反而可能下降。

补偿机制确实有效——而正因为它有效,也可能帮助当前结构继续存在。

当偏差逐渐成为正常运行的一部分

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 中,我关心的是这种额外成本不再偶尔发生,而是开始决定相当一部分日常开发工作的状态。

继续使用债务这个比喻:问题已经不再只是“存在债务”。

利息开始吞掉真正用于产品开发的能力。

因此,我们可以提出两个看起来很相似的问题。

第一个是:

为什么我们的开发者做这个修改需要六周?

第二个是:

为什么我们的系统为了一个业务上很小的修改,需要消耗六周的人力?

开发者太慢,还是系统太慢?以人为中心的视角把较长开发周期直接归因于开发者;系统视角则看到分析、依赖、协调、Regression Test 和 Release 保障带来的额外工作。

一个修改在业务上有多小,并不能告诉你一个难以修改的系统会把它放大成多少工作。

第一个问题很容易把原因放在人身上:经验、动力、Productivity、Seniorität。

第二个问题看的是系统。

也许根本没人真的写了六周 Feature Code。时间花在分析影响、寻找隐式依赖、请其他同事参与、扩大 Regression Test、重建历史 Sonderfall、检查那些理论上根本不该受到影响的区域,以及为 Release 增加更多安全保障。

这些都需要时间。

而在一个修改风险很高的系统中,其中很多其实都是完全专业的工作。

因此,开发流程“很慢”甚至可能是一个悖论性的结果:开发者正在非常认真地、负责任地对待一个高风险系统。

它依然昂贵。

只是原因与“开发者效率低”完全不同。

一个复杂系统存在得越久,经验就越有价值。

这本来很正常。

软件开发是 Knowledge Work。并不是所有重要知识都写在文档里,也不可能全部从 Code 中还原。

真正危险的是,经验知识开始越来越多地替代系统本该拥有的结构可理解性。

这时,一个人知道的就不只是某个业务领域的知识。

他可能还知道:

  • 修改 X 之后,哪个看似无关的 Component 必须额外测试;
  • 某个老 Sonderfall 只会在什么特殊数据组合下出现;
  • 为什么某行看起来很奇怪的代码最好别删;
  • 哪个 Service 名义上负责某件事,但实际一直被绕开;
  • 哪条错误信息真正表示的是完全不同的问题。

这时,一部分架构已经不只存在于 Repository 中。

它存在于人的脑子里。

Software Engineering 研究尝试通过 Truck FactorBus Factor 来衡量这种知识集中风险。

Avelino、Passos、Hora 和 Valente 分析了 133 个流行 GitHub 项目。在其中 65% 的项目中,估算出的 Truck Factor 不超过两个人。作者还通过 67 个项目中的实际开发者,对自动计算结果进行了额外验证。

这并不意味着 Truck Factor 很低的项目就一定是 Big Ball of Mud。

结构非常优秀的系统同样可能把关键知识集中在少数人身上。

但它至少说明:

软件项目依赖少数知识持有者,是一种真实并且可以测量的风险。

而在一个本身就难以理解的系统里,这种人脑中的知识甚至可能成为最重要的补偿机制,让系统看起来仍然“可控”。

掌握这些知识的 Specialist 并不自动是故事里的反派。

很可能正好相反。

也许这个人多年来一直承担责任:别人查了一天的 Production Error,他二十分钟找到;关键 Release 时他始终在线;复杂修改他主动接手;同事遇到问题他总会帮忙。

一次又一次。

所以当他说:

“这个我一天就做完了。”

从事实层面完全可能是真的。

但由此不能自动推出:

这个修改只需要一天。

它也许真正意味着:

某一个拥有多年经验和大量隐式知识的人,可以在一天内完成这个修改。

这是完全不同的结论。

组织未必拥有“一天内完成该修改”的一般能力。

它只是拥有了一个可以做到的人

于是,一个 Specialist 的个人强项可能向组织发出误导性信号。

系统看起来仍然可以掌控。

毕竟,一次次都有人把它掌控住了。Incident 被解决。Feature 最终完成。Release 被救回来。

而组织、团队和 Specialist 本人可能同时学到三件事:

  • 组织:“只要情况危险,就把 Person X 叫进来。”
  • 团队:“这块没有 Person X 最好别动。”
  • Person X:“如果我不介入,事情可能会出问题。”

这些行为完全不需要 Ego、权力欲或者恶意。

它们完全可能来自经验、责任感和乐于帮助别人。

正因为如此,这种动态才格外难打破。

少数人的异常强大表现可以在短期补偿系统结构上的缺陷。

而这又会让这些缺陷对组织来说没有那么立即痛苦。

这种依赖最终会对关键人物自己以及团队造成什么影响,值得单独讨论。

在这里首先要记住的是:

系统可以依靠人的专业能力补偿结构缺陷,因此显得比真实情况更加稳定。

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、资源以及其他组织目标竞争。

于是,一个技术问题变成经济问题。

这一点在本系列后面的文章里还会变得更重要。

关于为什么组织继续沿着一条已经明显有问题的路径投入资源,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 VoiceEmployee 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 更不舒服。

于是我们回到文章开头。

修改 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.