代码库设计
设计深模块:小接口背后藏大量行为,接口位置干净,可通过该接口测试。凡是在设计或重构代码的地方,都用这套语言和这些原则。目标是给调用方杠杆、给维护者局部性、给所有人可测性。
术语表
严格使用这些术语——不要替换成"组件"、"服务"、"API"或"边界"。统一的语言就是全部意义所在。
模块(module) —— 任何有接口和实现的东西。刻意不分规模:一个函数、一个类、一个包,或一条横跨多层的切片。避免:单元、组件、服务。
接口(interface) —— 调用方要正确使用该模块所必须知道的一切:类型签名,也包括不变量、顺序约束、错误模式、必需的配置,以及性能特征;同时也是模块与外界相接、两侧可独立变化的那道边界(Michael Feathers 的 seam 就是它,不必另起一名)。接口放哪里、对外暴露多宽,本身就是一项独立的设计决策,与接口背后放什么分开。避免:API、签名(太窄——只指类型层面的那一面)、接缝/缝/seam(就是接口)、边界(与 DDD 的限界上下文撞了)。
实现(implementation) —— 模块里面的东西,它的代码体。与适配器区分开:一个东西可以是小适配器 + 大实现(一个 Postgres 仓库),也可以是大适配器 + 小实现(一个内存 fake)。接口位置是话题时用"适配器",否则用"实现"。
深度(depth) —— 接口处的杠杆:调用方(或测试)每学一个单位的接口,能驱动多少行为。一个模块是深的,当大量行为藏在一个小接口背后;是浅的,当接口几乎和实现一样复杂。
适配器(adapter) —— 在接口处满足某个接口的具体物。描述的是角色(填哪个坑),不是内容(里面是什么)。
杠杆(leverage) —— 调用方从深度里得到的东西:每学一个单位的接口,能拿到更多能力。一份实现,在 N 个调用点和 M 个测试里都还回来了。
局部性(locality) —— 维护者从深度里得到的东西:变更、bug、知识与验证集中在一处,而不是散落到各个调用方。修一次,处处都修好。
深与浅
深模块 = 小接口 + 大量实现:
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
└─────────────────────┘
浅模块 = 大接口 + 少量实现(要避免):
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
设计接口时问自己:
- 能不能减少方法数?
- 能不能简化参数?
- 能不能把更多复杂度藏到里面?
原则
- 深度是接口的属性,不是实现的属性。 一个深模块内部可以由小的、可 mock 的、可替换的部件组成——它们只是不暴露在接口上。一个模块可以有内部接口(对其实现私有、给它自己的测试用),也可以有对外接口。
- 假想删除法。 想象删掉这个模块。如果复杂度消失了,它就是个透传层。如果复杂度在 N 个调用方那里重新出现,那它是在扛活的。
- 接口就是测试面。 调用方和测试穿过的是同一个接口。要是你想测到接口背后,这个模块大概形状不对。
- 一个适配器意味着一个假想的接口;两个适配器才意味着真的接口。 没有什么真的跨接口变化,就不要引入新接口。
为可测性而设计
好的接口让测试变得自然:
接收依赖,不要自己造依赖。
// Testable function processOrder(order, paymentGateway) {} // Hard to test function processOrder(order) { const gateway = new StripeGateway(); }返回结果,不要产生副作用。
// Testable function calculateDiscount(cart): Discount {} // Hard to test function applyDiscount(cart): void { cart.total -= discount; }表面要小。 方法少 = 需要的测试少。参数少 = 测试准备简单。
关系
- 一个模块恰好有一个接口(它呈现给调用方和测试的那一面)。
- 深度是一个模块的属性,对着它的接口来衡量。
- 一个适配器坐在接口处,满足该接口。
- 深度给调用方产出杠杆,给维护者产出局部性。
不采纳的提法
- 把深度定义为实现行数与接口行数之比(Ousterhout):会奖励往实现里灌水。我们改用"深度即杠杆"。
- 把"接口"等同于 TypeScript 的
interface关键字或某个类的公开方法:太窄——这里的接口包含调用方必须知道的每一个事实。 - "边界":和 DDD 的限界上下文撞了。说接口。
- 接缝/缝(seam):与接口同义,统一说接口,不为位置另造一词。
更深一层
- 给定依赖后深化一个模块簇 —— 见 DEEPENING.md:依赖分类、接口纪律、"替换而非叠加"的测试策略。
- 探索多个候选接口 —— 见 DESIGN-IT-TWICE.md:起几个并行子代理,用截然不同的方式设计同一个接口,再从深度、局部性、接口位置上对比。