代码库设计
设计深层模块:将大量行为置于小而简洁的接口背后,并将该接口放在调用者和测试都能触及的接缝处。目标是为调用者提供杠杆效应,为维护者提供局部性,并让测试能够抵御重构。
规则
- 深度是接口的属性,不是实现的属性。 一个深层模块可以由多个可互换的小部件构成;但它们不属于其接口的一部分。模块内部可以为自身测试保留接缝,同时也拥有接口处的外部接缝。
- 执行删除测试。 想象删除这个模块。如果复杂性随之消失,它只是一个透传层。如果复杂性在调用者之间重新出现,那么它就有存在的价值。
- 接口就是测试表面。 调用者和测试跨越的是同一个接缝。如果你需要绕过接口进行测试,模块的形状很可能不对。
- 一个适配器意味着假设的接缝;两个才让它成为现实。 仅在确实存在差异的地方(通常是生产环境和测试环境)添加接缝。
- 缩小接口。 更少的方法、更简单的参数、隐藏更多内部复杂性。
- 接受依赖而非创建依赖。 将支付网关作为参数传入,而不是在函数内部构造它。
- 优先返回结果而非产生副作用,只要设计允许这样做。
词汇表
请精确使用以下术语。不要替换为“组件”、“服务”、“API”或“边界”。
- 模块: 任何具有接口和实现的东西,无论规模大小:一个函数、类、包或跨层切片。
- 接口: 调用者正确使用模块所需知道的一切:类型、不变条件、顺序、错误模式、必需的配置和性能。不仅仅是签名。
- 实现: 模块内部的代码。一个小适配器可以有很大的实现(例如 Postgres 仓库),而一个大适配器可以有很小的实现(例如内存模拟)。
- 深度: 接口处的杠杆效应,即调用者或测试每学习一个接口单元所能获得的行为。当大量行为位于小巧的接口背后时,模块是深的;当接口的复杂度几乎与实现相当,模块就是浅的。
- 接缝(Michael Feathers):一个无需编辑该位置即可改变行为的位置;即模块接口所在之处。选择接缝本身就是一项决策。
- 适配器: 在接缝处填充接口的具体东西。它命名的是一个角色,而不是内容。
- 杠杆效应: 调用者从深度中获得的收益。一个实现能回报多个调用点和测试。
- 局部性: 维护者从深度中获得的收益。变更、缺陷、知识和验证都集中在一个地方。
已拒绝的表述:深度是实现行数与接口行数的比率,因为它鼓励填充;将“接口”仅理解为一个类型关键字或公共方法,因为太狭隘了;“边界”,因为它与 DDD 的限界上下文冲突。
如果项目维护了 CONTEXT.md 术语表,请在此术语的基础上同时使用其中的领域术语。
深入探讨
- 要按依赖类型加深一组浅层模块,请阅读 DEEPENING.md。
- 要比较几种截然不同的接口方案,请阅读 DESIGN-IT-TWICE.md。它可能会启动三个或更多子代理,因此只在用户明确要求时才使用。