代码库设计
设计 deep modules:在小 interface 后面隐藏大量行为,放置于干净的 seam,并可通过该 interface 进行测试。在设计和重构代码的任何地方使用这套语言和这些原则。目标是为调用者提供 leverage,为维护者提供 locality,并为所有人提供可测试性。
术语表
准确使用这些术语:不要用 “component”、“service”、“API” 或 “boundary” 替代。一致的语言正是重点。
Module:任何具有 interface 和 implementation 的东西。刻意与规模无关:函数、类、包或跨层切片。_避免_:unit、component、service。
Interface:调用者为了正确使用 module 必须知道的一切:类型签名,还包括不变量、顺序约束、错误模式、必需配置和性能特征。_避免_:API、signature(太窄,它们只指类型层面的表面)。
Implementation:module 内部的内容,其代码本体。与 Adapter 不同:一个东西可以是有大 implementation 的小 adapter(Postgres repo),也可以是有小 implementation 的大 adapter(in-memory fake)。当 seam 是主题时使用 “adapter”;否则使用 “implementation”。
Depth:interface 处的 leverage。调用者(或测试)每学习一单位 interface 所能行使的行为量。当大量行为位于小 interface 后面时,module 是 deep;当 interface 几乎与 implementation 一样复杂时,是 shallow。
Seam _(Michael Feathers)_:你可以改变行为而无需在该位置编辑的地方;module 的 interface 所在的*位置*。将 seam 放在哪里本身是一个设计决策,不同于其后面放什么。_避免_:boundary(与 DDD 的 bounded context 含义过载)。
Adapter:在 seam 处满足 interface 的具体事物。描述*角色*(填充哪个槽位),而非物质(内部是什么)。
Leverage:调用者从 depth 获得的东西。每学习一单位 interface 获得更多能力。一个 implementation 会在 N 个调用点和 M 个测试中收回成本。
Locality:维护者从 depth 获得的东西。变更、bug、知识和验证集中在一处,而不是散布到调用者中。修一次,处处修复。
深度与浅度
Deep module = 小 interface + 大量 implementation:
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
└─────────────────────┘
Shallow module = 大 interface + 少量 implementation(避免):
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
设计 interface 时,问:
- 我能减少方法数量吗?
- 我能简化参数吗?
- 我能在内部隐藏更多复杂度吗?
原则
- Depth 是 interface 的属性,不是 implementation 的属性。 deep module 可以在内部由小的、可 mock、可替换的部分组成;它们只是不属于 interface。module 可以具有 internal seams(私有于其 implementation,由其自己的测试使用)以及其 interface 处的 external seam。
- 删除测试。 想象删除该 module。如果复杂度消失,它只是 pass-through。如果复杂度在 N 个调用者中重新出现,它就是在值回成本。
- interface 是测试表面。 调用者和测试穿过同一个 seam。如果你想测试 *interface 之外*,该 module 可能形状错误。
- 一个 adapter 意味着假设性的 seam。两个 adapter 意味着真实的 seam。 除非确实有东西在其上变化,否则不要引入 seam。
面向可测试性的设计
好的 interface 使测试自然:
- 接受依赖,不要创建依赖。
```typescript // Testable function processOrder(order, paymentGateway) {}
// Hard to test function processOrder(order) { const gateway = new StripeGateway(); } ```
- 返回结果,不要产生副作用。
```typescript // Testable function calculateDiscount(cart): Discount {}
// Hard to test function applyDiscount(cart): void { cart.total -= discount; } ```
- 小表面积。 方法更少 = 需要的测试更少。参数更少 = 测试设置更简单。
关系
- 一个 Module 恰好有一个 Interface(它呈现给调用者和测试的表面)。
- Depth 是 Module 的属性,通过其 Interface 衡量。
- Seam 是 Module 的 Interface 所在之处。
- Adapter 位于 Seam 并满足 Interface。
- Depth 为调用者产生 Leverage,为维护者产生 Locality。
被拒绝的框架
- 将 Depth 视为 implementation-lines 与 interface-lines 的比率(Ousterhout):会奖励填充 implementation。我们改用 depth-as-leverage。
- 将 “Interface” 视为 TypeScript
interface关键字或类的 public methods:太窄:这里的 interface 包括调用者必须知道的每个事实。 - “Boundary”:与 DDD 的 bounded context 过度重载。使用 seam 或 interface。
更深入
- 在给定依赖的情况下深化一个 cluster,见 [DEEPENING.md](DEEPENING.md):依赖类别、seam 纪律,以及 replace-don't-layer 测试。
- 探索替代 interface,见 [DESIGN-IT-TWICE.md](DESIGN-IT-TWICE.md):启动并行 sub-agents,以几种根本不同的方式设计 interface,然后在 depth、locality 和 seam 放置方面进行比较。