ousterhout-quality-program

作者:Rob Zapp尚无安装尚无点赞更新于 2026年9月21日分类: 代码审查

它能做什么

每当编写或审查代码时涉及创建或更改边界——新的模块、类、组件、辅助函数、钩子、服务或包装器;任何共享代码的提取或集中;任何“让我们使其可重用”的时刻——以及在明确审查、重构或设计模块时使用。判断一个抽象是否物有所值:模块深度,是否隐藏设计决策,重复代码是保护共享不变量还是仅仅是形式上的重复,接口是否稳定。防止机械式的SOLID/清洁代码导致许多浅层类。还定义了读者成本测试(对人类和代理来说易于阅读和修改的代码)以及将现有代码库重构到此标准的流程。

安装会在你的 AgentsRoom 桌面端打开这个条目。如果还没有安装应用,你会被带到下载页面。

SKILL.md

---
name: ousterhout-quality-program
description: 每当编写或审查代码时涉及创建或更改边界——新的模块、类、组件、辅助函数、钩子、服务或包装器;任何共享代码的提取或集中;任何“让我们使其可重用”的时刻——以及在明确审查、重构或设计模块时使用。判断一个抽象是否物有所值:模块深度,是否隐藏设计决策,重复代码是保护共享不变量还是仅仅是形式上的重复,接口是否稳定。防止机械式的SOLID/清洁代码导致许多浅层类。还定义了读者成本测试(对人类和代理来说易于阅读和修改的代码)以及将现有代码库重构到此标准的流程。
---

# Ousterhout 质量程序

## 概述

模块的职责是通过一个小接口隐藏复杂性。核心衡量标准是**深度**:一个深度模块在提供大量功能的同时,接口保持简单;而浅层模块的接口几乎和其实现一样复杂,因此没有任何价值。复杂性是当变更迫使你理解或修改你未预料到的代码时的感受——Ousterhout 指出两个来源:**依赖性**(你不能更改 A 而不更改 B)和**晦涩性**(重要信息不明显)。

Ousterhout 仅告诉你一个好模块的*感觉*。它最有效的用法是结合其他几个视角,告诉你边界应该在哪里,以及如何安全地向边界移动。这个技能就是那个综合视角。

## 代码审查真正出错的地方

这个技能存在的两个失败点——在代理编写代码中反复观察到——是在**修正措施**上,而不是是否拆分的判定本身:

1. **浅层修正。** 给定六个 `as unknown as` 类型转换,未经辅助的审查者会将它们集中到一个通用的 `castRows<T>()` 辅助函数中——更整洁,但晦涩性依然存在。深层修正是带有测试的类型化行→领域映射器(先固定测试,应用 Parnas:类型转换是缺失边界的气味;Beck:先验证映射再移动它)。整理气味不是去除气味。
2. **反射式提取。** 给定三个兄弟组件中重复的相同更新逻辑,每个未经辅助的审查者都会说“提取共享辅助”——DRY 反射动作。这个程序的规则,扩展 Metz:等待不变式,而不是第三个相似——当代码保护共享规则时才集中,不是因为它们押韵。

当你发现自己推荐修正时,运行这两个测试:它是否去除了晦涩性还是仅仅重新定位了它?提取是否保护了不变式还是仅仅去重了形状?

## 比例门槛

当变更没有新增导出/可导入名称,没有创建新模块/类/组件/辅助/钩子/服务/包装器,也没有集中任何内容时,跳过此视角。纯重命名、机械代码修改、配置/数据编辑和单行修正免检。存疑时,仅运行两个核心测试(深度、不变式),到此为止。

## 完整规则

