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