codebase-design 是一套用来谈论模块设计的共享词汇:
什么算模块、接口、深度、接缝,每个词都有精确定义,不许随便换成近义词。
你什么时候会碰到它?当你想说「这个模块只是在转发调用」却找不到准确的词时,
或者当 improve-codebase-architecture、tdd、to-spec
这些 skill 的正文里引用这套词汇时。
词汇本身在 SKILL.md 里,它还有两个搭档文件:
DEEPENING.md 讲怎么把浅模块加深,DESIGN-IT-TWICE.md 讲怎么并行设计几种接口。
学完这节课,你能用这套词判断一个模块是深还是浅、接缝放得对不对,
也知道行为不对时该去改哪个文件的哪一段。
0001 把 22 个 skill 分成三层:配置层(跑一次性的初始设置)、编排层(你手动启动的完整流程)、
纪律层(被反复调用的基本功)。codebase-design 属于纪律层,而且是纪律层里特殊的一类:
它不规定流程,只规定词汇,作者把这类 skill 叫词汇地板(ask-matt 里写作
Vocabulary underneath)——主流程上的 skill 都可以踩在这块地板上说话。
它是 model-invoked 的(人和 AI 都能启动):别的 skill 想用它,
只要在正文里写一句自然语言的「Run the /codebase-design skill」就能把它拉进来
(0001 讲过,这叫 prose 调用),你也可以单独把它当参考书翻。
它和 0004 讲的 domain-modeling 是一对平行的词汇地板,各管一套词:
| 词汇地板 | 管哪套词 | 帮你纠正什么 | 主要用在哪 |
|---|---|---|---|
| domain-modeling | 领域语言(Order、Account 这些业务概念) | 概念模糊、一个词有几个意思、不可逆的决策 | 写进 CONTEXT.md 和 ADR(架构决策记录) |
| codebase-design | 模块形状的词(module、interface、seam…) | 浅模块、接缝放错位置、代码不可测、接口太大 | 设计讨论;被 architecture / tdd / to-spec 引用 |
ask-matt 的原文把这两个地板放在 Vocabulary underneath 一节: 当你的问题是用词问题、而不是要跑一整条流程时,直接用它; 如果是跑流程的过程中碰到用词问题,让上面那个 skill 把它拉进来就行。 注意别把两件事混进同一段对话:「改领域术语」和「加深模块形状」是两块地板, 混着用,两边留下的记录都会被搞脏。
罗盘:MISSION.md · 地图:0001 · 平行的一课:0004 domain-modeling · 路由规则:ask-matt/SKILL.md 的 Vocabulary underneath 一节
docs 和 SKILL.md 的说法一致:这个 skill 讲的是怎么设计
deep module(深模块)——一个模块把大量行为藏在一个小接口后面,
立在一条干净的 seam(接缝,就是不用改那处代码就能换行为的位置)上,
并且通过那个接口就能测试。目标是:给调用它的人 leverage
(杠杆,学会一点接口就能驱动很多行为),给维护它的人 locality
(局部性,改动和知识集中在一处),给所有人可测性。
这几个词在第 3 节都有精确定义,先有个印象就行。
codebase-design 是 deep-module 词汇的唯一权威出处(single source of truth)。
作者故意把它拆成一个独立的、人和 AI 都能启动的 skill,
这样任何别的 skill 只要引用它,而不用把同样的定义再抄一遍。
它不替你重构代码、不给你重构计划、也不写架构 HTML 报告——
那些是 improve-codebase-architecture 那条流程干的事。
你单独用它时,做的是「把词说对、把模块形状想清楚」,
不会触发 architecture 那条「扫描 → 报告 → 面试(grilling)」的流水线。
frontmatter 里的 description 字段写明了触发条件:当你要设计或改进一个模块的接口、
想找加深模块的机会、要决定接缝放在哪、想让代码更好测或对 AI 更好读,
或者别的 skill 需要这套词汇时——用它。
| 情境 | 该用 codebase-design 吗 |
更该去哪 |
|---|---|---|
| 接口该长什么样、接缝切在哪、模块够不够深 | 该(给你词汇、原则,还能并行设计几种接口) | — |
| 扫描整个仓库找浅模块、出一份 HTML 候选报告、再面试着挑一个加深 | 它会被 architecture 在内部拉进去 | improve-codebase-architecture(只能人启动) |
| 做红绿循环(先写测试再写实现)、在约定好的接缝上写测试 | 说的是同一套词(接缝、接口) | tdd;implement 内部也应该用 tdd |
| 写需求文档时画测试接缝 | 词汇保持一致 | to-spec(它会先画接缝草图) |
| 「account 这个词同时干三份活」 | 不该(那是领域词的问题) | domain-modeling |
| 不知道该走哪条流程 | 不该 | ask-matt |
权威原文: skills/engineering/codebase-design/SKILL.md · 给人看的叙事版: docs/engineering/codebase-design.md (aihero.dev/skills-codebase-design)
SKILL.md 开篇就要求:用这些词,不要换成 component / service / API / boundary。
词汇统一是这个 skill 存在的理由——换成近义词,会把真正要区分的差别抹掉。
下面这张表把每个词的定义按 SKILL.md 原文压缩了一遍,第一次读可以慢一点。
| 术语 | 定义(按 SKILL.md 原文压缩) | 故意不用的近义词 | 常见误用 |
|---|---|---|---|
| Module(模块) | 任何「有接口 + 有实现」的东西都算模块。它故意不限定尺度:一个函数、一个类、一个包、一个跨层的切片,都行 | unit、component、service | 默认「一个模块 = 一个文件 / 一个微服务」——尺度理解错了,后面就没法谈深度 |
| Interface(接口) | 调用者想正确使用这个模块,必须知道的一切:类型签名、必须恒成立的条件(invariants)、调用顺序的约束、出错时的表现、必填的配置、性能特征 | API、signature(这两个词太窄,只指类型那一面) | 把 TypeScript 的 interface 关键字、或者一份 public 方法清单,当成接口的全部 |
| Implementation(实现) | 模块内部的那坨代码 | — | 和 adapter 混着用:谈「模块里面是什么」用 implementation;谈「接缝上插的是哪个件」用 adapter |
| Depth(深度) | 衡量接口上的杠杆:调用者(或测试)每学会一单位接口,能驱动多少行为。行为多、接口小 → 深(deep);接口几乎和实现一样复杂 → 浅(shallow) | — | 拿「实现行数 ÷ 接口行数」当深度——这个算法被明确否定了 |
| Seam(接缝) Feathers |
不用改那处的代码、就能改变行为的位置;也就是模块接口所在的位置。接缝放哪,是一个独立的设计决策 | boundary(和 DDD 的 bounded context「限界上下文」撞车) | 把任何 import 边界都叫接缝;或者测试偷偷摸进私有内部,还自称「测在接缝上」 |
| Adapter(适配器) | 插在接缝上、满足某个接口的具体东西。这个词描述的是角色(填哪个槽),不描述它内部是什么 | — | 同一个 Postgres 仓库,谈体量时可以是「小 adapter + 大 implementation」;一个内存 fake 也可以是「adapter 角色重、implementation 很小」——按当下的话题选词 |
| Leverage(杠杆) | 调用者从深度得到的好处:每学一单位接口,得到更多能力。一份实现,在 N 个调用点和 M 个测试上反复回收价值 | — | 只数「功能列表长」,不看「接口是不是仍然小」 |
| Locality(局部性) | 维护者从深度得到的好处:变更、缺陷、知识、验证都集中在一处,而不是散落在各个调用者之间。修一次,处处都修好 | — | 把「为了单测,把纯函数拆得到处都是」误当成局部性——那其实是局部性的反面 |
Deep module Shallow module(要避免)
┌─────────────────────┐ ┌─────────────────────────────────┐
│ Small Interface │ 少方法、简参 │ Large Interface │ 多方法、复杂参
├─────────────────────┤ ├─────────────────────────────────┤
│ │ │ Thin Implementation │ 几乎只转发
│ Deep Implementation│ 复杂逻辑隐藏 │ │
│ │ └─────────────────────────────────┘
└─────────────────────┘
设计接口时,问自己三个问题:
· 能否减少方法数?
· 能否简化参数?
· 能否把更多复杂度藏进里面?
想象把这个模块整个删掉,然后看复杂度去了哪:
improve-codebase-architecture 在找浅模块时会主动套用这个测试,问:
「删掉它,复杂度是被集中消灭了,还是只是搬了个家?」
答案是「会集中」,才是你想要的加深信号。
四条原则全部围绕一件事:深度是长在接口上的,不是长在实现上的。
| 原则 | 意思 | 实际后果 |
|---|---|---|
| Depth 是接口的属性,不是实现的属性 | 深模块内部可以拆成小的、可仿冒(mock)、可替换的零件——只要这些零件不出现在对外接口里 | 允许两种接缝并存:内部接缝(实现私有,只给自己的测试用)和外部接缝(对外接口);别为了测试把内部接缝暴露成公共接口 |
| 删除测试(deletion test) | 见 §4.1 | 筛掉「看着像有一层、实际只在转发」的假模块 |
| 接口就是测试面(Interface is the test surface) | 调用者和测试走同一条接缝 | 如果你想「穿过」接口去测内部,多半是模块形状错了 |
| 一个 adapter = 假想的接缝;两个 adapter = 真接缝 | 不存在「将来真的要换实现」的理由,就不要开接缝 | 典型的正当双 adapter 是「生产 + 测试」;只有一个实现的 port(端口,跨接缝的插槽)只是多绕一层 |
SKILL.md 给了三条可以直接照做的经验法则(和 tdd、mocking 的纪律同方向):
new StripeGateway()。
calculateDiscount(cart): Discount),
而不是原地改状态、留下难以断言的副作用(side effect:函数除了返回值之外,对外界造成的改动)。
tdd 那条 skill 规定:测试通过公共接口验证行为,活在接缝上,绝不捅内部;
写任何测试之前,先把接缝写下来并跟你确认。
你刚学的「接口就是测试面」就是那条纪律的设计侧:
先把面设计对,再在这个面上做红绿循环(red-green:先写一个失败的测试,再写实现让它通过)。
┌──────────────┐
│ Module │ 恰好有一个 Interface
└──────┬───────┘
│ presents
▼
┌──────────────┐
Depth ───► │ Interface │ Depth 相对 Interface 度量
└──────┬───────┘
│ lives at
▼
┌──────────────┐
│ Seam │ 位置决策 ≠ 背后装什么
└──────┬───────┘
│ filled by
▼
┌──────────────┐
│ Adapter │ 满足 Interface 的具体物
└──────────────┘
Depth ──produces──► Leverage(调用者)
└──► Locality(维护者)
interface 关键字、或类的 public 方法:
太窄了;这里的接口包括调用者必须知道的每一个事实。
DEEPENING.md 假定你已经会第 3 节的词汇,它回答的问题是:
给定一个依赖,怎么安全地把一簇浅模块加深。
「deepening opportunity」(加深机会)在 architecture 那条 skill 里,
指「把浅模块变深」的重构候选(目的是可测性和 AI 可导航性——让 AI 更容易读懂代码、找到路);
在这个文件里,它落成三件具体的事:依赖分类、接缝纪律、测试策略。
| 类别 | 例子 | 能否加深 | 测试怎么写 |
|---|---|---|---|
| 1. In-process(进程内) | 纯计算、内存状态、没有 I/O | 总是可以 | 把模块合并起来,直接通过新接口测,不需要 adapter |
| 2. Local-substitutable(本地可替代) | 用 PGLite 代替 Postgres、内存文件系统 | 只要存在本地 stand-in(本地替身)就可以 | 测试套件跑本地替身;接缝留在模块内部,不在对外接口上开 port |
| 3. Remote but owned(远程但自己拥有) Ports & Adapters |
自己的微服务、内部 API | 可以:逻辑放进深模块,网络传输当 adapter | 定义 port;生产环境用 HTTP/gRPC/队列 adapter,测试用内存 adapter |
| 4. True external(真正的外部依赖,用 mock) | Stripe、Twilio 这类你控制不了的第三方 | 加深后的模块把外部依赖以 port 的形式注入进来 | 测试提供 mock adapter(仿冒的 adapter) |
对类别 3,推荐的表达形状是(按原文的意图): 「在接缝上定义 port,实现一个 HTTP adapter 给生产环境、一个内存 adapter 给测试, 这样即使部署横跨网络,逻辑也落在一个深模块里。」
这个文件出自 Ousterhout 的「Design It Twice」(设计两遍):第一个想法多半不是最好的。
当你已经选定了一个加深候选、想探索几种不同的接口方案时,用并行子代理模式
(sub-agent:主 agent 派出去、各自独立干活的 agent)。
architecture 的面试循环结尾也会点名它:想看多种接口设计 → 运行
/codebase-design 并走这个模式。
| 步 | 做什么 | 注意 |
|---|---|---|
| 1. 框定问题空间 | 给用户看一份说明:新接口的约束、依赖及其 DEEPENING 类别、粗略 sketch(草图)。这不是提案,只是把约束说具体 | 展示给用户后立刻进第 2 步:用户边读边想,agent 同时开始并行设计 |
| 2. 派生 3 个以上子代理 | 每个子代理产出一个截然不同的接口,各自写一份独立的技术简报(路径、耦合、依赖类别、接缝背后藏什么) | 简报独立于第 1 步给用户看的文案;简报里必须用上 SKILL 的词汇和项目 CONTEXT.md 里的领域词 |
| 3. 展示并比较 | 按顺序展示各设计 → 用散文对比深度、局部性、接缝位置 → 给出有倾向的推荐(可以是几个方案的混合体) | 用户要的是一个明确的判断,不是一份没有立场的菜单 |
Agent 1 Minimize interface — 1–3 entry points;最大化每点 leverage
Agent 2 Maximise flexibility — 多用例与扩展
Agent 3 Optimise for most common caller — 默认路径极简
Agent 4 (若适用)Ports & adapters,围绕跨 seam 依赖
┌─────────────────────────┐
│ codebase-design │ 词汇的唯一权威出处
│ (+ DEEPENING, │
│ DESIGN-IT-TWICE) │
└───────────┬─────────────┘
┌────────────────┼────────────────────┐
▼ ▼ ▼
improve-codebase- tdd / implement to-spec
architecture (在接缝上测) (画接缝草图)
· 找浅点、套删除测试 · 先确认接缝 · 优先复用已有接缝
· 报告里用这套词 · 接口=测试面 · 接缝尽量少(理想 1)
· 面试后可回本 skill · 反对测试耦合实现
做 design-it-twice
| Skill | 启动 | 怎么使用这套词汇 |
|---|---|---|
| improve-codebase-architecture | User | 开篇就强制运行 /codebase-design 来对齐词汇;每条建议必须使用 module/interface/depth/seam/adapter/leverage/locality,禁止 component/service/API/boundary。它说的「候选」就是加深机会。选定后面试你;想看多种接口设计 → 再回来走 design-it-twice。 |
| tdd | Model | 接缝就是测试的落点;只在你预先确认的接缝上写测试;好测试通过公共接口验证行为。和「接口就是测试面」是同一件事。 |
| implement | User | 写得很薄的入口(正文只有几行):尽量用 tdd,在预先约定的接缝上写。形状决策应该已经在上游(spec、面试、design-it-twice)用这套词汇定好了。 |
| to-spec | User | 写需求文档前先画测试接缝的草图:优先复用已有的、位置尽量高、数量越少越好(理想是一个),并和你确认。文档的 Implementation Decisions 一节谈的是模块和接口,不是文件路径。 |
| diagnosing-bugs | Model | 发现没有好接缝能锁住这个 bug 时,在复盘(post-mortem)里把问题移交给 improve-codebase-architecture,那边就会用上这套词汇。 |
| ask-matt | User | 在它的地图里,architecture 是「找候选的扫描」,codebase-design 是「设计所选候选的工作台」;tdd 和 architecture 都说这套词。 |
$TMPDIR/architecture-review-<timestamp>.html(Tailwind + Mermaid CDN)并自动打开 → 你挑一个 → 面试 + 可能更新 CONTEXT/ADR → 可选地 design-it-twice。| 动作 | 会写文件吗 | 说明 |
|---|---|---|
| 单独跑 /codebase-design 对齐词汇、讨论深度 | 什么都不写 | 只在对话里推理;0001 的全表里标的就是「主要是词汇和设计讨论」 |
| design-it-twice 子代理的输出 | 通常只在对话里 | 比较和推荐都发生在会话里;真正落地走后面的流程 |
| improve-codebase-architecture 的报告 | 写进操作系统临时目录的 HTML,不进仓库 | $TMPDIR / /tmp / %TEMP% |
| architecture 面试过程中的命名和决策 | 可能写 CONTEXT/ADR | 那是通过 domain-modeling 写的,不是 codebase-design 自己写文件 |
| 真的去改业务模块的形状 | 改代码库和测试 | 通过 to-spec / tickets / implement(+tdd) 来做,或你明确要求重构——这个 skill 不代劳 |
/grill-with-docs 主流程 → to-spec / tickets / implement(ask-matt 的说法:扫描产生想法,设计工作台是 codebase-design)。| 症状 | 先查 | 不要误改 |
|---|---|---|
| 对话里冒出 component/service/boundary,深度讨论跑偏 | codebase-design/SKILL.md 的 Glossary 和 Rejected framings 两节 |
architecture 报告的 HTML 样式 |
| 乱开 port、只有一个假 adapter | SKILL.md 的原则一节 + DEEPENING.md 的接缝纪律 |
tdd 的红绿循环细则 |
| 加深后测试仍耦合内部、旧的浅层测试还在堆 | DEEPENING.md 的「替换,不叠加」;tdd 的反面模式清单 |
本课的 quiz 文案 |
| 依赖跨网络,却把逻辑测成一团 mock 地狱 | DEEPENING 的类别 3 和类别 4 之分;ports & adapters 的形状 | domain-modeling 的 ADR 三条件 |
| design-it-twice 出来的方案雷同、没有激进差异 | DESIGN-IT-TWICE.md 的 agent 约束和简报内容 |
ask-matt 的路由句 |
| 整库扫描没按热点(hot spot)来、报告被写进了仓库 | improve-codebase-architecture/SKILL.md 和它的 HTML-REPORT 文件 |
codebase-design 的词汇段(那是设计工作台,不是扫描器) |
| AI 从不主动加载这个 skill | frontmatter 里 description 的 “Use when…” 一段 |
plugin.json 是否发布(它本来就是已发布的 model skill) |
先别往回翻表,凭记忆答。选项的长度刻意对齐,不会从版式泄题。术语以本课和 SKILL.md 的精确定义为准。
本课主一手材料(请打开原文读,不要只背本页摘要):
skills/engineering/codebase-design/SKILL.md
—— 词汇、深/浅、原则、可测性、关系图、被否定的说法。
…/DEEPENING.md
—— 依赖四类、接缝纪律、替换不叠加。
…/DESIGN-IT-TWICE.md
—— 并行子代理接口设计。
docs/engineering/codebase-design.md
—— 给人看的叙事版:「语言不是流程 / 什么时候用 / 谁引用它」。
速查页(本课同步): reference/codebase-design.html
导航: 上一课 0004 domain-modeling (平行的词汇地板:一个管领域词,一个管模块形状)。 总览仍回 0001 系统地图; 面试编排见 0003。
建议下一课(0006,还没写):
按 NOTES.md 的课程顺序,两个词汇地板之后是
handoff(再往后是 research / prototype)——跨会话的桥,和「可运行答案」的岔路。
和本课的交接点:当 design-it-twice 或 architecture 的面试把上下文塞满时,
用 handoff 换个新会话,别在一个已经变迟钝的窗口里硬写加深后的接口。
SKILL.md / DEEPENING.md / DESIGN-IT-TWICE.md 的原文,不会临场编造。
做完检索练习后,回复「练习结果 / 哪里卡住 / 开 0006 或先补 0004」,我们安排下一课。