我的前端是 Big Ball of Mud 吗?
你进入一个新项目。应用能跑,目录齐全,有 Components、有 Services,也许甚至还有架构图。
而你一开始什么都看不懂。
这本身并不值得担心。新的 Domain 有新的术语,长期演进的应用有自己的历史,大型 Codebase 本来就需要时间理解。哪怕系统结构非常优秀,你也不可能在第一天就知道为什么某个 State 存在、某个业务词到底意味着什么,或者为什么一个流程必须经过这五个步骤。
所以,更值得观察的不是一开始你理解多少,而是你的理解如何随时间变化。
随着知识增加,系统会不会越来越简单?你是否逐渐发现稳定的规则、边界和反复出现的 Pattern,以至于面对一个从未见过的区域,也能相对可靠地预测它应该长什么样?
还是说,你学到的主要是:为什么这里不遵守那条规则,为什么这个 Service 是例外,为什么那个 Component 因为历史原因直接访问三个其他业务领域,以及为什么某个模块绝对不能按照它的名字所暗示的方式来使用?
某个时刻,一个令人不安的怀疑会出现:
你学到的不是架构,而是架构的例外。
即便如此,也不能自动说明你面对的是 Big Ball of Mud。不是所有坏架构都是 Big Ball of Mud。一个巨大的 Component 不是。一个 God Service 不是。一套自研 Routing 不是。几个 Dependency Cycle 也不是。甚至一个 Angular Feature 把 Framework 提供的机制更多当作建议而不是规则,也还不足以得出这个结论。
真正的问题是:
这个系统还剩下多少真正有效的秩序?
Foote 和 Yoder 最初把 Big Ball of Mud 描述为一种近乎偶然形成、缺少规划结构的系统:它的组织方式更多由眼前需求塑造,而不是由可识别的设计所塑造。后续关于 Architecture Erosion 的研究也因此不仅关注单个违规点,而关注预期架构与实际有效架构之间不断扩大的偏差,以及这种偏差对维护和演进产生的影响。
对前端诊断来说,这个区别非常重要:Big Ball of Mud 并不只是“存在很多架构问题”的系统。真正危险的是,这些问题开始失去局部边界,而秩序本身越来越无法作为有效原则约束系统。
一个局部的“神”还不是 Big Ball of Mud
Section titled “一个局部的“神”还不是 Big Ball of Mud”先看一个反例。
设想一个复杂的 Planning Feature:地图、多种规划状态、大量业务规则、频繁交互。中心是一个随着多年演进而不断膨胀的 Component,周围大约还有十个与它高度耦合的小 Component。
这个中心 Component 加载数据、持有 State、编排用户交互、打开 Dialog、做业务判断、执行 Navigation、触发 Refresh,同时还顺手承担一些本来应该拥有独立职责的事情。合理的 Layering 几乎看不到。Framework 提供的机制在方便时会被使用,不方便时则出现各种自研做法。
简而言之:它像一个局部的小神。
这当然是糟糕的前端架构。一个 Component 聚合了太多职责,Layering 实际失效,Presentation、业务逻辑和 Orchestration 在同一个地方汇合。从局部看,它确实是一种 God Object,任何较大的修改都会因此变得不必要地困难。
但更重要的观察是:这个问题停在了一个业务边界以内。
Planning Feature 内部结构很差,但其他业务区域不会任意访问它的 State;它也不会自动知道系统其余一半的内部细节。旁边仍然存在其他边界清晰的 Slice。做 Billing 的人不会因为改发票而必然进入 Planning Component;做 User Management 的人也不必先理解它的内部状态机。
问题很严重,但仍然被限制住了。
一个被限制在清晰 Slice 内的 God Object,还不是 Big Ball of Mud。
更一般地说:
坏架构可以是局部的。Big Ball of Mud 是系统性的边界失效。
这是一个非常重要的反向检查。否则,几乎所有足够老的应用最终都可以被叫作 Big Ball of Mud。真正有用的诊断,不应该只问系统里是否存在糟糕结构,而应该问:系统是否仍然有能力限制这些糟糕结构继续扩散。

