代码设计
设计深度模块:大量行为隐藏在简洁的接口之后,接口位于清晰的接缝处,并可通过该接口进行测试。在设计和重构代码的任何地方使用这些语言和原则。目的是为调用方提供杠杆效应,为维护者提供局部性,为所有人提供可测试性。
术语表
请精确使用这些术语:不要用“组件”、“服务”、“API”或“边界”来替代。保持语言一致是重点。
模块:任何具有接口和实现的事物。刻意保持规模无关性:可以是一个函数、类、包或跨层切片。避免使用:单元、组件、服务。
接口:调用方为正确使用模块而必须了解的一切:包括类型签名,以及不变量、顺序约束、错误模式、所需配置和性能特征。避免使用:API、签名(过于狭窄,仅指类型层面的表面)。
实现:模块内部的内容,其代码主体。与适配器不同:某个东西可以是带有大型实现的小型适配器(如PostgreSQL仓库),也可以是带有小型实现的大型适配器(如内存模拟)。当讨论主题是接缝时,使用“适配器”;否则使用“实现”。
深度:接口处的杠杆效应。调用方(或测试)每学习一个接口单元所能触发的行为量。当一个模块通过简洁的接口封装了大量行为时,它被称为深度模块;当接口的复杂度和实现几乎相当时,则称为浅度模块。
接缝 (Michael Feathers):能够在不直接修改某处的情况下改变其行为的位置;它是模块接口所在的位置。确定接缝的位置本身就是一个设计决策,与接口背后是什么不同。避免使用:边界(与DDD的限界上下文概念混淆)。
适配器:在接缝处满足某个接口的具体事物。它描述的是角色(填充什么槽位),而不是实质内容(内部是什么)。
杠杆效应:调用方从深度中获得的好处。即每学习一个接口单元,就能获得更多的能力。一份实现能够回馈N个调用点和M个测试。
局部性:维护者从深度中获得的好处。变更、缺陷、知识和验证集中在同一个地方,而不是散布在调用方之间。一次修复,处处生效。
深度与浅度对比
深度模块 = 小接口 + 大量实现:
┌─────────────────────┐
│ 简洁的接口 │ ← 少量方法,简单参数
├─────────────────────┤
│ │
│ 深层的实现 │ ← 隐藏复杂逻辑
│ │
└─────────────────────┘
浅度模块 = 大接口 + 少量实现(避免):
┌─────────────────────────────────┐
│ 大型接口 │ ← 许多方法,复杂参数
├─────────────────────────────────┤
│ 薄薄的实现 │ ← 只是透传
└─────────────────────────────────┘
在设计接口时,请问:
- 我能否减少方法的数量?
- 我能否简化参数?
- 我能否将更多复杂性隐藏在接口内部?
原则
- 深度是接口的属性,而非实现的属性。 一个深度模块的内部可以由可模拟、可替换的小部分构成;但它们不是接口的一部分。一个模块可以拥有内部接缝(对其实现私有,由其自身的测试使用)以及位于其接口处的外部接缝。
- 删除测试。 想象删除该模块。如果复杂性消失,那它就是个透传。如果复杂性重新出现在N个调用者那里,那它本身就值得存在。
- 接口即测试面。 调用者和测试跨越相同的接缝。如果你想绕过接口进行测试,那模块的形状可能有问题。
- 一个适配器意味着一个假设的接缝。两个适配器才意味着一个真实的接缝。 除非有东西确实跨接缝变化,否则不要引入接缝。
可测试性设计
好的接口使测试变得自然:
-
接受依赖,而不是创建它们。
// 可测试 function processOrder(order, paymentGateway) {} // 难以测试 function processOrder(order) { const gateway = new StripeGateway(); } -
返回结果,而不是产生副作用。
// 可测试 function calculateDiscount(cart): Discount {} // 难以测试 function applyDiscount(cart): void { cart.total -= discount; } -
表面面积小。 方法越少 = 需要的测试越少。参数越少 = 测试设置越简单。
关系
- 模块 有且只有一个 接口(它向调用者和测试展示的表面)。
- 深度 是 模块 的属性,依据其 接口 来衡量。
- 接缝 是 模块 的 接口 所在之处。
- 适配器 位于 接缝 处并满足 接口。
- 深度 为调用者产生 杠杆效应,为维护者产生 局部性。
已放弃的表述
- 深度作为实现行数与接口行数的比率(Ousterhout):奖励填充实现。我们使用深度即杠杆效应的概念。
- “接口”作为TypeScript的
interface关键字或类的公共方法:过于狭窄:这里的接口包含调用方必须知道的全部事实。 - “边界”:与DDD的限界上下文混淆。请说接缝或接口。
深入探索
- 根据依赖关系深化一个集群,请参阅 DEEPENING.md:依赖分类、接缝纪律和替换而非分层测试。
- 探索替代接口,请参阅 DESIGN-IT-TWICE.md:启动并行子代理,以多种截然不同的方式设计接口,然后比较深度、局部性和接缝位置。