| name | codebase-design |
| description | 设计深模块的共享术语体系。当用户想要设计或改进某个模块的接口、寻找加深(deepening)的机会、决定接缝(seam)放在哪里、让代码更易测试或更利于 AI 导航时,或当其他技能需要用到深模块术语时使用。 |
代码库设计(Codebase Design)
设计深模块(deep module):在小接口背后承载大量行为,置于一个清晰的接缝处,并可通过该接口进行测试。无论在何处设计或重构代码,都使用这套语言和这些原则。目标是:为调用方带来杠杆(leverage),为维护者带来局部性(locality),为所有人带来可测试性(testability)。
术语表
请精确使用这些术语——不要用「组件(component)」「服务(service)」「API」或「边界(boundary)」来替代。语言的一致性正是全部要点所在。
模块(Module) —— 任何具有接口和实现的东西。刻意做到与规模无关:可以是一个函数、类、包,或跨层的一段切片。避免:单元(unit)、组件(component)、服务(service)。
接口(Interface) —— 调用方为正确使用该模块所必须了解的一切:类型签名,以及不变量、顺序约束、错误模式、必需的配置和性能特征。避免:API、签名(signature)(太狭窄——它们只指类型层面的表层)。
实现(Implementation) —— 模块内部的东西,它的代码主体。与**适配器(Adapter)**有别:一个东西可以是「小适配器 + 大实现」(如 Postgres 仓储),也可以是「大适配器 + 小实现」(如内存伪实现)。当接缝是讨论主题时,用「适配器」;否则用「实现」。
深度(Depth) —— 接口处的杠杆:调用方(或测试)每学习一个单位的接口,能够驱动多少行为。当大量行为藏在小接口背后时,模块是**深(deep)的;当接口几乎与实现一样复杂时,模块是浅(shallow)**的。
接缝(Seam) (Michael Feathers) —— 一个你无需在此处编辑就能改变行为的位置;模块接口所在的地点。接缝放在哪里本身就是一项独立的设计决策,与接缝背后放什么有别。避免:边界(boundary)(因与 DDD 的限界上下文含义重叠而被过度使用)。
适配器(Adapter) —— 在某个接缝处满足接口的具体事物。它描述的是角色(填补哪个槽位),而非实质(内部是什么)。
杠杆(Leverage) —— 调用方从深度中获得的东西:每学习一个单位的接口就获得更多能力。一份实现可在 N 个调用点和 M 个测试中反复收益。
局部性(Locality) —— 维护者从深度中获得的东西:变更、缺陷、知识和验证都集中在一处,而非散落到各个调用方。修一次,处处皆修。
深 vs 浅
深模块 = 小接口 + 大量实现:
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
└─────────────────────┘
浅模块 = 大接口 + 少量实现(应避免):
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
设计接口时,问自己:
- 我能减少方法的数量吗?
- 我能简化参数吗?
- 我能把更多复杂性藏进内部吗?
原则
- 深度是接口的属性,而非实现的属性。 一个深模块的内部可以由许多小的、可 mock、可替换的部件组成——它们只是不属于接口的一部分。模块既可以拥有内部接缝(internal seam)(对其实现私有,供自身测试使用),也可以在其接口处拥有外部接缝(external seam)。
- 删除测试(deletion test)。 设想删掉这个模块。如果复杂性随之消失,那它只是一个透传(pass-through)。如果复杂性在 N 个调用方处重新冒出来,那它就是在发挥价值。
- 接口即测试面(test surface)。 调用方和测试跨越的是同一个接缝。如果你想测试接口之外/之内的东西,那这个模块的形状很可能不对。
- 一个适配器意味着假想的接缝;两个适配器意味着真实的接缝。 除非确实有东西在接缝两侧发生变化,否则不要引入接缝。
为可测试性而设计
好的接口让测试变得自然:
-
接受依赖,而非创建依赖。
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):这会奖励往实现里塞水分。我们改用「深度即杠杆」。
- 将「接口」等同于 TypeScript 的
interface 关键字或类的公有方法:太狭窄——这里的接口包含调用方必须了解的每一个事实。
- 「边界(Boundary)」:与 DDD 的限界上下文含义重叠。请说接缝(seam)或接口(interface)。
深入了解
- 在给定依赖的前提下加深一组模块 —— 见 DEEPENING.md:依赖分类、接缝纪律,以及「替换而非叠加(replace-don't-layer)」的测试策略。
- 探索可选的接口方案 —— 见 DESIGN-IT-TWICE.md:并行启动多个子代理,用几种截然不同的方式设计接口,再从深度、局部性和接缝位置上进行对比。