每段生成或审查的代码在任务完成前都必须通过 Ousterhout 视角——不仅仅是显式设计审查——除非变更低于比例门槛(无新边界,无集中:重命名、代码修改、配置编辑)。两个测试:(1)**深度**——新接口必须隐藏远多于它暴露的内容;接口复杂度与其包装内容相当则毫无价值。(2)**不变式**——仅当提取共享代码保护共享规则时才提取,绝不因为三个位置相似;修正必须去除晦涩性,而非重新定位(将六个类型转换集中到一个辅助函数仍是六个类型转换)。当变更创建或重塑边界时,先找一个成熟产品如何解决此类形状和规模的问题,并采用其约定,除非有明确理由不采用(训练中回忆的模式是主张,不是来源),然后运行以下检查。

## 何时使用

- 决定一个新类/函数/钩子是否值得其接口,还是仅仅是浅层透传。
- 文件超过大小阈值,且你在决定*如何*拆分,而不仅仅是是否拆分。
- 重复代码诱使你提取共享辅助函数。
- 设计或审查围绕业务规则的边界(授权范围检查、资金/四舍五入规则、状态机转换保护、数据保留规则)。
- 接口即将增加参数或特殊情况。
- 将现有代码库提升到此标准——见下文“将现有代码库重构到此标准”。

**不适用:**琐碎机械编辑,或项目约定已规定结构——见上文比例门槛。若有 `karpathy-guidelines`,则用于手术式变更纪律,配合测试驱动开发技能作为重构安全网。

## 视角

每个视角增加一个问题。Ousterhout 是主干;其他视角修正其盲点。

| 视角 | 它提出的核心问题 | 何时覆盖其他视角 |
|---|---|---|
| **Ousterhout** — 深层模块 | 这个接口隐藏的内容是否多于它暴露的? | 默认主线。 |
| **Parnas** — 信息隐藏 | 这个模块隐藏了什么设计决策(可能会改变)? | 模块应当深层的*原因*。如果它没有隐藏任何会变的东西,深度只是表面功夫。 |
| **Brooks** — 本质复杂性 vs 偶然复杂性 | 这是否消除了偶然复杂性,还是仅仅把本质领域复杂性搬了个地方? | 杀死那些“重构”——只是搬移混乱而没有缩小它。 |
| **Evans** — 领域驱动设计 | 这个边界是否用领域语言命名,而非通用工具语言? | 重命名 `utils`/`helpers` —— 用这个仓库实际拥有的不变式来命名边界。 |
| **Fowler** — 重构 / 异味 | 向更深设计迈出的最小安全步骤是什么? | 把“应该更深”转化为通过测试的具体步骤。 |
| **Beck** — 简单设计,测试优先 | 在加深接口之前,我是否证明了当前行为? | 防止过早架构设计。先让它工作并通过测试,再加深正确的接口。 |
| **Hickey** — 简单 vs 容易 | 这是否交织了无关概念,还是确实只有一个概念? | 浅层辅助通常是*容易*(近且快),而非*简单*(少交织概念)。优先简单。 |
| **Metz** — 重复优于错误抽象 | 这段重复代码是保护共享不变式,还是仅仅看起来相似(本程序规则,扩展Metz)? | Metz:重复比错误抽象便宜——把错误抽象内联回来而不是扭曲它。本程序扩展她的规则:**不要**因为重复就集中;只有当它保护真正不变式时才集中。容忍重复直到不变式显现。 |
| **Hyrum's Law** — 可观察行为 | 调用者会依赖接口合同之外的行为吗? | 主张小而稳定的接口:每个可观察行为最终都会成为承重部分。 |

## 组合方案

按此顺序应用——后面的视角只有在前面的通过后才重要:

1. **Metz — 准入门槛。** 这个边界/抽象是否值得存在?本程序规则,扩展Metz:只有当代码保护共享规则时才提取——三个相似的不是显现的不变式。如果否,停止。
2. **Parnas / Ousterhout** — 把易变决策(授权范围、舍入规则、转换守卫、保留规则)隐藏在深层模块后。
3. **Evans** — 用领域语言命名该模块,而非 `utils`。
4. **Beck / Fowler** — 对现有代码,用测试固定当前行为,然后以小而安全的步骤重构。对新生成代码没有当前行为可固定——写定义预期行为的测试。
5. **Hickey** — 拒绝仅因工作流相似而混合无关概念的接口。

