| name | codebase-design |
| description | 设计深层模块的共享词汇。当用户想要设计或改进模块接口、寻找深化机会、决定缝合点放在哪里、使代码更可测试或更适合 AI 导航,或当其他技能需要深层模块词汇时使用。 |
代码仓设计
设计深层模块:通过一个小接口承载大量行为,放置在干净的缝合点处,可通过该接口进行测试。在任何设计或重构代码的地方使用这些语言和原则。目标是为调用者提供杠杆效应,为维护者提供局部性,为所有人提供可测试性。
术语表
严格使用以下术语 — 不要用 "component"、"service"、"API" 或 "boundary" 替代。一致的语言才是重点。
Module(模块) — 任何具有接口和实现的东西。有意识地与规模无关:函数、类、包或跨层切片。避免使用:unit、component、service。
Interface(接口) — 调用者正确使用模块所需了解的一切:类型签名,还包括不变量、顺序约束、错误模式、必需配置和性能特征。避免使用:API、signature(太窄 — 它们仅指类型层面的表面)。
Implementation(实现) — 模块内部的内容,它的代码体。区别于 Adapter(适配器):一个东西可以是一个小适配器加一个大实现(Postgres 仓库),也可以是一个大适配器加一个小实现(内存假实现)。当讨论缝合点时用 "adapter";否则用 "implementation"。
Depth(深度) — 接口处的杠杆效应:调用者(或测试)每学习一个单位的接口可以驱动的行为量。当大量行为隐藏在小接口后面时,模块是深层的;当接口几乎和实现一样复杂时,模块是浅层的。
Seam(缝合点) (Michael Feathers) — 一个可以在不编辑该位置的情况下改变行为的地方;模块接口所在的位置。缝合点放在哪里本身就是一个设计决策,与缝合点后面放什么不同。避免使用:boundary(与 DDD 的有界上下文重载)。
Adapter(适配器) — 在缝合点处满足接口的具体事物。描述的是角色(它填充哪个槽位),而非实质(内部是什么)。
Leverage(杠杆效应) — 调用者从深度中获得的好处:每学习一个单位的接口获得更多的能力。一个实现为 N 个调用点和 M 个测试带来回报。
Locality(局部性) — 维护者从深度中获得的好处:变更、bug、知识和验证集中在一个地方,而非分散在调用者之间。一次修复,处处生效。
深层 vs 浅层
深层模块 = 小接口 + 大量实现:
┌─────────────────────┐
│ 小接口 │ ← 少量方法,简单参数
├─────────────────────┤
│ │
│ 深层实现 │ ← 隐藏的复杂逻辑
│ │
└─────────────────────┘
浅层模块 = 大接口 + 少量实现(应避免):
┌─────────────────────────────────┐
│ 大接口 │ ← 大量方法,复杂参数
├─────────────────────────────────┤
│ 薄实现 │ ← 仅仅是透传
└─────────────────────────────────┘
设计接口时,问自己:
- 我能减少方法数量吗?
- 我能简化参数吗?
- 我能隐藏更多内部的复杂性吗?
原则
- 深度是接口的属性,而非实现的属性。 一个深层模块内部可以由小型、可模拟、可替换的部分组成 — 只是它们不属于接口的一部分。一个模块可以拥有内部缝合点(对其实现私有,用于其自身测试)以及位于其接口处的外部缝合点。
- 删除测试。 想象删除这个模块。如果复杂性消失,它就是个透传层。如果复杂性在 N 个调用者中重新出现,它就在发挥价值。
- 接口就是测试表面。 调用者和测试穿过同一个缝合点。如果你想测试接口_之外_的内容,模块可能形状不对。
- 一个适配器意味着假设的缝合点。两个适配器意味着真实的缝合点。 除非有东西确实在缝合点两侧变化,否则不要引入缝合点。
为可测试性而设计
良好的接口使测试变得自然:
-
接收依赖,不要创建依赖。
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 关键字或类的公开方法:太窄 — 此处的接口包括调用者必须了解的每个事实。
- "Boundary":与 DDD 的有界上下文重载。说 seam 或 interface。
深入阅读