software design philosophy deep modules

软件设计哲学 · 深模块 vs 浅模块

来源:John Ousterhout《A Philosophy of Software Design》 提炼时间:2026-04-24


核心概念

什么是模块

模块指任何具有接口和内部实现的代码单元: - 函数、类、包、服务都算模块 - 模块化把系统切分成不同部分,每个部分可以独立理解和修改

复杂性的来源

系统复杂性来自三个地方:

  1. 依赖 — 理解 A 需要先理解 B
  2. 模糊 — 代码做的事和预期不一致
  3. 浅薄 — 模块接口暴露的信息和内部隐藏的信息差不多,调用者得不到太多价值

深模块是解法

深模块 = 简单接口 + 大量隐藏复杂性

┌─────────────────┐
│  简单接口(3个)  │ ← 调用者只需要知道这3个操作
├─────────────────┤
│                 │
│  复杂内部实现     │ ← 大量复杂性对调用者隐藏
│                 │
└─────────────────┘

经典例子:Unix I/O - 接口:open read write close(4个操作) - 内部:处理磁盘、网络、终端、内存映射…所有设备差异全部隐藏 - 对调用者来说,读文件和读网络完全相同

经典例子:GC(垃圾回收) - 接口:分配内存、释放内存(自动) - 内部:追踪引用、标记存活对象、压缩内存…全部隐藏 - 程序员不需要知道内存碎片如何整理

浅模块的问题

浅模块 = 简单接口 + 简单实现

┌─────────────────┐
│  简单接口(10个) │ ← 调用者要理解10个接口
├─────────────────┤
│  简单实现         │ ← 没什么复杂性被隐藏
└─────────────────┘

调用者承担了理解模块协作关系的认知负担,而不是模块内部。


判断标准

深模块特征

浅模块特征

反模式:类itis

一个类试图做得太多,导致接口和实现一样浅。典型例子:Java 早期很多类的 get/set 把所有内部状态直接暴露。


设计决策

通用模块应该设计得更深

技术组件(消息队列、缓存、安全、日志)属于通用模块,应该有简洁但强大的接口,把复杂性藏在内部。

业务模块有时候故意设计得浅一点——因为业务逻辑本身就是多变的,隐藏太多反而增加理解成本。

接口设计优先

设计新模块时,先想接口,不先想实现。接口是模块和调用者之间的契约,接口错了内部再好也没用。

检验标准:能用一句话描述模块接口代表什么抽象吗?如果描述不清,接口可能设计过度了。

信息隐藏原则

每个模块应该隐藏: - 如何实现的决策 - 内部数据结构和算法 - 性能优化手段 - 依赖的第三方库

不应该隐藏: - 模块的职责边界 - 明确的输入输出格式 - 复杂度级别(让调用者知道这里有复杂性)


与 AI coding 工具的关系

为什么基本原则更重要了

Matt Pocock 指出:AI 生成代码是概率采样,不是逻辑推导。越是遵循基本原则(深模块、接口清晰),AI 越能稳定输出高质量代码。

如果代码库烂(浅模块、依赖混乱),AI 生成的代码只会复制这个烂结构,然后放大烂结构的问题。

具体影响

1. 接口即合同 AI 编程时,清晰的接口定义等于给 AI 的指令。当模块边界清晰,AI 能更准确地推断哪个模块该放哪个逻辑。

2. 深模块减少 AI 的认知负担 如果模块是浅的,AI 需要同时考虑多个模块的交互,错误率上升。深模块把复杂性隔离,AI 只需要知道"用它就行"。

3. 测试即验证 TDD 的价值在 AI 时代被放大——AI 生成代码可能看起来对但实际错,行为测试能快速发现偏差。


快速检验清单

拿到一个模块,问:

如果任何一个回答"是",可能是浅模块信号。


参考:John Ousterhout, "A Philosophy of Software Design", 2018