再学 AI
第 18 课 / codebase-design
当前 · 工程阅读 → 对照 → 问答 → 场景

用小接口承载有价值的复杂性

模块的深度看调用者获得了多少能力、需要理解多少细节。代码行数多不代表模块深。

还不清楚 Skill、Agent、安装和调用?先读 从零开始的6节入门课

在关系图中查看 codebase-design 与其他技能的关联 →

先把必要的概念讲清楚

本课提供讨论软件结构的基本语言。你不需要先会设计大型系统,但要能辨认:调用者需要知道多少内部细节、修改一个能力会影响多少地方、测试应从哪里观察结果。

下面是老师补充的入门说明;原作者的要求保留在中英对照正文中。所有例子均为帮助理解而构造的教学情境。

module|模块

承担一组相关职责的代码单元,可以是文件、包或服务,不必等于一个文件。学习进度模块可以公开“记录进度”和“查询进度”,把计算完成比例、保存数据等细节藏在内部。划分是否合理,要看职责和依赖,不能只数文件。

interface / API|接口

使用者与一项能力交互时遵循的约定,包括可调用什么、传入什么、得到什么、失败怎样表示。接口可以是程序函数,也可以是网络请求。创建收藏接口接收用户和课程信息、返回收藏结果;它不需要向调用者暴露数据库表结构。这里的接口通常不是指页面外观。 本教材涉及更广义的“接口”时,还包括错误、调用顺序和约束等使用约定;不能把接口一概等同于网络 API。

deep module|深模块

用相对简单的公开接口封装较多内部复杂性。调用者只说“为这节课生成复习安排”,模块内部处理规则、时间和重复项。深是接口与实现复杂性的关系,不是目录嵌套深,也不是函数越长越好。

seam|测试接入点

作者在测试语境中指测试进入系统、触发行为并观察结果的公共边界。例如调用“收藏课程”接口,再通过“我的收藏”接口检查结果。入口可以是模块公开函数、服务接口或页面,并非一定是浏览器。选择高层稳定入口能覆盖内部多个步骤;入口少不等于测试场景少。

refactoring|重构

在保持约定外部行为的前提下改善内部结构,例如合并重复逻辑、调整职责归属。用户仍能完成同样操作,但代码更容易理解和修改。重构不等于顺便加新需求;测试帮助证明外部行为未被意外改变。

读原文,理解每一步为什么这样做

左右内容按小节对应;窄屏先中文、后英文。两种语言均完整展示,对应讲解紧接在小节之后。译文传达原文要求;老师讲解补充概念、原因、例子与适用边界。

中文译文English · 英文原文
中文译文
name: codebase-design
description: "设计深模块的共同词汇。设计或改进模块接口、寻找加深机会、决定接缝位置、提升可测试性或 Agent 导航性,或其他技能需要这些词汇时使用。"
English · 英文原文
name: codebase-design
description: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
中文译文

设计容易使用和维护的模块

设计深模块:通过简洁的接口提供较多行为,安排清楚可替换行为的衔接位置,并通过接口验证功能。设计或调整代码时采用下面的语言和原则,让调用者学得少、得到多,让维护者在集中位置修改和验证。

English · 英文原文

Codebase Design

Design deep modules: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone.

中文译文

共同词汇

准确使用以下概念,不随意替换成 component、service、API 或 boundary。保持一致用词就是本文的目的。

模块 Module:同时有接口和实现的事物。刻意不限制规模,可以是函数、类、软件包,也可以是贯穿多个层次的功能切片。不要随意改称 unit、component 或 service。

接口 Interface:调用者正确使用模块必须知道的一切。不仅是类型签名,还包括必须保持的条件、操作顺序、错误形式、必要配置和性能特征。API 或 signature 范围较窄,不能完全代替这里的接口。

实现 Implementation:模块内部的代码。与 adapter 是不同分类。一个承担范围较小的适配器,内部可能很大,例如 Postgres 数据仓库;范围较大的适配器,内部也可能很小,例如内存替代实现。谈衔接处承担的角色时用适配器,谈内部代码时用实现。

