software-design-philosophy

software-design-philosophy

热门

通过深度模块、信息隐藏和战略性编程管理软件复杂性。当用户提到“模块设计”、“API过于复杂”、“浅层类”、“复杂性预算”、“战略性与战术性”、“深度模块”、“信息泄露”、“透传方法”、“这段代码过度设计”或“简化这个设计”时使用。在审查接口的简洁性、评估抽象是否值得、决定是否值得编写注释、或在通用方法与专用方法之间选择时也触发。涵盖深度与浅层模块、复杂性警示标志、以及作为设计文档的注释。关于代码质量,请参见clean-code。关于架构边界,请参见clean-architecture。

1698Star
171Fork
更新于 2026/7/16
SKILL.md
readonly只读
name
software-design-philosophy
description

通过深度模块、信息隐藏和战略性编程管理软件复杂性。当用户提到“模块设计”、“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 而不是 nretryDelayMs 而不是 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 是斯坦福大学计算机科学系的Bosack Lerner教授,也是Tcl脚本语言和Tk工具包的创建者。他根据斯坦福CS 190课程开发了《软件设计哲学》,将数十年的系统构建经验提炼为适用于各种语言和规模的原则。