用途
该技能用于回答“代码应该放在哪里”以及“每个模块应该隐藏什么”。它把 Parnas 1972 年提出的信息隐藏原则作为唯一规则:每个模块拥有一个秘密——一个可能变化的设计决策——并把它藏在比秘密本身更小的接口之后。
模块是任何具有接口和实现的东西,与大小无关:一个应用切片(如 Nuxt layer 或 workspace package)、一个目录、一个文件、一个类、一个函数。规则在每个粒度上都相同,只是缩放不同。其收益是局部性:一次变更、一个 bug、一个测试,以及一个 agent 必须加载的上下文,都留在同一个模块内。
若要塑造单个模块的接口(深度、接缝、适配器),使用 /codebase-design。
主要能力
四项检查
对每个创建、移动或审计的模块运行这四项检查。全部通过才算完成。
- 单一秘密:用领域词汇完成“当 ___ 时它会变化”,不要提及技术。两个答案意味着两个模块;一个答案分散在多个模块中意味着它们是一个模块。
- 深:接口比它隐藏的东西更小。删除测试:删除该模块,如果其复杂性消失而不是在调用方重新出现,说明它只是一个透传,应内联。
- 封装:调用方和测试只能通过接口触及秘密。它们导入模块的入口点,而不是其背后的文件。接口携带领域类型,因此 ORM 行、SDK 对象和 HTTP 响应留在内部。没有调用方需要知道内部步骤的顺序。测试若必须越过接口,说明接缝位置错误。
- 有向:模块之间的依赖形成无环图,并指向更稳定的模块。耦合是平衡的:两个模块越近,它们可以互相知道得越多。同一模块内的文件可以共享内部;不同切片只共享窄接口。
缩放层级
应用:切片
按业务能力切分应用,每个切片一个限界上下文:auth、billing、email。每个切片拥有自己的 UI、逻辑、数据访问和测试。Nuxt layer、workspace package 和顶层目录是同一动作在不同工具成本下的体现。
一个切片发布一个入口点(layer exports、index.ts、package exports map),其他切片只使用它。一个切片拥有自己的表;其他切片通过其接口询问。
在切片内部,保持决策纯净,把 I/O 推到边缘:函数式核心,命令式外壳。核心无需 mock 即可测试。I/O 只在存在两个适配器时才放在端口和适配器之后;生产加一个测试替身也算。
同一个词在两个切片中可能含义不同(auth 和 billing 中的 Account)。给每个切片自己的模型,并在接口处翻译。
共享内核只保存每个切片都同意且很少变化的概念,如 money 和 ids。保持它极小。
目录:模块
先按功能分组,只在功能内部按技术角色分组:目录树应该喊出领域,而不是框架。一个切片拥有自己的 components/ 和 composables/(如 Nuxt layer)就是这条规则;一个顶层 components/ 容纳所有功能的组件则违反它。
目录模块的入口文件就是它的接口。其中其他所有文件都是私有的。
按知识拆分,而不是按执行顺序。fetch.ts、parse.ts 和 save.ts 如果都知道文件格式,那就是一个秘密分在三个文件里(时间分解)。
当两个切片长出相似代码时,重复直到第三次使用显示出真正的概念(三次法则)。错误的共享抽象会耦合切片;重复只花费打字成本。
文件、类、函数
当一个文件持有两个秘密时拆分;共享一个秘密的文件合并。大小是查看的理由,不是拆分的理由:一个长文件如果在一个小接口后只有一个秘密,那就是一个深模块。
一个类导出尽可能少的东西。每个字段一个 getter 和 setter 会公开其表示;给调用方他们需要的行为(告诉,不要询问)。
函数的参数就是它的接口。一个切换行为的布尔标志意味着两个函数。一个只透传给被调用方的参数会把被调用方的秘密向上泄漏。
文件内部的布局(报纸顺序、stepdown)属于 /clean-code。
命名
用领域的语言,按秘密命名模块:billing/、invoice-pdf/。需要“and”的名字标志着两个秘密。
放置新代码
- 命名它的秘密。完成“当 ___ 时它会变化”。当答案命名的是领域概念而非技术时完成。
- 找到拥有该秘密的模块。在切片、目录和文件缩放层级检查同级,而不仅仅是你所在的文件。当你有了归宿,或已证明不存在归宿时完成。
- 放置它,或创建一个以该秘密命名的同级模块,带其入口点。当其邻居与它在同一触发条件下变化时完成。
- 对你触及的每个模块运行四项检查。全部通过时完成。
强制边界
仅靠纪律维持的边界会侵蚀。当边界重要时,提出一个在违规时使 CI 失败的适应度函数:导入边界 lint 规则(eslint-plugin-boundaries、dependency-cruiser、import/no-cycle)、TypeScript 项目引用,或 package exports map。
坏味道
每个坏味道都是一项失败的检查。移动任何东西之前先命名所涉秘密。
| 坏味道 | 失败检查 | 修复 |
|---|---|---|
| 发散式变化:一个模块因不相关原因被编辑 | 单一秘密 | 按原因拆分 |
| 霰弹式修改:一次变更编辑许多模块 | 单一秘密 | 聚集到一个模块 |
| 层优先的顶层:controllers/、services/、models/ | 单一秘密 | 按功能重新打包 |
| 通用桶:utils/、common/、helpers/、manager | 单一秘密 | 把每项移到它服务的模块 |
| 特性依恋:代码主要使用另一个模块的数据 | 单一秘密 | 移到那个模块 |
| 时间分解 | 单一秘密 | 按共享知识合并 |
| 一个模型被含义不同的切片共享 | 单一秘密 | 每个切片一个模型,在接口处翻译 |
| 透传模块或包装器 | 深 | 内联它 |
| 类炎:许多小模块,每个接口很宽 | 深 | 合并为一个深模块 |
| 深导入:billing/internal/stripe | 封装 | 导入入口点;若缺少东西则加宽它 |
| 接口中的基础设施类型 | 封装 | 在接缝处映射到领域类型 |
| 领域代码导入框架、数据库或 UI | 有向 | 领域定义端口;基础设施适配它 |
| 导入循环 | 有向 | 把共享概念移到一个模块或内核,或合并 |
使用方式
该技能以规则和检查清单的形式提供,供在创建、移动或审计模块时逐项应用。它不提供安装命令或代码示例;具体执行依赖使用者将其原则应用到自己的代码库中。
适用场景
- 决定新代码应放在哪个目录或模块。
- 审计现有代码结构,识别坏味道并修复。
- 设计应用切片、目录模块、文件、类或函数的边界。
- 在 CI 中强制导入边界。
限制
资料未提供该技能的具体安装方式、运行环境、依赖工具或平台支持信息。它是一套设计原则与检查清单,而非可执行工具;实际效果取决于使用者对原则的理解与应用。