一个局部 God Object 还不是 Big Ball of Mud。真正危险的是边界本身开始失效。
随便打开一个 Component
Section titled “随便打开一个 Component”回到那个新项目。
你先随便浏览一下应用。不是科研,不跑指标,只想看看这里一个“普通 Component”通常长什么样。
于是你打开一个。
文件顶部是一长串依赖:User State、Navigation、Permission、Cache、Dialog Service、一个业务 Service、另一个业务 Service、Global State、另一个 Domain 的某个东西,然后还有一个你从未见过但似乎同样很重要的依赖。
单看这一点说明不了太多。Shell、复杂 Page Component 或明确设计出来的 Orchestration Point,本来就可能有很多依赖。
于是你再打开第二个 Component。
然后第三个。
某个时刻,随机观察开始变成重复 Pattern。
真正有价值的问题不再是:
这个 Component 有多少 Dependency?
而是:
如果我随机打开十个 Component,它们的依赖大多还停留在自己的业务区域和预期 Layer 内吗?
如果答案大多是“是”,你可能只是碰到了几个局部例外。如果几乎任意 Component 都知道 User State、Navigation、Global Service 和多个陌生 Domain,那么这个信号就强得多。
关键不是 Injection 的数量,而是局部构件似乎经常需要知道整个系统的事情。
那个名字无害的 5,000 行 Service
Section titled “那个名字无害的 5,000 行 Service”然后你遇到 cache.service.ts。
名字一开始让人很安心:一个 Cache,很正常。
你打开文件。
里面确实有 Cache。
除此之外还有 User State、Navigation Logic、一些 Initialization、多个业务派生、State Mutation、Global Communication,也许还有 Permission,以及一些与 Cache 的唯一关系只是“它们恰好也写在 cache.service.ts 里”的方法。
五千行以后,你至少明白了一件事:Caching 只是它众多兼职之一。
即使这样,应用也还没有因此自动变成 Big Ball of Mud。大 Service 首先是 Design Problem,也许甚至可以相对独立地拆分。
更值得看的,是它的 Fan-in:有多少地方依赖它?
如果一个历史形成的 Service 被越来越多互不相关的业务区域使用,它的角色就发生了变化。它会成为系统的引力中心。每一个新需求都有一个看似合理的理由继续使用它:反正已经存在,反正已经拥有几乎所有数据,反正到处都已经注入了。
于是形成一个强化效应。
到最后,名字只描述了它的起点,不再描述它的职责。
这类节点在诊断上远比单纯的文件大小更重要。一个 5,000 行但只有三个明确 Consumer 的 Service,与一个只有 1,500 行、却让整个应用结构性依赖它的 Service,是完全不同的问题。
几乎所有东西都继承的 Base Class
Section titled “几乎所有东西都继承的 Base Class”在另一个区域,世界看起来意外整洁。
很多 Component 很小。一百行,有时更少。几乎称得上模范。
然后你注意到这一行:
extends BaseComponent你打开 BaseComponent。
里面有 Navigation、User Information、Permission、Loading State、Error Handling、Cache、多个 Global Service,也许还有一些业务 Helper——它们大概曾经实用到让人觉得“只放在一个地方太可惜”。
然后你搜索引用。
一百个结果。
复杂度并没有消失。它只是被搬到了一个会自动跟随几乎所有 Component 的位置。
当然,Inheritance 本身也不是 Big Ball of Mud 的证据。一个职责明确的技术 Base Class 完全可以合理存在。危险的是,它同时跨越技术与业务边界,因此把大范围系统隐式连接起来。
这样一来,一个局部 Component 会失去重要属性:仅靠它可见的代码,你已经无法可靠判断它真正依赖什么、又隐式携带了哪些行为。
可见结构与实际有效结构开始分离。
第一个 User Flow
Section titled “第一个 User Flow”迟早你会不满足于看单个文件,而想理解一个完整流程。
选一个看似简单的流程:
Login → Initialization → Navigation → 第一张业务 View。
你从 Login 开始。接着进入 Auth Service,再进入 Global State,然后是 Initialization Service。Guard 加进来,Interceptor 也加进来。某处加载 User,另一处构建 Permission。Navigation 依赖一套自研抽象。在第一张真正的业务 View 出现之前,你已经穿过了一堆两小时前还不知道存在的文件。
某个时刻,你盯着一个 Service,问了一个非常基础的问题:
我为什么会来到这里?
偶尔发生一次,这甚至有点好笑。大型系统的真实流程本来就会经过多个技术构件。
但如果它反复发生,就触碰到架构的一个重要属性:Locality of Reasoning。
我这里很务实地使用这个词。它指的是:为了合理理解一个看起来局部的系统部分,你需要同时装进脑中的上下文到底有多大。
Parnas 很早就指出,模块化并不只是为了得到更漂亮的文件树。好的分解应该通过把重要设计决策隐藏在模块边界之后,提高系统的可理解性和可变更性。
放到前端里意味着:如果你在修改一个业务上很小的功能,那么系统其余相当大的一部分,对当前任务来说应该可以不相关。
当业务上很小的问题经常需要系统级理解空间时,这个系统就值得警惕。
这个信号比单个大文件强得多。因为此时问题已经不是 Style 或一般 Code Quality,而是架构是否还有能力限制所需上下文。

