Miller 定律:人类工作记忆一次只能处理约 7 个单位的信息。但程序可以增长到百万行。
矛盾的唯一解法是:让你能在不理解整体的情况下理解局部。模块是这个机制的形式化。它的核心功能不是”组织代码”,而是制度化的主动无知——刻意选择不去了解某些细节,以便能够专注于其他的事。
一、模块是什么?(本体论)
模块是命名的无知单元——它允许你在不知道实现细节的情况下使用它。这不是缺陷,是核心功能。
David Parnas 在 1972 年的奠基论文中提出了一个至今仍被低估的洞察:模块化不是”把相关代码放在一起”,而是”把你可能想要改变的决策隐藏起来”。
两种划分方式的差异:
按功能步骤划分(常见但错误):
module_read_input
module_process_data
module_write_output
按设计决策划分(Parnas 的意思):
module_storage_format ← 隐藏"数据如何存储"的决策
module_algorithm ← 隐藏"如何处理"的决策
module_io_protocol ← 隐藏"如何读写"的决策第一种划分,每个步骤都暴露给其他模块,改任何一步都可能牵连所有人。第二种划分,改存储格式只需改 module_storage_format,其他模块感知不到。
这个区别决定了模块系统的质量:按步骤划分容易,按设计决策划分才有真正的信息隐藏效果,才能让局部修改不传播到全局。
二、如何划分边界?(设计论)
接口是承诺,实现是自由
信息隐藏的核心逻辑:
公开接口 → 对使用者的承诺,不能随意改变
私有实现 → 只属于自己的自由,可以随时重写把实现设为私有,就获得了重构的自由——你可以把整个内部算法推倒重来,只要公开接口不变,所有使用者不受影响。这是可维护性的根基,也是为什么 private 不是限制,而是保护。
循环依赖:设计错误的形式证明
如果模块 A 依赖 B,同时 B 依赖 A,说明这两个模块在概念上是同一个东西,只是假装成两个。
依赖结构是真实概念结构的镜像。循环依赖不只是技术问题,而是边界划错了的证明——需要的不是技术 workaround,而是重新思考边界。
| 语言 | 循环依赖行为 | 检测时机 | 真正的解法 |
|---|---|---|---|
| Python | 可能运行成功,但容易出问题 | 运行时 | 提取公共概念到第三个模块 |
| TypeScript | ESM 会警告,但可能编译通过 | 编译时(警告) | 依赖注入、接口抽象 |
| Rust | 编译错误,强制解决 | 编译时(错误) | 提取公共模块,重新划分边界 |
Rust 强制报错不是苛刻,是诚实——它拒绝让你用技术手段掩盖概念错误。
命名空间是概念地图
模块的组织方式应该反映问题域的结构。两种常见组织方式:
按技术层次:按领域概念:
service/ user/
user.ts service.ts
order.ts model.ts
model/ repository.ts
user.ts order/
order.ts service.ts
repository/ model.ts
user.ts
order.ts哪种模块树更贴近你的领域,哪种就更稳定——技术层次会变(从 MVC 到 DDD),但领域概念相对稳定。模块树和领域模型的偏差,是”代码坏味道”最深层的来源。
三、谁能越过边界?(权限论)
可见性控制是”主动无知”的执行机制——它决定了谁有权利知道什么。
三种哲学立场,和 01 类型系统 的身份认定三种立场(鸭子/结构/标称)高度对应:
| 策略 | 哲学 | 机制 | 代表语言 |
|---|---|---|---|
| 约定 | 信任共识——大家都知道规则,自觉遵守 | _ 前缀 + __all__,语言不强制 | Python |
| 编译时检查 | 工具验证——编译器在构建时核实 | private 关键字,运行时可绕过 | TypeScript、Java |
| 编译器强制 | 形式证明——不满足约束,代码不存在 | pub 体系,无法绕过 | Rust |
核心洞察:可见性控制不是安全机制,是认知管理机制。private 不是”秘密”,是”你不需要知道这个来使用我”。目标是缩小使用者需要理解的认知面积——公开接口越小,使用者的心智负担越低。
Python:社区共识
Python 没有 public / private 关键字。可见性靠命名约定实现:
| 约定 | 含义 | 实际效果 |
|---|---|---|
name | 公开 API | 可自由导入和使用 |
_name | 内部实现(约定) | from module import * 不导入,但手动导入可以 |
__name | 名称修饰 | 类内部变为 _ClassName__name,防止子类意外覆盖 |
__all__ | 模块公开接口列表 | 控制 * 导入的范围 |
Python 的立场是:信任比强制更重要。约定比规则更灵活,大多数情况下足够用——但在大型团队中,Linter 规则(pylint、flake8)成为事实上的”软编译器”,承担部分验证职责。
TypeScript:编译时验证
TypeScript 的 public / private / protected 是编译时检查——编译后完全消失,运行时无法阻止访问”私有”成员((obj as any)._field 可以绕过)。
JavaScript 的 # 私有字段是真正的运行时私有:
class Counter {
#count = 0; // 运行时强制私有,无法从外部访问
private label = ''; // 编译时检查,运行时可绕过
}两者的区别反映了”编译时验证”和”运行时强制”的哲学差异。TypeScript 选择编译时,是因为目标是开发者体验,不是运行时安全。
Rust:细粒度的承诺范围
Rust 的可见性不只是 pub / 私有二分,而是对承诺范围的精确控制:
| 修饰符 | 承诺范围 |
|---|---|
| 无(默认) | 仅当前模块及其子模块可见 |
pub | 任何地方可见 |
pub(crate) | 当前 crate 内可见 |
pub(super) | 父模块可见 |
pub(in path) | 指定路径内可见 |
pub(crate) 的存在尤其有意思:这个函数对当前 crate 内部是公开的,但对外部消费者是私有的。这是对”内部实现 vs 公开 API”的精确区分,在 TypeScript 中需要靠文档约定,在 Rust 中编译器强制。
四、边界如何组合?(组合论)
ESM vs CJS:时态哲学的分歧
JavaScript 的模块系统是两种时态哲学共存的结果——和 07 编译与执行 的 AOT vs 解释执行是同一种分歧的模块版本:
| 维度 | ESM(静态) | CJS(动态) |
|---|---|---|
| 加载时机 | 编译时确定依赖图 | 运行时执行 require() |
| 时态哲学 | 先知——提前决定所有依赖 | 经验主义——运行时按需加载 |
| Tree-shaking | 支持(静态结构可分析) | 不支持 |
| 条件加载 | 需要动态 import() | 原生支持 |
| 顶层 await | 支持 | 不支持 |
ESM 是现代标准,但 CJS 在 Node.js 生态中十五年积累的历史包袱至今共存。两者混用时的 "type": "module"、.mjs / .cjs 扩展名,是”历史时态冲突”留下的工程代价。
Tree-shaking 的本质:因为 ESM 的依赖图在编译时完全确定,打包工具可以静态分析哪些 export 从未被 import,直接删除——这是模块系统的静态性换来的性能红利。CJS 的动态 require() 让这个分析不可能。
Rust:显式模块树
Rust 的模块树不是从文件系统发现的,而是在代码中显式声明的:
// main.rs
mod models; // 声明 models 模块存在
pub mod services; // 声明 services 模块对外可见这看起来是额外负担,实质是”显式优于隐式”的工程哲学:编译器不需要扫描文件系统,模块树在编译开始前完全确定,有利于增量编译。Python 和 JavaScript 文件即模块的设计更方便,但”文件存在但从未被导入”的死模块问题无法被编译器发现。
孤儿规则:所有权哲学在模块层的投影
Rust 的孤儿规则:为类型 T 实现 trait Tr 时,T 或 Tr 中至少有一个必须在当前 crate 中定义。
这防止了边界入侵——第三方 crate 不能声称对别人类型的行为权。如果允许任意 crate 为任意类型实现任意 trait,两个 crate 可能对同一类型提供冲突的实现,编译器无法决定用哪个。
孤儿规则是 06 内存管理 所有权概念在模块层面的投影:每个行为的定义权,必须有明确的归属。
五、多语对照
可见性哲学光谱,从约定到证明:
约定(信任)验证(工具)证明(编译器)
│ │ │
Python `_` TypeScript Rust `pub`
Go 首字母大写 Java 四级修饰 体系
JavaScript `#`Go 的可见性策略:首字母大写 = 公开,首字母小写 = 私有。是 Python 约定的变体,但规则更简单、更一致——不需要 _ 前缀约定,整个生态统一遵守。Java 的四级修饰(public / protected / package-private / private)是编译时验证,比 TypeScript 更细粒度但不如 Rust 的 pub(crate) 精确。
| 维度 | Python | TypeScript / JS | Rust |
|---|---|---|---|
| 模块组织 | 文件即模块,目录即包 | ESM + CJS 双模型 | 显式 mod 声明 |
| 可见性机制 | _ 约定 + __all__ | private(编译时)/ #(运行时) | pub 体系(编译器强制) |
| 循环依赖 | 允许(不推荐) | 允许(警告) | 编译错误 |
| 导入时机 | 运行时动态 | 编译时静态(ESM) | 编译时静态 |
| 孤儿规则 | 无 | 无 | 有 |
| Tree-shaking | 不适用 | 支持(ESM) | 支持 |
| 单独编译 | 不支持 | 支持(.d.ts) | 支持 |
| 大型项目适用性 | 中(约定弱) | 良好(类型边界) | 优秀(编译器边界) |
三语的模块系统差异,本质上是对”认知复杂度应该由谁管理”的不同答案:Python 相信程序员的自律,TypeScript 相信工具的验证,Rust 相信编译器的证明。和 03 包管理与工具链 一起,构成了边界管理的两个尺度——模块管理项目内的认知边界,包管理项目间的信任边界。
延伸阅读
理论:
- Parnas《On the Criteria To Be Used in Decomposing Systems into Modules》(1972) — 模块化设计奠基,至今仍是最好的模块设计指南
- Miller《The Magical Number Seven, Plus or Minus Two》(1956) — 工作记忆容量的认知科学基础
实践:
- Rust 模块系统 — 显式模块树的官方指南
- ESM 规范 — ECMAScript 模块标准
- Python import 系统 — 运行时模块加载机制
关联 meta 维度
- 03 包管理与工具链 — 模块是项目内的认知边界,包是项目间的信任边界;两者是同一哲学在不同尺度的体现
- 01 类型系统 — 类型定义值的契约,模块定义接口的契约;可见性的三种哲学(约定/验证/证明)与类型身份认定(鸭子/结构/标称)完全对应
- 04 错误处理 — 错误边界往往也是模块边界:公开 API 应暴露类型化的错误,内部错误不应泄漏到模块外
- 06 内存管理 — Rust 孤儿规则是所有权哲学在模块层的投影:每个行为定义权必须有明确归属
- 07 编译与执行 — ESM vs CJS 是 AOT vs 解释执行的模块版本;显式模块树有利于增量编译