用途
Codebase Design 是一套用于设计深模块(deep modules)的共享词汇与原则。它适用于任何正在设计或重构代码的场景,目标是让调用方获得杠杆(leverage)、维护者获得局部性(locality)、所有人获得可测试性。
核心概念
深模块指“小接口 + 大量实现”:少量方法、简单参数,复杂逻辑隐藏在内部。浅模块则相反,接口几乎和实现一样复杂,应当避免。
设计接口时可以自问:能否减少方法数量?能否简化参数?能否把更多复杂度藏进内部?
术语表
资料要求严格使用以下术语,不要用 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:维护者从深度中获得的东西,变更、缺陷、知识和验证集中在一处而非散布到调用方。修一次,处处修好。
原则
- 深度是接口的属性,不是实现的属性。深模块内部可以由小的、可 mock、可替换的部分组成,它们只是不属于接口。模块可以有内部接缝(实现私有,供自身测试使用)以及接口处的外部接缝。
- 删除测试:想象删除该模块。如果复杂度消失,它只是直通;如果复杂度在 N 个调用方中重新出现,它就在发挥价值。
- 接口即测试面:调用方和测试穿过同一个接缝。如果想越过接口测试,模块形状可能不对。
- 一个适配器意味着假设的接缝,两个适配器才意味着真实的接缝。除非确实有东西在接缝两侧变化,否则不要引入接缝。
为可测试性设计
- 接受依赖,不要创建依赖。例如
function processOrder(order, paymentGateway) {}可测试,而内部new StripeGateway()则难以测试。 - 返回结果,不要产生副作用。例如
function calculateDiscount(cart): Discount {}可测试,而直接修改cart.total则难以测试。 - 缩小接口面积:方法越少,需要的测试越少;参数越少,测试设置越简单。
关系
一个 Module 恰好有一个 Interface。Depth 是 Module 的属性,相对于其 Interface 衡量。Seam 是 Module 的 Interface 所在之处。Adapter 位于 Seam 并满足 Interface。Depth 为调用方产生 Leverage,为维护者产生 Locality。
被拒绝的框架
- 深度作为实现行数与接口行数的比率(Ousterhout):会奖励填充实现。此处采用“深度即杠杆”。
- 把 Interface 理解为 TypeScript 的 interface 关键字或类的公开方法:过于狭窄,此处接口包括调用方必须知道的一切事实。
- Boundary:与 DDD 的限界上下文混淆,应说 seam 或 interface。
深入阅读
- 给定依赖时深化一个集群:见 DEEPENING.md,涵盖依赖类别、接缝纪律和“替换而非分层”测试。
- 探索替代接口:见 DESIGN-IT-TWICE.md,启动并行子代理以几种截然不同的方式设计接口,然后按深度、局部性和接缝位置进行比较。