深模块改造实战指南:以依赖分类、接缝纪律与“替换而非叠加“测试策略安全深化浅模块(codebase-design / DEEPENING)
2026/9/12 5:45:42 网站建设 项目流程

深模块改造实战指南:以依赖分类、接缝纪律与"替换而非叠加"测试策略安全深化浅模块(codebase-design / DEEPENING)

【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills

导读:本文围绕 skills 仓库中codebase-design技能的配套文档 DEEPENING.md 展开,系统讲解如何在已知依赖的前提下,把一个"浅模块集群"安全地深化为一个深模块。你会掌握四类依赖的分类方法与对应测试方式、判断接缝真伪的"双适配器"纪律,以及"替换而非叠加"的测试重构策略——这些能力可直接用于本仓库的improve-codebase-architecture深化流程,也可迁移到任意需要重构模块边界的代码库。

1. DEEPENING.md 在 codebase-design 中的定位

codebase-design 是整个仓库共享的"深模块设计词汇表":它精确定义了module(模块)interface(接口)depth(深度)seam(接缝)adapter(适配器)leverage(杠杆)locality(局部性)这七个术语,并明确禁止使用 "component""service""API""boundary" 等替代词。

而 DEEPENING.md 是这个词汇表的实操延伸:SKILL.md 负责"定义什么是深模块",DEEPENING.md 负责回答"给定依赖约束时,如何安全地把浅模块集群深化成一个深模块"。它与 DESIGN-IT-TWICE.md(探索替代接口设计)一起,构成了 SKILL.md 中 "Going deeper" 的两个支撑文件。从 docs/engineering/codebase-design.md 可知,该技能是"按需读取"这两个文件,而非一次性加载。

DEEPENING.md 全文围绕三个核心问题组织:

  1. 候选模块的依赖属于哪一类?——类别决定深化后的模块如何跨接缝测试;
  2. 接缝应该放在哪里、何时才算真接缝?——接缝纪律;
  3. 旧测试怎么办?——替换而非叠加的测试策略。

2. 前置词汇:理解本文的三块基石

在深入依赖分类之前,先明确三个必须精确使用的术语(完整定义见 SKILL.md):

术语含义别用
Module(模块)任何"有接口+有实现"的东西,刻意与规模无关:函数、类、包、跨层切片都算unit / component / service
Seam(接缝)Michael Feathers 提出的术语:能在不改动该处代码的情况下改变行为的地方,即模块接口所在的位置boundary
Adapter(适配器)在接缝处满足某个接口的具体事物,描述的是"角色"而非"本质"——内存 fake 和 Postgres repo 都是适配器

DEEPENING.md 第一行就声明"假定读者已掌握 SKILL.md 中的词汇",因此本文后续讨论均沿用这套语言:深化的是"模块",测试跨的是"接缝",注入的是"适配器"。

3. 依赖分类:四种类别决定测试方式

当评估一个深化候选时,第一步是对其依赖分类。类别决定了深化后的模块如何跨接缝进行测试。这是 DEEPENING.md 的核心骨架,原文定义了四类:

类别 1:进程内依赖(In-process)

纯计算、内存态、无 I/O。这类依赖永远可以深化:直接把各模块合并,然后通过新接口直接测试即可,不需要任何适配器

典型场景:工具函数、纯算法、内存数据结构。由于没有外部边界,深化是零成本的——合并后逻辑集中在一处,通过新接口测试即可覆盖全部行为。

类别 2:本地可替换依赖(Local-substitutable)

依赖存在本地测试替身,例如用 PGLite 替代 Postgres、用内存文件系统替代真实文件系统。只要替身存在就可以深化。深化后的模块在测试套件中直接运行这个本地替身进行测试。

关键特征是:接缝是内部的(private 于实现),模块外部接口处没有端口(port)。也就是说,生产代码和测试代码共用同一套依赖解析方式,只是测试时替换了底层存储,无需在接口层做抽象。

类别 3:远程但自有的依赖(Remote but owned,Ports & Adapters)

跨越网络边界的自有服务:微服务、内部 API。此时需要在接缝处定义一个端口(port,即接口)

  • 深模块拥有逻辑
  • 传输层作为适配器注入
  • 测试使用内存适配器(in-memory adapter)
  • 生产使用 HTTP / gRPC / 队列适配器

DEEPENING.md 给出了推荐的表达模板(可直接用于向用户/评审说明设计方案):

"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."(在接缝处定义端口,为生产实现 HTTP 适配器、为测试实现内存适配器,这样即使逻辑跨网络部署,它依然栖身于同一个深模块中。)

这是四类中唯一要求"端口+双适配器"架构的类别,也是 SKILL.md 中 "Accept dependencies, don't create them" 原则的典型应用——模块不自行 new 一个网关,而是接受注入:

