name: 系统设计 description: 构建可重用编码系统的原则。适用于设计模块、API、CLI 或其他供他人使用的代码。基于 John Ousterhout 的 “A Philosophy of Software Design”。涵盖深模块、复杂性管理和设计红色标志。 tags:
- 设计
- 架构
- 模块
- 复杂性
系统设计
构建可重用、可维护编码系统的原则。来自 John Ousterhout 的 “A Philosophy of Software Design”。
核心原则:对抗复杂性
复杂性是大多数软件问题的根本原因。它逐步积累——每个捷径都增加一点,直到系统变得不可维护。
复杂性定义: 任何使软件难以理解或修改的东西。
症状:
- 变更放大:简单变更需要多次修改
- 认知负载:进行变更所需了解的内容量
- 未知未知:不清楚需要改变什么
深模块
最重要的设计原则:使模块深。
┌─────────────────────────────┐
│ 简单接口 │ ← 小表面积
├─────────────────────────────┤
│ │
│ │
│ 深实现 │ ← 大量功能
│ │
│ │
└─────────────────────────────┘
深模块: 简单接口,背后隐藏大量功能。
浅模块: 接口复杂度相对于提供的功能高。红色标志。
示例
深: Unix 文件 I/O - 仅 5 个调用(open, read, write, lseek, close)隐藏了巨大的复杂性(缓冲、缓存、设备驱动程序、权限、日志)。
浅: Java 的文件读取需要 BufferedReader 包装 FileReader 包装 FileInputStream。接口复杂度与实现复杂度匹配。
应用此原则
- 偏好做更多事情的较少方法,而不是许多小方法
- 积极隐藏实现细节
- 模块的接口应比其实现简单得多
- 如果接口与实现一样复杂,重新考虑抽象
战略与战术编程
战术: 现在让它工作。每个任务增加小的复杂性。债务累积。
战略: 投资时间在良好设计上。初始较慢,长期更快。
进度
│
│ 战略 ────────────────→
│ /
│ /
│ / 战术 ─────────→
│ / ↘ (变慢)
│ /
└──┴─────────────────────────────────→ 时间
经验法则: 将开发时间的 10-20% 用于设计改进。
工作代码不够
“工作代码”不是目标。目标是既工作又有良好设计。如果你满足于“它能工作”,你就是在进行战术编程。
信息隐藏
每个模块应封装其他模块不需要的知识。
信息泄漏(红色标志): 相同知识出现在多个地方。如果一处改变,所有都必须改变。
时间分解(红色标志): 根据事情发生的时间而不是它们使用的信息来拆分代码。经常导致泄漏。
应用此原则
- 问:“这个模块封装了什么知识?”
- 如果答案是“不多”,模块可能很浅
- 根据所知而不是运行时间分组代码
- 默认私有;只暴露必要的内容
通过设计消除错误
异常增加复杂性。处理它们的最佳方式:设计使其不可能发生。
而不是:
function deleteFile(path: string): void {
if (!exists(path)) throw new FileNotFoundError();
// 删除...
}
做:
function deleteFile(path: string): void {
// 直接删除。如果不存在,目标已达到。
// 无需处理错误。
}
应用此原则
- 重新定义语义使错误变得无关紧要
- 内部处理边缘情况而不是暴露它们
- 更少的异常 = 更简单的接口 = 更深的模块
- 问:“我能改变定义使这不是错误吗?”
通用模块
某种程度上通用的模块比特殊用途的模块更深。
不要太通用: 不要在你需要一个函数时构建框架。
不要太具体: 不要硬编码限制重用的假设。
最佳点: 以自然处理明天问题的方式解决今天的问题。
要问的问题
- 覆盖所有当前需求的最简单接口是什么?
- 这个方法将用在多少情况下?
- 这个 API 对我的当前需求易于使用吗?
将复杂性向下推
当复杂性不可避免时,将其放在实现中,而不是接口中。
坏: 将复杂性暴露给所有调用者。 好: 内部处理复杂性一次。
模块有简单接口比简单实现更重要。
示例
配置:不是要求调用者配置所有内容,而是提供合理的默认值。内部处理选择默认值的复杂性。
设计两次
在实现之前,考虑至少两种不同的设计。比较它们。
好处:
- 揭示你没有意识到的假设
- 通常第二种设计更好
- 即使第一种设计胜出,你理解了原因
不要跳过: “我想不出其他方法”通常意味着你没有足够努力。
红色标志摘要
| 红色标志 | 症状 |
|---|---|
| 浅模块 | 接口复杂度 ≈ 实现复杂度 |
| 信息泄漏 | 相同知识在多个模块中 |
| 时间分解 | 代码按时间拆分,而不是信息 |
| 过度暴露 | 接口中太多方法/参数 |
| 传递方法 | 方法除了调用另一个外很少做其他事 |
| 重复 | 相同代码模式多次出现 |
| 特殊-通用混合 | 通用代码与特殊用途代码混合 |
| 联合方法 | 不能理解一个而不读另一个 |
| 注释重复代码 | 注释说明代码明显做什么 |
| 模糊名称 | 名称没有传达太多信息 |
应用于 CLI/工具设计
当构建 CLIs、插件或工具时:
- 深命令: 做很多事情的较少命令,而不是许多浅命令
- 合理默认值: 在常见情况下无需配置工作
- 渐进披露: 简单用法优先,高级选项可用
- 一致接口: 所有命令使用相同模式
- 错误消除: 设计使常见错误不可能发生
示例:良好 CLI 设计
# 深:一个命令处理好常见情况
swarm setup
# 不浅:不需要 10 个标志用于基本使用
# 合理默认值:选择合理模型
# 渐进:高级用户以后可以自定义
关键要点
- 复杂性是敌人。 每个设计决策都应减少它。
- 深模块胜出。 简单接口,丰富功能。
- 隐藏信息。 每个模块拥有特定知识。
- 设计消除错误。 改变语义以消除边缘情况。
- 设计两次。 总是考虑替代方案。
- 战略 > 战术。 投资于设计,而不仅仅是工作代码。
- 将复杂性向下推。 实现吸收复杂性,接口保持简单。