深度 Depth:调用者每学习一部分接口,能够获得多少行为能力。简洁接口背后承担大量工作,是深模块;接口需要理解的复杂度几乎等同内部实现,是浅模块。

可替换衔接位置 Seam:采用 Michael Feathers 的概念,指可以改变某处行为,却不必直接修改该处代码的位置,也就是模块接口所在的位置。放在哪里,与其后放什么实现,是两个设计问题。不要笼统换成 boundary,以免混淆领域驱动设计的限界上下文。

适配器 Adapter:在某个衔接位置满足接口要求的具体对象。这个词描述它填入哪个位置、承担什么角色,不描述内部材质或代码多少。

调用收益 Leverage:调用者通过深度得到的好处,学习较少规则即可使用更多能力。一份实现可让 N 个调用位置和 M 个测试受益。

修改集中性 Locality:维护者获得的好处。改动、bug、知识和验证集中在一个地方,而不散落到多个调用者。修一次,各处共同受益。

English · 英文原文

Glossary

Use these terms exactly: don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.

Module: anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. Avoid: unit, component, service.

Interface: everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. Avoid: API, signature (too narrow, they refer only to the type-level surface).

Implementation: what's inside a module, its body of code. Distinct from Adapter: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.

Depth: leverage at the interface. The amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is deep when a large amount of behaviour sits behind a small interface, shallow when the interface is nearly as complex as the implementation.