## 结构反模式

**机械式的SOLID / Clean Code产生浅层模块。** 教条式解读——每个类只负责一件事,提取每个函数,保持一切微小——导致一堆类,其接口复杂度与其内部一样。当规则说“拆分它”,问拆分隐藏了什么*决策*(Parnas)以及它隐藏的是否多于暴露的(Ousterhout)。如果它没有隐藏任何会变的东西,就别拆分。这个防护在重构压力下最重要(“清理它”,“这个文件太大”)——在冷静分析时,审查者已经抗拒;在重构中期,为了产生可见变化,才写出一堆浅层文件。

## 常见错误

- **仅按大小拆分。** 一个400行的查询模块隐藏一个连贯决策,可能比四个各100行且各自泄露相同连接的模块更深。
- **拆分命名为 `helpers`/`utils`。** 如果不能用领域语言命名(Evans),边界可能错了。
- **第二次出现就提取。** 本程序规则,扩展Metz:等待不变式显现,而非第三个相似。
- **未固定行为就加深。** Beck:没有测试证明当前行为,“加深”重构就是重写。
- **把透传当模块。** 转发参数的包装器增加接口但不隐藏任何东西——定义上是浅层。
- **把押韵当不变式。** 共享不变式的最好证据是共变:历史上这些副本被一起修复或修改(同一个bug修复两处)。独立变更的相似是押韵;保持它们重复。
- **整理异味而非消除。** 把六个类型转换集中成一个通用转换助手是同样晦涩的整洁版。深层修复是命名那个被掩盖的边界。

## 读者成本:第三个测试

深度和不变式决定边界是否应存在。读者成本决定其周围代码是否易改。下一个读者,无论是人还是代理,都要为安全修改必须加载的每一行付费。代理用代币付费,通过文本搜索、部分读取和类型检查/测试循环导航,因此同样的缺陷对它们代价更高。问:

- **可查找?** 每个概念一个名称,拼写一致,能通过纯文本搜索找到。缺陷:名称由字符串拼接而成,依赖导入副作用连接,重导出链隐藏定义,一个概念有两个名称。
- **读者能否提前停止?** 合约位于文件顶部或导出语句上方:它承诺什么,隐藏什么,绝不做什么。缺陷:合约只能通过阅读主体推导。
- **机器可校验?** 每个边界的输入输出都有精确类型,类型检查替代了阅读调用者。缺陷:`any`,裸字典,布尔标志的含义藏在主体中。
- **耦合是否可见?** 必须一起变更的地方被强制(共享类型、测试、单一来源),否则在两个位置都标记。隐藏耦合的证据是历史中共同变更,但代码中无任何提及。
- **无噪声?** 无重复代码的注释,无注释掉的代码,无死分支,无变更历史注释,无废弃路径与替代路径并存。
- **可预测?** 布局遵循仓库现有模式;测试放在读者会查找的位置且能独立运行。

文件大小故意未提及。非常大的文件是寻找第二个隐藏决策的理由,绝不是拆分的理由:读者可以搜索和阅读范围,且不隐藏任何内容的拆分会增加接口但不减少负载。

对于代码内标记和仓库代码地图,使用可用的`context-audit`:其`AIDEV-NOTE:`锚点(一个不可恢复的事实加上来源引用,最多两行,位于现场)是无法强制执行耦合的约定。

## 将现有代码库重构到此标准

改造的评判标准与新代码相同;不同的是顺序和克制。大部分代码库应保持不变。