好的架构不只是整理文件,而是减少一次修改所需要理解的上下文。
当业务边界只剩下文件夹名称
Section titled “当业务边界只剩下文件夹名称”文件树甚至可能非常漂亮:
customers/orders/planning/schools/billing/users/administration/看起来很令人安心。业务 Slice 都在。
然后你开始改 planning。
里面直接依赖 schools。业务上说得通。
流程也需要 users。同样可以理解。
接着 administration 进来了。然后是 customers。还要用 billing 里的 Shared Service。另一边,schools 又反向 Import planning,而 users 还使用 customers 里的类型。
每一条连接可能都有一段看起来完全合理的历史。
这正是诊断困难的地方。
业务区域当然需要相互沟通。完全隔离既不现实,也不自动等于好架构。真正重要的是怎样沟通:是否存在明确 Integration Point?依赖方向能否解释?谁拥有信息、谁只是消费信息,是否清楚?
还是说,本质上任何区域都可以知道任何区域?
一个几乎可以被任何区域任意跨越的业务边界,最终就不再是边界,只剩下文件夹名称。
这时,“可见架构”和“有效架构”的区别开始变得重要。目录仍然可以叫 customers、orders、billing,但这些名字无法证明它们真的像模块一样隐藏信息、限制依赖、隔离变化。
文件树越来越像一种愿望。Dependency Graph 才开始告诉你哪些边界在代码里仍然真正有效。
然后 Layer 也失去意义
Section titled “然后 Layer 也失去意义”同样的问题还会出现在第二个维度。
Slice 本质上回答:
谁在业务上负责什么?
Layer 回答的是另一个问题:
这一层允许承担哪一类职责?
在结构良好的前端里,Layer 可以有不同命名和切法。关键不在于是否严格叫 presentation、application、domain、infrastructure,而在于它们的语义能否重复、能否可靠预测。
在一个正在 Erosion 的系统里则不同。
Component 加载数据、转换 DTO、做业务判断、修改 Global State、编排流程、执行 Navigation,同时还维护自己的 UI State。
Service 则打开 Dialog、包含 Business Rule、处理 HTTP、管理 Cache,并决定下一张 View 应该出现什么。
Infrastructure 开始做业务决定。Presentation 拥有业务状态转换。Shared Service 什么都知道一点。
问题不是某个 Component 偶尔放了一条更适合别处的规则。真实项目里几乎都会存在这种局部违规。
真正重要的是:当你问
我们的 Business Rule 到底放在哪里?
已经得不到有用答案。
如果答案是“要看具体情况”,还很正常。
如果答案是“原则上哪里都有”,那就值得认真看了。
当 Business Rule 实际上可以存在于任何地方时,Layering 也许还存在于目录里,却已经不再作为架构发挥作用。
关于 Architecture Erosion 的研究也并不只把它理解为“代码看起来乱”。结构违规、偏离架构决策,以及维护和演进中的困难,会相互关联并共同出现。

