
software-design-philosophy
热门通过深度模块、信息隐藏和战略性编程管理软件复杂性。当用户提到“模块设计”、“API过于复杂”、“浅层类”、“复杂性预算”、“战略性与战术性”、“深度模块”、“信息泄露”、“透传方法”、“这段代码过度设计”或“简化这个设计”时使用。在审查接口的简洁性、评估抽象是否值得、决定是否值得编写注释、或在通用方法与专用方法之间选择时也触发。涵盖深度与浅层模块、复杂性警示标志、以及作为设计文档的注释。关于代码质量,请参见clean-code。关于架构边界,请参见clean-architecture。
通过深度模块、信息隐藏和战略性编程管理软件复杂性。当用户提到“模块设计”、“API过于复杂”、“浅层类”、“复杂性预算”、“战略性与战术性”、“深度模块”、“信息泄露”、“透传方法”、“这段代码过度设计”或“简化这个设计”时使用。在审查接口的简洁性、评估抽象是否值得、决定是否值得编写注释、或在通用方法与专用方法之间选择时也触发。涵盖深度与浅层模块、复杂性警示标志、以及作为设计文档的注释。关于代码质量,请参见clean-code。关于架构边界,请参见clean-architecture。
软件设计哲学框架
一个实用的框架,用于应对软件工程的根本挑战:复杂性。在设计模块、审查API、重构代码或提供架构决策建议时应用这些原则。
核心原则
编写软件的最大限制是我们理解所创建系统的能力。 复杂性是敌人:它使系统难以理解、难以修改,并成为错误的根源。评估每个设计决策时问自己:“这会增加还是减少系统的整体复杂性?”——目标不是零复杂性,而是最小化不必要的复杂性,并将必要的复杂性集中到可管理的地方。
评分
目标:10/10。 在审查或创建设计时,通过计算它满足八个快速诊断问题中的多少个来评分(每个约1.25分),然后对照区间进行合理性检查:
- 9-10 —— 深度模块,接口远简单于实现;无信息泄露(实现可以更改而不影响调用者);接口注释捕获设计意图;设计改进是常规操作。所有八个诊断通过。
- 6-8 —— 大部分是深度的,但有一两个泄露、浅层类或未文档化的抽象。5-6个诊断通过。
- 3-5 —— 类炎或时间分解、反复泄露、仅重述代码的注释。2-4个诊断通过。
- ≤2 —— 战术性龙卷风代码:浅层模块、普遍泄露、未记录设计意图。0-1个诊断通过。
始终说明当前分数、失败的诊断行以及每个行需要达到10/10的具体更改。
软件设计框架
管理复杂性并产生易于理解和修改的系统的六个原则:
1. 复杂性及其原因
核心概念: 复杂性是指系统结构中使其难以理解和修改的任何东西。它表现出三种症状——变更放大、认知负荷和未知的未知——并有两个原因:依赖关系和模糊性。
关键见解:
- 变更放大:一个简单的更改需要在许多地方进行编辑
- 认知负荷:开发人员必须记住太多信息才能进行更改
- 未知的未知:不清楚必须更改什么或哪些信息是相关的——最糟糕的症状
- 复杂性是渐进的——它由数百个小决策累积而成(“千刀万剐”),因此每个决策都很重要
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 变更放大 | 集中共享知识 | 提取颜色常量,而不是在20个文件中硬编码#ff0000 |
| 认知负荷 | 减少开发人员必须知道的内容 | open(path) 而不是要求缓冲区大小、编码、锁定模式 |
| 未知的未知 | 使依赖关系显式化 | 类型系统和接口揭示更改影响的内容 |
| 模糊性 | 精确命名事物 | numBytesReceived 而不是 n;retryDelayMs 而不是 delay |
当需要命名代码库的哪个症状以便修复时,请参见 references/complexity-symptoms.md —— 每个症状的识别测试、依赖关系分类(语法/语义/时间/隐藏)、C = Σ(cp·tp) 成本公式以及10行红旗表。
2. 深度与浅层模块
核心概念: 最好的模块是深度的:简单接口背后的强大功能。浅层模块的接口相对于它们提供的功能来说过于复杂——它们增加复杂性而不是隐藏它。
为什么有效: 接口是模块对系统其余部分施加的成本;实现是收益。因此,一个比重新实现更难学习的方法净收益为负——深度,而不是代码行数,决定了一个模块是否值得存在。
关键见解:
- 深度 = 提供的功能 / 接口复杂性(Unix文件I/O是深度的;薄薄的Java I/O包装器是浅层的)
- “类炎”:创建太多小而浅的类的疾病——每个接口增加认知负荷
- 小方法本身并不好;深度比大小更重要
- 最好的抽象通过几个简单的概念隐藏了显著的复杂性
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 深度模块 | 在简单API背后隐藏复杂性 | file.read(path) 隐藏磁盘块、缓存、缓冲、编码 |
| 类炎治疗 | 合并相关的浅层类 | RequestParser + RequestValidator + RequestProcessor → 一个 RequestHandler |
| 接口简洁性 | 更少的参数,更少的方法 | config.get(key) 带有合理的默认值,而不是15个构造函数参数 |
当判断一个抽象是否值得时,请参见 references/deep-modules.md —— 深度比的前后代码、类炎治疗的详细过程以及案例研究(Unix I/O、GC、TCP/IP)。
3. 信息隐藏与泄露
核心概念: 每个模块应封装其他模块不需要的知识。信息泄露——一个设计决策反映在多个模块中——是软件设计中最重要的红旗之一。
为什么有效: 一个存在于一个模块中的决策可以在那里更改,而无需其他地方;同一个决策泄露到N个模块中,将一个编辑变成N个编辑,而编译器不会提醒你进行。隐藏是将变更放大转换回局部更改的关键。
关键见解:
- 时间分解导致泄露:按何时发生拆分代码会强制跨阶段共享知识——改为按知识组织
- 通过数据格式、协议或共享假设的后门泄露是最微妙的形式
- 装饰器经常泄露——它们暴露被装饰的接口
- 如果两个模块共享知识,合并它们或创建一个封装该知识的新模块
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 格式泄露 | 集中序列化 | 一个模块拥有JSON编码/解码,而不是到处使用 json.dumps |
| 时间分解 | 按知识组织,而不是时间 | 将“读取配置”和“应用配置”合并到一个配置模块中 |
| 协议泄露 | 抽象传输细节 | MessageBus.send(event) 隐藏HTTP vs. gRPC vs. 队列 |
当更改迫使你同步编辑两个模块时,请参见 references/information-hiding.md —— 四种泄露形式及代码(接口、后门、时间、装饰器)、五种减少策略、HTTP处理案例研究以及检测表。
4. 通用与专用模块
核心概念: 设计“有点通用”的模块:接口足够通用以支持多种用途,实现处理当前需求。问:“覆盖我所有当前需求的最简单接口是什么?”
为什么有效: 反直觉的是,通用接口通常更简单——随着需求增长,特殊情况方法会成倍增加,而一个通用方法可以吸收它们。陷阱是另一个方向:当前需求不需要的通用性是投机复杂性,为可能永远不会出现的用例付出代价。
关键见解:
- “有点通用”是过于具体和过于通用之间的最佳点
- 将复杂性向下推:较低级别的模块应处理困难情况,以便上层保持简单
- 配置参数通常代表未能做出决策——每个参数都是推给调用者的复杂性
- 有疑问时,先实现更简单、更通用的方法
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| API通用性 | 为概念设计,而不是一个用例 | text.insert(position, string) 而不是 text.addBulletPoint() |
| 减少配置 | 自动确定行为 | 自动检测文件编码,而不是使用 encoding 参数 |
| 避免过度专门化 | 一个通用方法优于多个特定方法 | store(key, value, options) 而不是 storeUser()、storeProduct()、storeOrder() |
当选择接口的通用程度时,请参见 references/general-vs-special.md —— “所有当前需求的最简单接口”测试、配置参数反模式以及向下推复杂性的详细过程。
5. 作为设计文档的注释
核心概念: 注释应描述代码中不明显的内容:设计意图、抽象原理、不变性和假设。“好代码是自文档的”对于低级实现细节之外的内容来说是一个神话。
为什么有效: 代码只能记录它做什么——永远不能记录为什么选择这种方法而不是其他方法,或者它默默假设了什么。这个原理是系统中信息最易消失的:它只存在于作者的脑海中,一旦作者离开就消失了,因此注释是捕获它的唯一机会。
关键见解:
- 四种类型:接口注释(最重要——它们定义抽象)、数据结构成员注释、实现注释、跨模块注释
- 先写注释(注释驱动设计)以在代码之前澄清思路
- 不要重复代码已经清楚的内容;将注释放在它们描述的代码旁边,并一起更新
- 如果注释难以编写,设计可能过于复杂
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 接口注释 | 描述抽象,而不是实现 | “返回最接近位置的小部件,如果阈值内没有则返回null” |
| 数据结构注释 | 解释不变性 | “列表按优先级降序排序;平局按插入顺序打破” |
| 实现注释 | 解释为什么,而不是什么 | “// 二分查找:列表始终排序,可容纳10万+项” |
| 跨模块注释 | 链接相关决策 | “// 此超时必须与RetryPolicy.java中的重试间隔匹配” |
当编写或审查注释且不确定应该包含什么时,请参见 references/comments-as-design.md —— 四种注释类型及示例、注释驱动设计过程以及对自文档代码神话的反驳。
6. 战略性编程与战术性编程
核心概念: 战术性编程快速实现功能,但每个捷径都会积累复杂性。战略性编程投入10-20%的额外精力进行良好设计,将每次更改视为改进结构的机会。
为什么有效: 战术性速度是借来的:每个捷径使未来的更改更加困难,而战略性投资会复合增长——战略性设计的系统在几个月内就能更快地工作。
关键见解:
- 战术性龙卷风:快速交付但留下破坏的开发人员——短期受赞誉,长期有破坏性
- 你的主要工作是恰好能工作的优秀设计,而不是恰好有设计的可工作代码
- 初创公司最需要战略性编程——早期捷径随着团队成长会复合为严重的债务
- 每次更改都是投资机会:让代码变得更好一点;重构是每个功能的一部分,而不是特殊事件
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 战术性陷阱 | 抵制快速而粗糙的修复 | 不要为“仅此一个特殊情况”添加布尔参数 |
| 战略性投资 | 在功能开发期间改进结构 | 在添加功能的同时重构笨拙的模块接口 |
| 设计评审 | 评估结构,而不仅仅是正确性 | 问“这会使系统更简单吗?”而不仅仅是“它能工作吗?” |
当决定一个更改应投入多少设计精力,或为此辩护时,请参见 references/strategic-programming.md —— 10-20%投资数学、战术性龙卷风模式以及为什么初创公司最需要战略性编程。
常见错误
| 错误 | 为什么失败 | 修复 |
|---|---|---|
| 创建太多小类 | 类炎增加接口而没有深度;每个边界都是认知开销 | 将相关的浅层类合并为更深的模块 |
| 按时间顺序拆分模块 | “读取,然后处理,然后写入”强制跨模块共享知识 | 将共享知识的代码分组到一个模块中 |
| 在接口中暴露实现 | 调用者依赖内部实现;更改会传播 | 围绕抽象设计接口;隐藏格式和协议 |
| 将注释视为可选 | 设计意图和假设丢失;新人猜测错误 | 先写接口注释;与代码一起维护 |
| 为所有东西使用配置参数 | 推给调用者的参数是你拒绝做出的决策(见§4) | 自动确定行为;提供合理的默认值 |
| 快速而粗糙的战术性修复 | 捷径累积直到系统无法工作 | 投入10-20%额外精力;将每次更改视为设计机会 |
| 透传方法 | 仅将参数转发给另一个方法的方法增加接口但没有功能 | 将透传合并到调用者或被调用者中 |
| 为特定用例设计 | 专用接口积累特殊情况 | 问:覆盖所有当前需求的最简单接口? |
快速诊断
| 问题 | 如果否 | 行动 |
|---|---|---|
| 你能用一句话描述每个模块吗? | 模块做得太多或缺乏目的 | 拆分为连贯、可描述的职责 |
| 接口是否比实现简单? | 模块是浅层的——复杂性向外泄露 | 隐藏更多;将浅层类合并为更深层的类 |
| 你能更改实现而不影响调用者吗? | 信息正在跨边界泄露 | 将泄露的知识封装在一个模块中 |
| 接口注释是否描述了抽象? | 设计意图丢失;模块将被误用 | 记录模块承诺的内容,而不是如何工作 |
| 设计讨论是否是代码评审的一部分? | 评审捕获错误但不捕获复杂性增长 | 将“这能降低复杂性吗?”添加到评审标准中 |
| 每个模块是否隐藏了一个重要的设计决策? | 模块围绕代码组织,而不是信息 | 重新组织,使每个模块拥有特定的知识 |
| 新人能否在不阅读实现的情况下理解模块边界? | 抽象未文档化或泄露 | 改进接口注释;简化接口 |
| 你是否花费10-20%的时间进行设计改进? | 每个功能都会积累债务 | 在每个PR中包含设计改进 |
延伸阅读
有关包含详细示例的完整方法论:
- 《软件设计哲学》 作者:John Ousterhout(第二版)
关于作者
John Ousterhout 是斯坦福大学计算机科学系的Bosack Lerner教授,也是Tcl脚本语言和Tk工具包的创建者。他根据斯坦福CS 190课程开发了《软件设计哲学》,将数十年的系统构建经验提炼为适用于各种语言和规模的原则。