1. **普查,只读。** 列出边界(模块、服务、共享助手)。对每条记录:隐藏的决策或“无”;接口大小与主体对比;历史中的共同变更伙伴;读者成本缺陷。暂不更改。
2. **按变动频率排序,不按丑陋度。** 优先级是代码变更频率乘以阅读成本。冷代码且可用则保持原样,无论多浅。核心领域复杂性保持原位(Brooks)。
3. **每个发现分配一个修正方案:**
   - 透传层或包装器不隐藏任何东西:删除它,调用者直接使用被包装的内容;
   - 错误抽象被标志和特殊情况扭曲:内联回去(Metz),然后寻找真正的不变量;
   - 共享一个决策的浅层兄弟:合并成一个接口;
   - 泄露的决策(调用者知道格式、规则、模式):拉入拥有它的模块;
   - 通用名称(`utils`、`helpers`、`manager`):重命名为隐藏的决策,或解散到调用者;
   - 未类型化边界:加类型,用映射器替代类型转换;
   - 隐藏耦合:强制执行,或在两个位置标记;
   - 噪声:删除。

   独立变更的韵律不做修正。
4. **先固定行为。** 未有测试证明当前代码行为前不开始修正(Beck)。重构保持行为不变;行为变更为单独提交。
5. **将工作切分为单个代理能独立完成的单元。** 每单元一个边界。每单元命名其拥有的文件、必须保持的合约和能独立验证的命令。无两个并发单元写同一文件;共享文件(桶文件、注册表、路由表)由单一所有者或等待集成。多个单元依赖的接口变更先行,作为独立单元。
6. **衡量结果。** 选一个代表性变更,统计读者为完成它需加载的文件和行数;变更后再统计。导出名称和总行数应减少或持平。增加接口的重构需说明理由。
7. **停止** 当剩余部分是冷代码、核心或韵律。

相关技能(如有):`repo-review`(设计类型)生成普查报告作为建议;`design-cleanup`执行修复和重新扫描循环以消除偶发复杂性;`context-audit`添加锚点和代码地图;`ousterhout-build-deep`是执行单元的作者时间检查表。

## 该技能定位

此技能是审查和判断层:用它决定抽象是否深刻、是否以正确决策命名、是否值得提取。`find-shared-code`在扫描近期历史寻找值得共享的代码时,将其作为准入测试。附录中给出每位作者的推理。

---

## 附录:深入透镜

每位作者捕捉的失败模式及其给出的唯一动作。上表是快速参考;此处是背后的推理。

### Ousterhout — 深模块(主干)

*《软件设计哲学》*

- **深度** = 益处(隐藏的功能)÷ 成本(接口复杂度)。深模块用少量接口隐藏大量内容。浅模块接口复杂度几乎等同于主体,因而无收益。
- **复杂性** 是系统中使理解或修改困难的任何因素。两大来源:
  - **依赖** — 不能单独更改一部分而不影响另一部分。
  - **晦涩** — 重要信息从代码中不明显。
- **症状:** 变更放大(一决策多处编辑)、认知负担(需记忆的信息量)、未知的未知(无法判断变更影响的代码)。
- **关键动作:** 将复杂性*向下拉*——模块吸收难点,调用者无需处理。配置参数和透传将复杂性*推向*调用者;那是浅层。

捕捉:泄露实现的接口;无用的助手。

### Parnas — 信息隐藏(为何深度重要)

*《系统模块划分标准》(1972)。*

- 围绕**可能会改变的设计决策**进行分解,而不是围绕计算步骤。每个模块隐藏一个这样的决策。
- 这是深度模块的直接前身。模块之所以深度,*是因为*它隐藏了一个否则会波及调用者的决策。

陷阱:一个“模块”什么都不隐藏——它的深度只是表面现象。问:这个接口背后有什么变化是调用者永远看不到的?如果答案是“没有”,那么这个边界只是装饰。

### Brooks — 本质复杂性与偶然复杂性

*没有银弹。*

- **本质**复杂性是领域固有的(评估确实如此复杂)。**偶然**复杂性是我们的工具和结构强加的。
- 只有偶然复杂性是可以消除的。通过将本质领域复杂性从一个文件移动到另一个文件来“清理”的重构,实际上什么都没做。