当 Slice 不再限制依赖、Layer 不再限制职责时,架构最终可能只剩标签。
那个不是 Router 的 Router
Section titled “那个不是 Router 的 Router”某一天,你想理解 Navigation。
你开始找 Router。
你找到了一个名字叫 Router 的东西。
至少开局不错。
然后你发现,真正的 Navigation 其实跑在另一套自研 Infrastructure 上。Route 有很多 Property,有些显而易见,有些名字听起来很重要,却完全看不出语义。某些字段影响 Permission,某些影响 Initialization,还有一些会改变 Global State 的行为。
你开始找文档。
没有。
但项目里有一个已经待了九年的人。
这里仍然要强调:这不是 Big Ball of Mud 的证明。Framework 可以被抽象。Angular Router 可以被封装。在需求真正合适时,团队甚至可以设计完全自己的 Navigation Model。
问题依旧是:复杂度是否有边界、是否能解释。
如果一套中央 Infrastructure 依赖多个 Domain,拥有系统级 Side Effect,行为无法从代码或文档中推导,而关键规则主要存在于少数人的记忆里,那么原本独立的问题就开始叠加。
真正的信号不是“自研 Infrastructure”。
而是这些条件的组合。
系统里的引力中心
Section titled “系统里的引力中心”几周以后,你又发现一件事。
Ticket 明明完全不同。一次是 User Management,一次是 Planning,下一次是 Permission,再下一周只是 Form 的某个小修改。
但你却惊人地经常落进同样的五个文件。
一个 Global Store。两个巨大 Service。一个 Base Class。还有一个 Helper——它大概是在真正还只负责“帮助”时得到这个名字的。
这些构件可以被理解为系统的引力中心。它们会吸引职责。
机制其实很简单:如果一个中央 Service 已经知道 User、Navigation、Permission 和多个业务 State,那么下一条需求放进去就特别方便。你可能需要的一切,它几乎都已经能访问。
于是 Fan-in 继续增加。同时,Fan-out 往往也继续增加,因为这个中心为了完成更多事情,又必须知道更多其他区域。
到了下一个 Ticket,决定就更容易:
“就放 Global Service 吧,其他东西也都在那里。”
这当然不是物理定律,“黑洞”也不是软件指标。但作为比喻,它准确描述了一个真实强化效应:已有的中心性,让下一次继续中心化在短期看来更合理。
Hub 并不天然有问题。应用确实需要某些有意设计的中央技术能力,比如 Logging、Telemetry 或部分 Infrastructure,它们拥有高 Fan-in 完全正常。
真正可疑的是:一个中心之所以中央,不是因为它拥有清晰而真正中央的职责,而是因为它不断积累越来越多外来的职责。
某个时候,你会画 Dependency Graph
Section titled “某个时候,你会画 Dependency Graph”到这里,很多判断仍然来自经验和观察。
某个时刻,这不够了。
也许你想确认,自己是不是只是恰好撞上了几个特别丑的地方。也许你想知道那些所谓 Slice 在技术上到底是否存在。也许你只是想看看,这种“所有东西都连在一起”的感觉有没有结构证据。
于是你开始分析 Import 和 Dependency。
一开始的期待很简单:Graph 里应该能看到业务 Cluster。也许会有少数中央技术组件,也许有几条不理想的跨边界连接,但整体上应该仍能看见架构图里声称存在的结构。
有时确实如此。
有时完全不是。
你看到大量 Cross-Slice Dependency。箭头双向穿梭。某些节点 Fan-in 极高,把系统大块区域连接起来;另一些节点 Fan-out 巨大。所谓独立区域之间出现 Cycle,多个 Cycle 又组合成大型 Strongly Connected Component(SCC)。
在有向依赖图中,一个 SCC 包含那些可以沿有向路径相互到达的节点。对软件依赖来说,大型 SCC 最重要的含义是:这些部分在当前层级已经无法再被清晰地按无环方向排序。因此研究和依赖 Cycle 分析工具会把 SCC 当作有意义的结构单元——但它仍然不是自动的 Big-Ball-of-Mud 判定器。
一个 Cycle 还不是 Big Ball of Mud。
十个 Cycle 也不自动是。
真正值得关注的是:系统相当大的部分是否陷在大型循环结构中;这些 Cycle 是否穿过 Slice Boundary;同样的节点是否又同时是全局引力中心。
Sangal、Jordan、Sinha 和 Jackson 在 2005 年就展示了如何使用静态提取的 Dependency Model 来揭示大型软件系统的实际结构,并把它与明确架构规则比较。这样的模型真正有价值的地方就在这里:Graph 本身不是架构,但它可以告诉你,架构图里声称存在的边界是否真的还存在于代码中。
而当加权 Dependency Line 最终密到像一块深色区域时,“Big Ball of Mud”这个名字至少在视觉上突然变得异常贴切。
当然,这只是一个玩笑。这个历史术语并不是因为 Graph 长这样才得名。
修改会停留在它业务上应该停留的地方吗?
Section titled “修改会停留在它业务上应该停留的地方吗?”Dependency Graph 展示静态结构。另一个视角来自每天正常开发时发生的事情。
你拿到一个 Ticket:
“Planning 增加一条 Validation。”
听起来非常局部。
你先修改 Planning。然后发现 Global Service 也要改。接着 Shared Base Class 要改。之后又发现第二个业务区域间接使用了同一个 State。保险起见,你还测试了另外三个 Flow,因为并不确定这个全局机制还拥有多少 Side Effect。
这种事情偶尔发生完全正常。有些需求的实际业务影响范围,本来就比 Ticket 标题看起来更大。
所以真正重要的依旧是重复性。
业务修改半径与技术修改半径,到底有多一致?
如果一个业务上局部的需求通常也可以技术上局部实现,那么系统边界正在发挥作用。
如果小的业务修改经常需要分散到大量距离很远的区域,系统就失去了 Locality of Change。
业务修改半径越经常与技术修改半径不一致,这个诊断就越值得认真。
而且你不必只看静态 Import。Gall、Hajek 和 Jazayeri 在 1990 年代末研究过所谓 Logical / Evolutionary Coupling:两个 Module 在静态结构上可能看起来分离,但版本历史却反复显示它们必须一起修改。尤其是跨越 Module 或 Subsystem Boundary 的长期共同变化,可能暴露隐藏依赖和有问题的系统分解。
这对 Big Ball of Mud 的诊断非常有价值。也许 orders 与 customers 在 Dependency Graph 上仍然相对分离,但如果十年来改一个区域经常迫使另一个区域一起变化,那么版本历史会讲出另一层故事。
架构不仅体现在代码如何排列。
也体现在代码能够如何变化。
到底谁拥有这个东西?
Section titled “到底谁拥有这个东西?”看完技术结构以后,迟早会出现一个惊人地简单的问题:
谁负责?
Planning Slice 谁负责?
Global State 呢?
Cache Service 呢?
Base Class 呢?
Navigation 呢?
那条在五个不同地方都实现了一遍的关键业务规则呢?
你可能听到各种答案:
“这个大家都管一点。”
“历史上就是这么长出来的。”
“这个得问 X。”
“其实没人负责。”
或者经典版本:
“那里最好谁都别动。”
Ownership 并不只是组织架构图上的 Team Assignment。即使没有正式把 Module 分配给某个 Team,系统结构本身也可以表达 Ownership。
如果我在寻找一条业务规则:我能否推断哪个区域应该拥有它?
如果某个 State 被修改:是否存在一个可识别的位置控制这项决定?
如果一个 Module 提供接口:它对外承诺什么、哪些细节只属于内部,是否清晰?
在高度失去边界的系统里,不只是 Code 被到处散布,职责本身也变得模糊。
而这会进一步强化问题:当没人能说清一个决定到底属于谁时,也就很难长期保护相应边界。
Architecture Erosion 研究同样表明,它的原因并不只来自技术。组织因素、知识、开发实践,以及架构长期维护能力,与技术违规一样重要。
架构应该让系统变得更可预测
Section titled “架构应该让系统变得更可预测”到这里,可以退一步问:
我们为什么关心 Slice、Layer、Dependency Direction 和 Ownership?
不是为了让架构图更漂亮。
架构的重要作用,是缩小可能的解决空间与搜索空间。
如果我知道某个业务区域的规则应该位于它的 Domain,我就不需要同时去任意 Component、HTTP Service 和 Shared Helper 里寻找。
如果我知道 billing 不允许直接依赖 planning,我就能排除一大批路径。
如果我知道 Presentation 只能通过定义好的 Application Interface 与 Slice 交互,那么一次 UI 修改就不要求我理解整个 Infrastructure。
好的结构会随着时间产生可预测性。
你打开一个陌生区域,大致知道里面应该出现什么。
你不认识具体代码,但认识它的规则。
也正因此,Onboarding Experience 是很有价值的诊断信号。一个复杂但结构优秀的系统,一开始同样可能令人不知所措。但随着理解增加,你的 Mental Model 应该开始压缩:大量零散观察,可以由少量稳定规则解释。
Big Ball of Mud 可能正好相反。
你知道得越来越多。
但能从这些知识可靠推导出来的东西却越来越少。
从感觉走向诊断
Section titled “从感觉走向诊断”当然,几乎没人会在加入项目的第一周就开始 Architecture Forensics。
通常只是一些小小的不适:
“为什么它依赖那个东西?”
然后变成:
“我为什么现在会在这个文件里?”
再后来:
“为什么所有人都知道这个 Service?”
最后:
“为什么这里所有东西都认识所有东西?”
这种感觉是一个合理起点。
但不能成为最终结论。
因此,真正有用的是把多个视角放在一起:
- 可见的 Dependency;
- 实际 Change Pattern;
- Slice Boundary;
- Layer Boundary;
- 引力中心;
- 理解半径;
- 修改半径;
- Ownership。
只有当多个视角开始讲述同一个故事时,最初的不适才会升级为结构性怀疑。

