用途
Codebase Design 提供一套共享词汇和原则,用于在任何代码库中设计“深模块”(deep modules)。深模块指大量行为隐藏在小型接口之后,放置在清晰的接缝(seam)处,并可通过该接口进行测试。该技能适用于用户想要设计或改进类或模块的接口、寻找深化机会,或重构代码的场景。其目标是:为调用者带来杠杆效应(leverage),为维护者带来局部性(locality),为所有人带来可测试性。
术语表
该技能强调使用精确术语,不要用“component”“service”“API”或“boundary”替代。
- Module(模块):任何具有接口和实现的东西。刻意与规模无关:可以是函数、类、action、包或跨层切片。避免使用:unit、component、service。
- Interface(接口):调用者正确使用模块所需知道的一切:方法签名,还包括不变量、顺序约束、错误模式、必需配置和性能特征。避免使用:API、signature(过于狭窄,仅指类型层面的表面)。
- Implementation(实现):模块内部的内容,即其代码主体。与 Adapter 不同:一个东西可以是小适配器配大实现(如 ORM 支持的 repository),也可以是大适配器配小实现(如内存 fake)。当接缝是主题时用“adapter”,否则用“implementation”。
- Depth(深度):接口处的杠杆效应:调用者(或测试)每学习一单位接口所能行使的行为量。当大量行为位于小接口之后时,模块是深的;当接口几乎与实现一样复杂时,模块是浅的。
- Seam(接缝):Michael Feathers 提出的概念;一个可以在不编辑该处的情况下改变行为的地方;模块接口所在的位置。接缝放在哪里是独立的设计决策,与接缝后面放什么不同。避免使用:boundary(与 DDD 的 bounded context 混淆)。
- Adapter(适配器):在接缝处满足接口的具体事物。描述角色(填充什么槽位),而非实质(内部是什么)。
- Leverage(杠杆效应):调用者从深度中获得的东西:每学习一单位接口获得更多能力。一个实现可在 N 个调用点和 M 个测试中得到回报。
- Locality(局部性):维护者从深度中获得的东西:变更、bug、知识和验证集中在一处,而非分散到调用者中。一次修复,处处修复。
深与浅
深模块 = 小接口 + 大量实现:
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
└─────────────────────┘
浅模块 = 大接口 + 少量实现(应避免):
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
设计接口时,问自己:
- 我能减少方法数量吗?
- 我能简化参数吗?
- 我能把更多复杂性隐藏在内部吗?
原则
- 深度是接口的属性,而非实现的属性。深模块内部可以由小的、可替换的部分组成;它们只是不属于接口。模块可以有内部接缝(对其实现私有,供自身测试使用)以及接口处的外部接缝。
- 删除测试:想象删除该模块。如果复杂性消失,它只是一个直通(pass-through)。如果复杂性在 N 个调用者中重新出现,它就在发挥价值。
- 接口即测试表面:调用者和测试跨越同一个接缝。如果你想测试接口之外的东西,模块的形状可能不对。
- 一个适配器意味着假设的接缝。两个适配器意味着真实的接缝。 除非确实有东西在接缝处变化,否则不要引入接缝。
映射到技术栈
这些概念在典型应用框架中的落点:
- 依赖注入是接缝机制:一个接口(或 protocol、trait、函数类型)加上将实现连接进来的东西。在采用之前应用两适配器规则:只有一个实现、为了 mock 而连接的接口是假设的接缝。框架 fake 通常使其不必要。
- 框架 fake 是框架拥有的接缝处的适配器:框架提供的测试文件系统、队列、邮件器或 HTTP stub 是该接缝的第二个适配器:免费提供。对于框架已拥有的 I/O,优先使用它们而非手工打造的端口。
- ORM 模型是宽泛的共享接口:每个属性、查询和关系都是每个调用者可用的表面区域。深度通常位于它们之上:一个 action 或 service,其小接口(如 reconcileInvoice(invoice))隐藏了查询、状态转换和副作用。
- 控制器、路由处理器、CLI 命令、job 和 UI 组件是入口点适配器:满足传输接口并委托给深模块的薄东西。如果真实逻辑在其中积累,那就是深化候选(同样的逻辑很快会从第二个入口点需要)。
技术栈参考:从清单文件识别技术栈。如果存在匹配文件,阅读它以了解该技术栈术语中的这些概念。
- Laravel(composer.json 中的 laravel/framework):references/laravel.md
为可测试性而设计
好的接口使测试自然:
- 接受依赖,不要创建它们:通过构造函数或参数传入的支付网关可以在测试中替换为 fake。在方法内部从配置构建的(new StripeGateway(secret))则不能。
- 返回结果,不要产生副作用:calculateDiscount(cart) → Discount 是纯计算,可直接断言。applyDiscount(cart) 会变更并持久化,只能通过检查其改变来测试。
- 小表面区域:更少方法 = 需要更少测试。更少参数 = 更简单的测试设置。
关系
- 一个 Module 恰好有一个 Interface(它呈现给调用者和测试的表面)。
- Depth 是 Module 的属性,相对于其 Interface 衡量。
- Seam 是 Module 的 Interface 所在之处。
- Adapter 位于 Seam 处并满足 Interface。
- Depth 为调用者产生 Leverage,为维护者产生 Locality。
被拒绝的框架
- 深度作为实现行数与接口行数的比率(Ousterhout):奖励填充实现。我们使用深度即杠杆效应。
- “Interface”作为语言的 interface 关键字或类的公共方法:过于狭窄,这里的 interface 包括调用者必须知道的每个事实。
- “Boundary”:与 DDD 的 bounded context 混淆。说 seam 或 interface。
深入
- 给定依赖关系深化一个集群:参见 references/deepening.md:依赖类别、接缝纪律和替换而非分层测试。
- 探索替代接口:参见 references/design-it-twice.md:启动并行子代理,以几种截然不同的方式设计接口,然后比较深度、局部性和接缝放置。