陷阱:伪装成简化的重排。问:总复杂性是否下降了,还是只是移动了?

### Evans — 领域驱动设计

*领域驱动设计。*

- 边界应以领域的**通用语言**命名,而非通用工具术语。名为`helpers`的模块什么都没命名;名为`AccessScope`或`PricingPolicy`的模块命名了一个不变量。
- 有界上下文防止业务不变量跨接缝泄漏。

陷阱:正确分解但命名无意义。如果你无法用领域语言命名模块,说明你可能切错了边界。

### Fowler — 重构与代码异味

*重构。*

- 提供具体、安全、命名明确的操作(提取函数、移动字段、用多态替换条件语句),从当前设计走向更深层设计。
- 每一步都是行为保持且小规模的,因此可逆。

陷阱:“这应该更深”与知道下一次提交之间的鸿沟。Ousterhout设定目标;Fowler指明道路。

### Beck — 简单设计,测试优先

*测试驱动开发;XP。*

- Beck发布的简单设计四条规则顺序为:通过测试、无重复、揭示意图、最少元素。本程序遵循后期Fowler/Haines的调整顺序——先意图后重复——因为它服务于本程序的Metz扩展不变量规则(见下文Metz):在你能命名它所保护的意图之前,不要对重复采取行动。
- 测试优先是防止过早架构的刹车。先让它工作并证明行为,然后再加深测试所保护的接缝。

陷阱:在行为确定之前构建架构。没有测试证明当前行为,“加深”重构就是未经验证的重写。

### Hickey — 简单与容易

*简单变容易。*

- **简单** = 不交织:一个概念,不与其他概念交织(客观)。
- **容易** = 触手可及、熟悉、快速(相对于你)。
- 两者独立。浅层辅助通常是*容易*的——写起来快,离得近——但如果它交织了无关关注点,就不是*简单*的。

陷阱:便利伪装成设计。即使交织的写起来更快,也应优先选择保持概念不交织的结构。

### Metz — 宁可重复也不选错抽象

*“错误的抽象”(2016)。*

- 重复远比错误的抽象便宜。过早抽象会迫使未来所有调用者都绕过那些从未对所有调用者都成立的假设。
- 当抽象被证明错误时,Metz的补救措施是将其内联回去,让重复回归,而不是强行调整以适应从未设计的情况。
- **本程序扩展Metz的规则:不要因为代码重复而集中化。集中化应当保护真实的共享不变量。**在不变量显现之前,容忍重复。

陷阱:过度集中——浅层共享辅助,所有人都必须绕过它。这是机械“无论如何都要DRY”的对立面。

### Hyrum定律 — 可观察行为成为契约

*“用户足够多时,系统的每个可观察行为都会被某人依赖。”*

- 接口*碰巧*做的任何事——顺序、时机、错误文本——最终都会有人依赖。因此你暴露的表面比你文档的表面更大。
- 这支持Ousterhout偏好**小而稳定的接口**:暴露越少,意外成为承重的可能越小。

陷阱:宽泛接口将僵化。每多一个可观察项,未来的约束就多一条。

### 它们如何结合

- **Parnas → Ousterhout:**隐藏一个易变决策→模块即深度。
- **Brooks:**确认深度移除了复杂性而非仅仅搬移。
- **Evans:**用领域语言命名边界。
- **Beck → Fowler:**确定行为,然后小步安全重构。
- **Metz:**在不变量真实之前抵制集中化。
- **Hickey:**保持接口单一概念。
- **Hyrum:**保持接口小以保持稳定。

危险在于将Ousterhout与机械解读的SOLID或Clean Code混合:这会产生许多接口浅薄的微小类和函数——与深度模块完全相反。Ousterhout配合Metz作为平衡,是解药。

标签

设计架构审查重构ousterhout