| name | codebase-design |
| description | 用于设计深模块的共享词汇体系。适用于用户希望设计或改进模块接口、寻找深化机会、决定 seam 的位置、提高代码的可测试性或 Agent 可导航性,或其他 Skill 需要使用深模块词汇时。 |
代码库设计
设计深模块:用很小的接口封装大量行为,把接口放在清晰的 seam 上,并通过该接口完成测试。凡是设计或重组代码,都应使用下列语言和原则。目标是让调用方获得杠杆,让维护者获得局部性,并让所有参与者都能自然地测试代码。
术语表
严格使用以下术语,不要替换成 “component”“service”“API” 或 “boundary”。保持语言一致,正是建立这套词汇的目的。
Module(模块)——任何同时拥有接口和实现的事物。该概念刻意不限定规模,可以是一段函数、一个类、一个 package,也可以是跨越多个层级的完整切片。应避免使用:unit、component、service。
Interface(接口)——调用方为了正确使用模块而必须了解的全部内容:既包括类型签名,也包括不变量、顺序限制、错误模式、必要配置和性能特征。应避免使用:API、signature。这两个词范围过窄,只能表达类型层面的表面结构。
Implementation(实现)——模块内部的代码主体。它与 Adapter 含义不同:一个事物可以是很小的 adapter,却拥有很大的实现,例如 Postgres repository;也可以是很大的 adapter,却只有很小的实现,例如内存 fake。讨论 seam 时使用 “adapter”,其他情况下使用 “implementation”。
Depth(深度)——接口提供的杠杆:调用方或测试每学习一单位接口,可以调动多少行为。大量行为隐藏在小接口后面时,模块是深的;接口复杂度几乎与实现相当时,模块是浅的。
Seam(Michael Feathers)——无需在该位置编辑代码,就能改变行为的地方;也就是模块接口所在的位置。把 seam 放在哪里是一项独立设计决策,与 seam 后面封装什么内容不同。应避免使用 boundary,因为它在 DDD 中常用于 bounded context,含义已经过载。
Adapter(适配器)——位于 seam 上、满足某个接口的具体事物。它描述的是角色,也就是填入哪个位置;不描述内部由什么构成。
Leverage(杠杆)——调用方从深度中获得的收益:每学习一单位接口,就能得到更多能力。一份实现可以同时服务 N 个调用点和 M 个测试。
Locality(局部性)——维护者从深度中获得的收益:变更、缺陷、知识和验证集中在一处,而不会散落到各调用方。修复一次,所有位置同时修复。
深模块与浅模块
深模块 = 小接口 + 大量实现:
┌─────────────────────┐
│ 小接口 │ ← 方法少,参数简单
├─────────────────────┤
│ │
│ 深实现 │ ← 复杂逻辑隐藏在内部
│ │
└─────────────────────┘
浅模块 = 大接口 + 少量实现,应避免:
┌─────────────────────────────────┐
│ 大接口 │ ← 方法多,参数复杂
├─────────────────────────────────┤
│ 薄实现 │ ← 只负责透传
└─────────────────────────────────┘
设计接口时询问:
- 能否减少方法数量?
- 能否简化参数?
- 能否把更多复杂度隐藏在内部?
原则
- 深度是接口的属性,不是实现的属性。 深模块内部完全可以由小型、可 Mock、可替换的部件组成,只是这些部件不属于对外接口。模块既可以拥有实现私有、供内部测试使用的内部 seams,也可以在接口处拥有外部 seam。
- 删除测试。 想象把模块删除。如果复杂度随之消失,说明它只是透传层;如果复杂度重新散落到 N 个调用方,说明该模块确实发挥了价值。
- 接口就是测试表面。 调用方和测试通过同一个 seam。如果你希望越过接口向内部测试,模块形态很可能存在问题。
- 只有一个 adapter 时,seam 只是设想;出现两个 adapter 后,seam 才真实存在。 只有 seam 两侧确实存在变化时,才引入它。
为可测试性而设计
良好接口会让测试自然发生:
-
接收依赖,不要自行创建依赖。
function processOrder(order, paymentGateway) {}
function processOrder(order) {
const gateway = new StripeGateway();
}
-
返回结果,不要直接制造副作用。
function calculateDiscount(cart): Discount {}
function applyDiscount(cart): void {
cart.total -= discount;
}
-
保持很小的表面积。 方法越少,需要的测试越少;参数越少,测试准备越简单。
关系
- 一个 Module 恰好拥有一个 Interface,也就是它呈现给调用方和测试的表面。
- Depth 是 Module 的属性,并通过它的 Interface 衡量。
- Seam 是 Module 的 Interface 所在的位置。
- Adapter 位于 Seam 上,并满足对应的 Interface。
- Depth 为调用方带来 Leverage,为维护者带来 Locality。
不采用的表述
- 把深度理解成实现代码行数与接口代码行数之比(Ousterhout):这种算法会奖励无意义地填充实现。这里采用“深度即杠杆”的定义。
- 把 “Interface” 限定为 TypeScript 的
interface 关键字或类的 public methods:范围过窄。这里的接口包含调用方必须了解的每一项事实。
- “Boundary”:该词在 DDD 的 bounded context 中已经含义过载。应使用 seam 或 interface。
进一步深入
- 在已知依赖的情况下深化一组模块——参见 DEEPENING.md:依赖分类、seam 纪律,以及“替换而不叠加”的测试原则。
- 探索不同接口方案——参见 DESIGN-IT-TWICE.md:并行启动多个子 Agent,让它们以差异足够明显的方式设计接口,再从深度、局部性和 seam 位置三个角度比较。