Seam (Michael Feathers): a place where you can alter behaviour without editing in that place; the location at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. Avoid: boundary (overloaded with DDD's bounded context).

Adapter: a concrete thing that satisfies an interface at a seam. Describes role (what slot it fills), not substance (what's inside).

Leverage: what callers get from depth. More capability per unit of interface they learn. One implementation pays back across N call sites and M tests.

Locality: what maintainers get from depth. Change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.

老师讲解 · 对应上方原文 · 含教学举例

接口简单,不等于内部工作简单

模块把一组相关工作组织在一起;接口规定外部如何使用它;实现是内部怎样完成工作。例如“生成今日复习安排”可以是模块的公开能力,内部可能读取学习记录、计算复习间隔、去重并排序。

这里的接口不只是一个函数名、网络地址或 TypeScript 的 interface 声明。假设保存学习记录要求“先登录”“完成时间不能早于开始时间”“重复提交不会生成两条记录”,这些也是调用者必须理解的使用约定,因此属于本课所说的接口。

适配器(adapter)描述的是角色:具体由谁来满足某个接口。 例如同一个“保存学习记录”接口,可以由 PostgreSQL 数据库存储实现,也可以由测试中的内存存储实现。两者内部工作不同,但都占据同一个位置、遵守同一套对外约定。转换模型供应商的数据格式也是一种适配,但不能把“适配器”只理解成格式转换器。

原文说的 leverage,是调用者只需理解少量接口知识,就能获得较多能力;locality 是相关知识、修改和验证能否集中。它们分别从调用者和维护者的角度说明深模块的收益。

同一个 seam,在这里为什么不能只译成“测试入口”

本课采用 Michael Feathers 的定义:可以不修改某个位置自身的代码,就改变从该处接入的行为的位置。中文常说“接缝”,初学时可以把它理解为“允许替换行为的衔接位置”。这里讨论的是程序结构,比 to-spec 中的测试接入点含义更广。

例如订单处理函数接收一个支付对象。正式运行时传入真实支付服务,测试时传入一个会返回“支付成功”的受控替代对象。订单处理函数本身不用修改,支付行为就能替换。接收这个对象、并按照约定调用它的位置,就是一个 seam;真实支付实现和测试替代物则是在这个位置使用的不同适配器。

位置、约定、具体实现要分开想。 seam 回答“在哪里替换”;interface 回答“替换进来的对象必须遵守什么”;adapter 回答“这次具体使用谁”。如果把这三者全叫作接口,就会把不同设计问题混为一谈。

在测试规格里,作者把注意力放在“从哪个公开位置触发和观察行为”,因此“测试接入点”更容易理解。两处用词要联系上下文,不能把某一课的简化解释当成所有文档中的唯一含义。

用一个出题模块,把这些抽象词放进实际情境

假设网站需要根据一篇课文生成一道题。 调用者传入课文和难度,得到题目、选项和答案。内部负责组织请求、调用模型、解析返回并检查字段。

这整块能力是模块,内部完成事情的代码是实现。接口不仅是一个函数名,还包括参数、结果、可能失败的情况、密钥要求和等待时间。使用者需要知道这些才能正确调用。

如果每个页面都要自己拼提示、解析 JSON、修补字段,复杂性仍分散在外面。若模块通过清楚入口集中承担这些工作,调用者学少量规则就能使用,它就更深;不要求故意写长代码。

如果正式环境在同一个调用位置接真实模型,测试接固定返回题目的替代实现,这个可以替换的位置就是 seam,两个具体实现分别扮演 adapter。

本篇 seam 比“测试接入点”含义更宽,强调替换行为的位置。测试文章按其用途解释为从哪里进入系统验证。因此不能全站机械译成“接缝”。

再想象删除出题模块。如果每个页面都要重新处理模型返回格式,说明它确实集中承担了复杂性。这比数文件行数更能说明设计价值。

中文译文

深模块与浅模块

深模块是较小的接口背后有较多实现:公开方法少、参数简单,复杂逻辑隐藏在内部。

浅模块则是接口很大、实现很薄:方法多、参数复杂,却主要向别处转发。应尽量避免这种结构。

设计时问:能否减少方法数量?简化参数?把更多复杂性留在内部处理?

English · 英文原文

Deep vs shallow

Deep module = small interface + lots of implementation:

┌─────────────────────┐
│   Small Interface   │  ← Few methods, simple params
├─────────────────────┤
│                     │
│  Deep Implementation│  ← Complex logic hidden
│                     │
└─────────────────────┘

Shallow module = large interface + little implementation (avoid):

┌─────────────────────────────────┐
│       Large Interface           │  ← Many methods, complex params
├─────────────────────────────────┤
│  Thin Implementation            │  ← Just passes through
└─────────────────────────────────┘

When designing an interface, ask:

  • Can I reduce the number of methods?
  • Can I simplify the parameters?
  • Can I hide more complexity inside?
老师讲解 · 对应上方原文 · 含教学举例

怎样用一个具体变化判断模块深浅

假设页面每次生成复习计划,都得自己知道模型参数、缓存键、重试次数和响应解析办法。即使这些代码被分成很多文件,复杂性仍暴露给调用者,接口并不深。

更深的模块允许页面只提交学习目标与当前进度,并得到一个定义清楚的结果。内部可以改缓存或换模型,而页面不必同步改十处。但如果接口太模糊,例如只提供 doAnything,也会让正确使用变困难,不能靠少一个方法掩盖问题。

“浅”不是自动有罪。一个适配第三方格式的小函数虽然短,仍可能承担清楚且有用的隔离职责。要看它隐藏了什么知识,而不只看行数。

中文译文

原则

  • 深度是相对于接口而言的,不由内部代码长短决定。深模块内部仍可由小的、可模拟和替换的部分组成,只是不暴露给外部。既可以有公开接口处的外部衔接位置,也可以有仅供内部及其测试使用的内部衔接位置。
  • 假想删除模块。如果复杂性随它消失,可能只是多余转发;如果复杂性重新散落到多个调用者,它就在发挥价值。
  • 接口就是测试使用面。调用者与测试通过同一衔接位置使用模块。若总想绕过接口测试内部,可能要重新考虑模块的组织方式。
  • 一个适配器可能只证明假想需求;两个适配器才体现实际变化。没有真正会变化的东西,不要提前引入可替换衔接位置。
English · 英文原文

Principles

  • Depth is a property of the interface, not the implementation. A deep module can be internally composed of small, mockable, swappable parts; they just aren't part of the interface. A module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface.
  • The deletion test. Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
  • The interface is the test surface. Callers and tests cross the same seam. If you want to test past the interface, the module is probably the wrong shape.
  • One adapter means a hypothetical seam. Two adapters means a real one. Don't introduce a seam unless something actually varies across it.
老师讲解 · 对应上方原文 · 含教学举例

把经常一起变化的知识放在一起

若每次新增一种课时类型,都必须同时修改十个互不相邻的条件分支,说明有关课时类型的知识散落了。把这些变化集中到更清晰的位置,往往比单纯改文件名更有价值。

这里也涉及重复:两段代码长得像,可能只是偶然相似;只有它们代表同一规则、应一起变化时,共享实现才可靠。过早合并不同业务规则,会制造一个充满开关的通用模块。

你可以问 AI:“这个设计隐藏了哪项知识?下次规则改变要改哪些调用者?”答案应该落到具体变化上,而不是只给“解耦、可扩展”等口号。

删除测试法:删掉的是模块,还是它替别人承担的复杂性

这里的 test 是思想实验,不是马上删除文件,也不是运行自动化测试。想象拿掉这个模块之后,原来由它隐藏的规则会到哪里去。

例如某个转发层原样接收十个参数,再原样调用下一层,调用者并没有因此少理解任何事情。移除这层后,可能只是少绕一次路,复杂性也随之减少。反过来,若删除“生成复习计划”模块后,五个页面都得自己处理间隔、排序和去重,复杂性就扩散到五处;说明该模块确实在集中承担工作。

判断的依据是它隐藏和集中了什么知识,而不是它叫 service、helper 还是 manager。作者用删除测试法帮助发现多余转发,不是在主张删除所有中间层。

“一个适配器是假想接缝,两个才是真实接缝”也是设计启发。 它提醒你先找到确实会变化的行为,再决定是否增加替换位置;不要只因为将来“也许要换”就为每个函数建立一层抽象。具体工程中,已明确的外部边界或测试替代需求也可能足以说明变化存在,不能把数量口号当作编译规则。

中文译文

怎样改善可测试性

  1. 从外部接收依赖,不在内部固定创建。

    // 便于测试:由调用者传入支付网关
    function processOrder(order, paymentGateway) {}
    
    // 较难测试:内部固定创建某一种网关
    function processOrder(order) {
      const gateway = new StripeGateway();
    }
    
  2. 返回结果,避免直接改变外部状态。

    // 返回计算结果
    function calculateDiscount(cart): Discount {}
    
    // 直接修改传入对象的状态
    function applyDiscount(cart): void {
      cart.total -= discount;
    }
    
  3. 对外使用面较小。 方法较少,需要分别覆盖的方法也较少;参数简单,测试准备更容易。

English · 英文原文

Designing for testability

Good interfaces make testing natural:

  1. Accept dependencies, don't create them.

    // Testable
    function processOrder(order, paymentGateway) {}
    
    // Hard to test
    function processOrder(order) {
      const gateway = new StripeGateway();
    }
    
  2. Return results, don't produce side effects.

    // Testable
    function calculateDiscount(cart): Discount {}
    
    // Hard to test
    function applyDiscount(cart): void {
      cart.total -= discount;
    }
    
  3. Small surface area. Fewer methods = fewer tests needed. Fewer params = simpler test setup.

老师讲解 · 对应上方原文 · 含教学举例

可测试性来自清楚边界,不只是测试工具

公开入口清楚,测试就能从稳定位置观察真实行为。若学习进度必须穿过多个全局变量、直接读取当前时间、访问真实模型才能计算,就很难稳定复现。

可以把时间、外部模型或存储放到明确边界:让核心规则在受控输入下运行,而在必要边界验证适配是否正确。不是把内部每一行都做成可替换接口,也不是所有依赖都 mock 掉。

例如固定“当前日期”和已学记录,期待生成三项复习任务;再单独验证真实存储或接口接线。这样既能查规则错误,也知道哪些真实集成尚未覆盖。

中文译文

概念关系

  • 模块有一个完整接口,即对调用者和测试的使用面。
  • 深度是模块相对于这个接口的性质。
  • seam 是接口所在的衔接位置。
  • adapter 在该位置满足接口要求。
  • 深度为调用者带来收益,为维护者带来修改集中性。
English · 英文原文

Relationships

  • A Module has exactly one Interface (the surface it presents to callers and tests).
  • Depth is a property of a Module, measured against its Interface.
  • A Seam is where a Module's Interface lives.
  • An Adapter sits at a Seam and satisfies the Interface.
  • Depth produces Leverage for callers and Locality for maintainers.
中文译文

本文不采用的理解

  • 用实现行数除以接口行数判断深度。作者认为这种与 Ousterhout 相关的比例式理解会鼓励填长实现;本文按接口带来的能力理解。
  • 把接口仅理解为 TypeScript 的 interface 关键字或类公开方法。这里还包含调用者必须知道的其他事实。
  • 用 boundary 统称这些概念。应具体说 seam 或 interface,避免与业务限界上下文混淆。
English · 英文原文

Rejected framings

  • Depth as ratio of implementation-lines to interface-lines (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
  • "Interface" as the TypeScript interface keyword or a class's public methods: too narrow: interface here includes every fact a caller must know.
  • "Boundary": overloaded with DDD's bounded context. Say seam or interface.
老师讲解 · 对应上方原文 · 含教学举例

避免把作者的设计语言变成机械口号

“文件越少越好”“每个函数只许几行”“所有层都必须有接口”,都不能直接推出好设计。函数很短却迫使读者追十层转发,仍可能难懂;集中相关逻辑有时会让单个文件更长,但理解路径更短。

原文的概念与关系要用来比较具体方案。例如哪个方案让换模型只改一个适配边界,哪个方案把供应商格式泄漏到每个页面。讨论这种差异,才是在使用设计语言。

进一步参考材料可以扩展判断,但本课没有声称一种架构适合所有系统。先观察当前修改的真实痛点,再决定是否重构。

中文译文

进一步阅读

  • DEEPENING.md:根据依赖整合模块,讨论依赖分类、衔接纪律,以及替换旧测试而不不断叠加测试的方式。
  • DESIGN-IT-TWICE.md:并行设计明显不同的接口,再比较深度、修改集中性和衔接位置。
English · 英文原文

Going deeper

  • Deepening a cluster given its dependencies, see DEEPENING.md: dependency categories, seam discipline, and replace-don't-layer testing.
  • Exploring alternative interfaces, see DESIGN-IT-TWICE.md: spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement.

原作者:Matt Pocock · 中文翻译为非官方译本

来源:skills/engineering/codebase-design/SKILL.md ↗

固定版本:3cca18b368ae95cdbdebbff572ccafa662551015

配套参考资料(英文)

先作答,再看参考思路

Q1 · 理解

用自己的话说明:它解决什么问题,完成后会留下什么?

请各用一句话回答。若它只做规划或解释,不要把“已开发”“已部署”写成产物。

Q2 · 判断

一个模块只有一个函数,但调用前需要设置十个全局变量,接口真的小吗?

我已思考,查看参考思路

并不小。接口包括使用者必须知道的所有条件,不只是函数数量。隐含前提会增加误用风险和测试难度。

Q3 · 追问

原文中哪条要求在你的环境下可能不成立?

说出具体一句及其前提,例如工具不可用、资料缺失、已有项目约定冲突,或它只是作者偏好。把你的答案带回课堂,我们据此继续讨论。

课堂回传格式:第 18 课 / 我的理解 / Q2 回答 / 仍不理解的原句。这里是阅读教材;实时问答在我们的对话中进行。

把方法放进一个具体情境

教学案例:研究 Agent 调用“读取证据集”就能得到来源、内容和失败状态,比要求每个调用者自行下载、解码、去重、处理空内容更容易正确使用。但不应隐藏影响证据可信度的失败。

边界与容易误读的地方

本文提出“一种适配器是臆想、两种才真实”等强判断,应结合项目实际看待,不是所有接口都必须恰好有两个实现。内部封装也不是把全部代码塞进一个巨型文件。

讨论后再实践:先判断上述情境是否适用,再选择真实任务。现在无需安装、运行命令或修改现有项目。

关联阅读