代码库设计
设计深度模块:将大量行为放在小型接口后面,放置在干净的接缝处,并通过该接口进行测试。无论代码正在被设计还是重构,都使用此语言和这些原则。目标是调用者的杠杆作用、维护者的局部性以及所有人的可测试性。
术语表
务必使用这些术语,不要用“组件”“服务”“API”或“边界”替代。一致的语言是关键。
模块:任何具有接口和实现的事物。有意跨缩放级别通用:一个函数、类、包或跨层切片。避免:单元、组件、服务。
接口:调用者在正确使用模块时必须知道的一切:类型签名,还有不变性、顺序约束、错误模式、所需配置和性能特征。避免:API、签名(过于狭窄,仅指类型层面的表面)。
实现:模块内部的内容,其代码体。与适配器不同:一个东西可以是小型适配器+大型实现(如Postgres仓库),也可以是大型适配器+小型实现(如内存模拟)。当接缝是主题时用“适配器”;否则用“实现”。
深度:接口处的杠杆作用。调用者(或测试)每学会一个接口单位所能行使的行为量。当大量行为隐藏在小型接口后面时,模块是深度的;当接口几乎和实现一样复杂时,则是浅度的。
接缝(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:启动并行子代理,以多种根本不同的方式设计接口,然后在深度、局部性和接缝放置方面进行比较。