怀疑经常从感觉开始,但诊断不应该停在感觉。
不是每个症状权重都一样
Section titled “不是每个症状权重都一样”于是剩下最不舒服的问题:哪些现象才算真正强的迹象?
目前并不存在一套经过科学验证的 Big-Ball-of-Mud 评分量表。下面的权重因此明确只是架构 Heuristic,而不是诊断阈值。
弱或局部的迹象
Section titled “弱或局部的迹象”单独看时,这些观察实际上说明不了太多:
- 一个非常大的 Component;
- 一个 God Service;
- 一个有问题的 Base Class;
- 某个孤立 Feature 缺少 Layering;
- 个别 Dependency Cycle;
- 过时的 Framework Pattern;
- 自研 Infrastructure;
- 一个本身就很难理解的 Domain。
每一点都可能是严重的架构问题,但关键仍然是:这个问题还有边界吗?
开头的 Planning Feature 可以结构糟糕,却依旧待在一个有效 Slice 里。一个全局技术 Service 可以拥有很高的中央性,却仍只承担一个精准的中央职责。
明显更强的迹象
Section titled “明显更强的迹象”当相同问题反复出现并跨越边界时,诊断会更有意义:
- 同一种 Pattern 出现在很多业务 Slice 中;
- Component 系统性访问多个外部 Domain;
- Business Rule 实际上可能存在于任意位置;
- 中央 Service 被应用大部分区域使用,同时不断积累不同职责;
- Cross-Slice Dependency 几乎失去可解释方向;
- 一个小修改经常需要理解远处区域;
- 架构规则拥有太多稳定例外,已经缺乏预测能力;
- 架构图和真实 Dependency 讲的是两套故事。
到这里,问题就开始从局部 Design Problem 转向整个系统结构。
非常强的迹象
Section titled “非常强的迹象”如果多个基础秩序机制同时失效,就尤其严重:
- 应用大部分区域位于共同 Dependency Cycle 或大型 SCC 中;
- 业务边界在技术上几乎不再有效;
- Layer 不再有可靠语义;
- 中央区域的 Ownership 无论组织上还是结构上都无法合理确定;
- 小修改经常拥有意外的系统级影响;
- 新开发者随着时间学会越来越多 Sonderfall,却仍然无法形成稳定 Mental Model;
- 多个效应同时出现并相互强化。
关键就在于这种组合。
大型 SCC 本身不是 Big Ball of Mud 的数学定义。高 Fan-in 不是。高 Change Rate 不是。十次 Layer Violation 也不是。
因此,没有任何严肃规则可以说:
从第五个症状开始,系统就是 Big Ball of Mud。
Big Ball of Mud 是一种系统状态,不是单一指标。
我在真正诊断时会找什么
Section titled “我在真正诊断时会找什么”如果今天让我分析一个陌生前端,我不会先从“有哪些坏 Pattern”开始。
坏 Pattern 几乎任何项目都能找到。
我会试着判断,最重要的几种结构性限制是否仍然有效。
业务 Slice 还能限制依赖吗?
不要求完全隔离,但 Integration Point 和依赖方向是否还能解释?
Layer 还能限制职责吗?
我能否相对可靠地预测 Business Rule、UI State、Orchestration 和 Infrastructure 应该在哪里?
Dependency 是否仍有可识别方向?
还是形成了大范围相互依赖空间?
Reasoning 还能保持局部吗?
为了理解一个很小的业务流程,我需要同时理解应用的多少部分?
Change 还能保持局部吗?
一个局部 Ticket 多频繁会迫使我进入业务上很远的区域?
中央构件之所以中央,是因为职责本身中央吗?
还是它们只是历史便利不断累积出来的收集点?
Ownership 能被识别吗?
能否看出谁拥有一项决定,而哪些地方只是消费它?
最后:
可见架构还能解释真实软件吗?
这也许是最重要的问题。
系统不需要完美才能拥有架构。它可以有 Legacy,可以有很丑的 Feature,可以有 Transitional Solution、历史妥协和 Technical Debt。
只要边界还具有意义,就仍然存在可 Reasoning 的基础。
Big Ball of Mud 开始于越来越少的东西能够被可靠排除。
任何 Responsibility 都可能存在于任何地方。
任何区域都可能依赖任何其他区域。
任何修改都可能在意外位置产生影响。
而每条规则都必须先补上一句:
“当然,除了……”
我有 Big Ball of Mud 吗?
Section titled “我有 Big Ball of Mud 吗?”也许你已经在自己的项目里认出了令人不舒服的相似之处。即便如此,单个症状仍不足以诊断 Big Ball of Mud。如果你想更系统地把这些观察放在一起衡量,可以使用一个小型架构 Self-check。
我有 Big Ball of Mud 吗? →
系统会随着时间变得更容易理解吗?
Section titled “系统会随着时间变得更容易理解吗?”最后,再回到你加入项目的第一天。
你进入一个大型系统,一开始什么都看不懂。
这很正常。
在结构良好的复杂系统中,这种状态会随着时间改变。你学习 Domain,识别 Responsibility,理解 Boundary 和 Dependency Direction。许多单独文件逐渐组合成一个模型。
而这个模型会让系统的新区域越来越可预测。
你也许从没见过某个 Component,但知道它大概允许承担哪些任务。你从没改过某个 Slice,却能判断它的业务规则大概应该存在在哪里。你看到一条 Dependency,也可以解释为什么方向合理。
你的知识在增长,但完成工作所需的 Mental Context 在缩小。
在 Big Ball of Mud 中,可能正好相反。
几个月后,你知道大量细节:Method A 必须在 Method B 前调用;Global Cache 有三个特殊情况;某一种 Navigation 永远不能直接触发;你甚至知道应该找谁解释为什么 BaseComponent 持有那个业务 State。
你当然比第一天知道得多。
但这些知识没有形成一个可靠模型。
因此,判断 Big Ball of Mud 不能看系统里有没有一份 5,000 行文件、某个开发者是否奇怪地使用 Framework,或者某个 Feature 是否结构很差。这些问题都可能很严重,但经常还拥有清晰边界。
真正可疑的是,边界本身开始消失。
Big Ball of Mud 在前端中主要表现为多种结构性边界失效共同出现:业务 Slice 不再限制依赖;Layer 不再限制职责;修改无法保持局部;中央构件不断积累外来职责;Dependency Direction 消失;Ownership 越来越难以确定。
此时,真正的问题不再是一个差 Component、一个 God Service 或一个错误 Import。
真正的问题是:架构越来越无法可靠排除可能性。
如果几个月以后,你已经知道系统的大量细节,却仍然无法可靠说明某项 Responsibility 从哪里开始、在哪里结束、允许依赖谁,那么你遇到的很可能已经不只是 Onboarding 问题。
来源与延伸阅读
Section titled “来源与延伸阅读”- Brian Foote, Joseph W. Yoder: Big Ball of Mud. Technical Report WUCS-97-34, 1997;后作为第 29 章收入 Neil Harrison, Brian Foote, Hans Rohnert(编): 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。 - Ruiyin Li, Peng Liang, Mohamed Soliman, Paris Avgeriou: Understanding Architecture Erosion: The Practitioners’ Perceptive. Proceedings of the 29th IEEE/ACM International Conference on Program Comprehension (ICPC), 2021, pp. 24–35. DOI:
10.1109/ICPC52881.2021.00037。 - David L. Parnas: On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM 15(12), 1972, pp. 1053–1058. DOI:
10.1145/361598.361623。 - Harald Gall, Karin Hajek, Mehdi Jazayeri: Detection of Logical Coupling Based on Product Release History. Proceedings of the International Conference on Software Maintenance (ICSM), 1998, pp. 190–197. DOI:
10.1109/ICSM.1998.738508。 - Neeraj Sangal, Ev Jordan, Vineet Sinha, Daniel Jackson: Using Dependency Models to Manage Complex Software Architecture. OOPSLA 2005, pp. 167–176. DOI:
10.1145/1094811.1094824。