// Testable(可测试:依赖注入,端口显式) function processOrder(order, paymentGateway) {} // Hard to test(难测试:模块内部自行创建依赖,接缝被隐藏) function processOrder(order) { const gateway = new StripeGateway(); }

类别 4:真正的外部依赖(True external,Mock)

不受你控制的第三方服务:Stripe、Twilio 等。深化后的模块把外部依赖当作注入的端口接收,测试时提供mock 适配器

与类别 3 的区别在于:类别 3 的服务是你自己的(可以完整设计协议、拥有两端实现),类别 4 的服务协议由第三方决定,你只能 mock 它的对外接口。

四类依赖速查表

类别依赖特征是否可深化测试方式接缝/端口
1. In-process纯计算、内存态、无 I/O总是可以直接通过新接口测试无,无需适配器
2. Local-substitutable存在本地替身(PGLite、内存文件系统)替身存在即可测试套件中运行本地替身内部接缝,外部接口无端口
3. Remote but owned自有服务跨网络边界可以内存适配器外部接缝处定义端口
4. True external第三方服务(Stripe、Twilio)可以mock 适配器外部依赖作为注入端口

4. 接缝纪律:两个适配器才是一个真接缝

DEEPENING.md 用两条规则约束"在哪里切接缝、切多深":

4.1 一个适配器 = 假设性接缝;两个适配器 = 真实接缝

One adapter means a hypothetical seam. Two adapters means a real one.

不要仅仅因为"可能有用"就引入端口。除非至少有两个适配器被证明有必要(典型是生产 + 测试),否则不要引入端口。只有一个适配器的接缝只是多余的间接层(indirection)。

这条纪律与 SKILL.md 的四原则之一完全一致(见 SKILL.md:"Don't introduce a seam unless something actually varies across it")。docs/engineering/codebase-design.md 也把它列为验收标准之一:"A proposed seam comes with a second adapter named, not just the first one"——任何接缝提案都必须能说出第二个适配器,否则该提案不合格。

4.2 内部接缝 vs 外部接缝

一个深模块可以同时拥有两类接缝:

  • 内部接缝(internal seams):私有于实现、仅供模块自身测试使用;
  • 外部接缝(external seam):位于模块接口处,对调用者可见。

不要因为测试用到了内部接缝,就把它们暴露到接口上。SKILL.md 对此有对应的原则("Depth is a property of the interface, not the implementation"):深模块内部完全可以由许多小的、可 mock、可替换的部件组成——它们只是不属于接口而已。DEEPENING.md 中的"类别 2(Local-substitutable)"正是内部接缝的典型:测试在套件里用 PGLite,但模块对外接口上没有任何端口。

5. 测试策略:替换(replace),不要叠加(layer)

深化重构最大的误区是"保留旧测试,再加新测试"。DEEPENING.md 给出的策略恰好相反:

5.1 删除旧测试

一旦深模块接口处的测试存在,浅模块上的旧单元测试就变成了浪费(waste)——删掉它们

旧的浅模块测试断言的是旧接口/旧实现形状;深化后它们描述的对象已不存在,留着只会维护两份"过时真相"。这与本仓库 tdd 技能"测试要能存活于重构"的理念一脉相承:测试应该描述行为,而不是实现

5.2 接口就是测试面(The interface is the test surface)

新测试全部写在深模块的接口处。理由很直接:调用者与测试跨越的是同一个接缝(SKILL.md 原则三)。如果你想"测试接口背后的东西",说明模块的形状很可能错了。

5.3 断言可观察结果,而非内部状态

测试断言的是通过接口产生的可观察结果,绝不触碰内部状态。内部状态是实现的私有细节,断言它就等于把测试绑死在实现上。

5.4 测试应能在内部重构中存活

如果实现一变,测试就得跟着变,说明你在测试接口之外的东西(testing past the interface)。

这是"接口即测试面"的直接推论,也是判断测试写得好不好的金标准。DEEPENING.md 最后一句是对全篇的收束:深化的目标之一就是让测试描述行为(behaviour),从而在重构(包括进一步深化)时无需改动。

替换策略的实施清单

  1. 在深模块新接口处编写覆盖全部可观察行为的测试;
  2. 运行新测试确认覆盖旧测试的行为面;
  3. 删除旧浅模块的单元测试;
  4. 继续任何内部重构,观察新测试是否"无感存活"——若需要改动,说明断言越过了接口。

6. 实战串联:从发现候选到完成深化

DEEPENING.md 不是孤立的——它是 improve-codebase-architecture 深化流程中"决定接缝形状"环节的支撑文档。完整链路如下:

improve-codebase-architecture(扫描代码库,发现浅模块候选) ↓ 应用 deletion test(删除它复杂度是消失还是扩散?) codebase-design / DEEPENING.md(对候选依赖分类 → 决定接缝与测试方式) ↓ DESIGN-IT-TWICE.md(并行子代理产出多个接口设计,按 depth/locality/seam 对比) ↓ 写测试于新接口 → 删除旧浅模块测试(replace, don't layer)

从 improve-codebase-architecture/SKILL.md 可以看到,扫描阶段就用上了本文件的概念:寻找"接口几乎和实现一样复杂的浅模块"、检查"紧耦合模块是否跨接缝泄漏"、对可疑模块做deletion test("删除它,复杂度是集中了,还是只是移动了?——'是,集中了' 正是你想要的信号")。

落地案例:把四类依赖映射到典型后端模块

假设你在深化一个 "订单处理" 模块集群,可对照四类依赖逐一处理:

依赖类别处理方式
折扣计算逻辑1. In-process直接合并进深模块,通过新接口测试
Postgres 存储2. Local-substitutable测试套件用 PGLite,内部接缝,外部无端口
自有库存微服务3. Remote but owned端口 + 生产 HTTP 适配器 + 测试内存适配器
Stripe 支付4. True external作为注入端口,测试提供 mock 适配器

可选加固:用工具强制接缝不被绕过

DEEPENING.md 解决的是"接口设计"问题;如果你还想防止未来的 import 绕过接口,仓库 in-progress/setup-ts-deep-modules 提供了基于 dependency-cruiser 的落地方法:以"包根文件=入口点、子文件夹=私有"的路径深度规则强制边界(对应 DEEPENING.md 中"内部实现私有于接口"的纪律)。注意:该技能处于 in-progress(beta)桶,且按 docs/engineering/codebase-design.md 的说明,它没有附带 lint 规则之外的进一步保障,属于可选项而非必需。

7. 常见误区与边界

  • 误区:把"深"定义为实现行数与接口行数之比。这是 Ousterhout 的原定义,但本技能明确拒绝它,因为该指标会奖励"注水实现"(padding the implementation)。本项目用depth-as-leverage(接口处的杠杆:调用者/测试每学习一单位接口所能驱动的行为量)替代。详见 SKILL.md 的 Rejected framings 与 docs/engineering/codebase-design.md。
  • 误区:把"接口"等同于 TypeScript 的interface关键字或类的公有方法。本项目中的 interface 包含调用者正确使用模块所需的一切事实:类型签名、不变量、顺序约束、错误模式、必需配置、性能特征。
  • 误区:见到网络边界就上 Ports & Adapters。只有自有且跨网络的依赖(类别 3)才需要端口+双适配器;本地有替身的(类别 2)不要过度设计端口,纯进程内依赖(类别 1)更是零成本深化。
  • 误区:只提一个适配器就宣布"这是接缝"。按接缝纪律,一个适配器只是假设性接缝,属于多余间接层;必须能说出第二个适配器(通常是生产 + 测试)才成立。
  • 误区:深化后保留旧浅模块测试。那是在"叠加"而不是"替换",旧测试会变成必须维护的浪费。

8. 相关资源

  • codebase-design 词汇表与四原则:本文依赖的全部术语定义、深/浅模块示意图、可测试性三条编码规则(接受依赖而非创建、返回结果而非副作用、小表面积)。
  • DESIGN-IT-TWICE.md:选定深化候选后,并行子代理产出 3+ 个激进不同的接口设计,按 depth / locality / seam placement 对比,其依赖分类正是引用本文件的类别。
  • improve-codebase-architecture:负责"发现"浅模块候选的扫描技能,本文的 deletion test 与依赖分类在其流程中被直接引用。
  • docs/engineering/codebase-design.md:该技能的权威文档页,含词汇表、四原则、常见问题(含"如何实际构建 TS 深模块"的三种强制手段讨论)。
  • tdd:共享同一套 deep-module 词汇;"seam" 在 tdd 中指"测试所在的边界",而 codebase-design 拥有它背后的模块形状。
  • setup-ts-deep-modules 与其 dependency-cruiser.config.cjs:用工具强制"入口点即唯一通路"的可选加固方案(in-progress,beta 桶)。

9. 总结

DEEPENING.md 用极简篇幅给出了深化重构的完整决策链:先对依赖分类(进程内 / 本地可替换 / 远程但自有 / 真外部),类别决定测试如何跨接缝;再守接缝纪律(一个适配器是假设、两个才是真实;内部接缝不暴露到外部接口);最后执行替换而非叠加的测试策略(新接口测试成型即删除旧测试,断言可观察结果,测试必须能存活于内部重构)。把这三步纳入improve-codebase-architecture的深化流程,就能让"合并浅模块"从凭感觉的重构,变成有分类依据、有接缝论证、有测试收尾的工程操作。

【免费下载链接】skillsSkills for Real Engineers. Straight from my .agents directory.项目地址: https://gitcode.com/GitHub_Trending/skills13/skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询