跳转到内容

我的前端是 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。真正有用的诊断,不应该只问系统里是否存在糟糕结构,而应该问:系统是否仍然有能力限制这些糟糕结构继续扩散。

两种前端架构对比。左侧是一个结构糟糕、拥有巨大中心 Component 和强耦合卫星组件的 Planning Feature,但问题仍被限制在一个业务 Slice 内;右侧大量依赖穿透多个 Slice 与 Layer。

一个局部 God Object 还不是 Big Ball of Mud。真正危险的是边界本身开始失效。

回到那个新项目。

你先随便浏览一下应用。不是科研,不跑指标,只想看看这里一个“普通 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 的数量,而是局部构件似乎经常需要知道整个系统的事情。

然后你遇到 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,是完全不同的问题。

在另一个区域,世界看起来意外整洁。

很多 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 会失去重要属性:仅靠它可见的代码,你已经无法可靠判断它真正依赖什么、又隐式携带了哪些行为。

可见结构与实际有效结构开始分离。

迟早你会不满足于看单个文件,而想理解一个完整流程。

选一个看似简单的流程:

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,而是架构是否还有能力限制所需上下文。

两种简化前端系统对比。左侧通过清晰 Slice、Layer 和有方向的依赖缩小一次修改需要搜索的范围;右侧的交叉依赖和全局节点让局部修改潜在影响系统大部分区域。

好的架构不只是整理文件,而是减少一次修改所需要理解的上下文。

文件树甚至可能非常漂亮:

customers/
orders/
planning/
schools/
billing/
users/
administration/

看起来很令人安心。业务 Slice 都在。

然后你开始改 planning

里面直接依赖 schools。业务上说得通。

流程也需要 users。同样可以理解。

接着 administration 进来了。然后是 customers。还要用 billing 里的 Shared Service。另一边,schools 又反向 Import planning,而 users 还使用 customers 里的类型。

每一条连接可能都有一段看起来完全合理的历史。

这正是诊断困难的地方。

业务区域当然需要相互沟通。完全隔离既不现实,也不自动等于好架构。真正重要的是怎样沟通:是否存在明确 Integration Point?依赖方向能否解释?谁拥有信息、谁只是消费信息,是否清楚?

还是说,本质上任何区域都可以知道任何区域?

一个几乎可以被任何区域任意跨越的业务边界,最终就不再是边界,只剩下文件夹名称。

这时,“可见架构”和“有效架构”的区别开始变得重要。目录仍然可以叫 customersordersbilling,但这些名字无法证明它们真的像模块一样隐藏信息、限制依赖、隔离变化。

文件树越来越像一种愿望。Dependency Graph 才开始告诉你哪些边界在代码里仍然真正有效。

同样的问题还会出现在第二个维度。

Slice 本质上回答:

谁在业务上负责什么?

Layer 回答的是另一个问题:

这一层允许承担哪一类职责?

在结构良好的前端里,Layer 可以有不同命名和切法。关键不在于是否严格叫 presentationapplicationdomaininfrastructure,而在于它们的语义能否重复、能否可靠预测。

在一个正在 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 构成的矩阵。左侧职责与依赖停留在可解释边界中;右侧 Business Rule、UI State、HTTP 与 Navigation 散落在任意位置,大量依赖同时穿透 Slice 与 Layer。

当 Slice 不再限制依赖、Layer 不再限制职责时,架构最终可能只剩标签。

某一天,你想理解 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”。

而是这些条件的组合。

几周以后,你又发现一件事。

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 完全正常。

真正可疑的是:一个中心之所以中央,不是因为它拥有清晰而真正中央的职责,而是因为它不断积累越来越多外来的职责。

到这里,很多判断仍然来自经验和观察。

某个时刻,这不够了。

也许你想确认,自己是不是只是恰好撞上了几个特别丑的地方。也许你想知道那些所谓 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 的诊断非常有价值。也许 orderscustomers 在 Dependency Graph 上仍然相对分离,但如果十年来改一个区域经常迫使另一个区域一起变化,那么版本历史会讲出另一层故事。

架构不仅体现在代码如何排列

也体现在代码能够如何变化

看完技术结构以后,迟早会出现一个惊人地简单的问题:

谁负责?

Planning Slice 谁负责?

Global State 呢?

Cache Service 呢?

Base Class 呢?

Navigation 呢?

那条在五个不同地方都实现了一遍的关键业务规则呢?

你可能听到各种答案:

“这个大家都管一点。”

“历史上就是这么长出来的。”

“这个得问 X。”

“其实没人负责。”

或者经典版本:

“那里最好谁都别动。”

Ownership 并不只是组织架构图上的 Team Assignment。即使没有正式把 Module 分配给某个 Team,系统结构本身也可以表达 Ownership。

如果我在寻找一条业务规则:我能否推断哪个区域应该拥有它?

如果某个 State 被修改:是否存在一个可识别的位置控制这项决定?

如果一个 Module 提供接口:它对外承诺什么、哪些细节只属于内部,是否清晰?

在高度失去边界的系统里,不只是 Code 被到处散布,职责本身也变得模糊。

而这会进一步强化问题:当没人能说清一个决定到底属于谁时,也就很难长期保护相应边界。

Architecture Erosion 研究同样表明,它的原因并不只来自技术。组织因素、知识、开发实践,以及架构长期维护能力,与技术违规一样重要。

到这里,可以退一步问:

我们为什么关心 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 可能正好相反。

你知道得越来越多。

但能从这些知识可靠推导出来的东西却越来越少。

当然,几乎没人会在加入项目的第一周就开始 Architecture Forensics。

通常只是一些小小的不适:

“为什么它依赖那个东西?”

然后变成:

“我为什么现在会在这个文件里?”

再后来:

“为什么所有人都知道这个 Service?”

最后:

“为什么这里所有东西都认识所有东西?”

这种感觉是一个合理起点。

但不能成为最终结论。

因此,真正有用的是把多个视角放在一起:

  • 可见的 Dependency;
  • 实际 Change Pattern;
  • Slice Boundary;
  • Layer Boundary;
  • 引力中心;
  • 理解半径;
  • 修改半径;
  • Ownership。

只有当多个视角开始讲述同一个故事时,最初的不适才会升级为结构性怀疑。

进入陌生前端时常见的一条诊断路径:从最初的不理解,到发现无法解释的依赖和方向感丢失,再到通过技术分析识别系统性 Pattern。

怀疑经常从感觉开始,但诊断不应该停在感觉。

于是剩下最不舒服的问题:哪些现象才算真正强的迹象?

目前并不存在一套经过科学验证的 Big-Ball-of-Mud 评分量表。下面的权重因此明确只是架构 Heuristic,而不是诊断阈值。

单独看时,这些观察实际上说明不了太多:

  • 一个非常大的 Component;
  • 一个 God Service;
  • 一个有问题的 Base Class;
  • 某个孤立 Feature 缺少 Layering;
  • 个别 Dependency Cycle;
  • 过时的 Framework Pattern;
  • 自研 Infrastructure;
  • 一个本身就很难理解的 Domain。

每一点都可能是严重的架构问题,但关键仍然是:这个问题还有边界吗?

开头的 Planning Feature 可以结构糟糕,却依旧待在一个有效 Slice 里。一个全局技术 Service 可以拥有很高的中央性,却仍只承担一个精准的中央职责。

当相同问题反复出现并跨越边界时,诊断会更有意义:

  • 同一种 Pattern 出现在很多业务 Slice 中;
  • Component 系统性访问多个外部 Domain;
  • Business Rule 实际上可能存在于任意位置;
  • 中央 Service 被应用大部分区域使用,同时不断积累不同职责;
  • Cross-Slice Dependency 几乎失去可解释方向;
  • 一个小修改经常需要理解远处区域;
  • 架构规则拥有太多稳定例外,已经缺乏预测能力;
  • 架构图和真实 Dependency 讲的是两套故事。

到这里,问题就开始从局部 Design Problem 转向整个系统结构。

如果多个基础秩序机制同时失效,就尤其严重:

  • 应用大部分区域位于共同 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 是一种系统状态,不是单一指标。

如果今天让我分析一个陌生前端,我不会先从“有哪些坏 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。如果你想更系统地把这些观察放在一起衡量,可以使用一个小型架构 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 问题。

  • 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