关于 software-design-philosophy
面向软件工程师的设计框架,帮助在模块设计、API 评审、重构和架构决策中管理复杂性。它以深度模块、信息隐藏和战略性编程等原则,评估设计如何影响系统的理解与修改。
主要功能
- 01复杂性诊断与评分
- 02深度模块与简化接口
- 03识别并减少信息泄漏
- 04平衡通用性与特定需求
- 05以注释记录设计意图
- 06将重构融入日常开发
适用场景
- 01评审模块或 API 设计
- 02重构难以理解的代码
- 03定位跨模块的信息泄漏
- 04评估架构决策对复杂性的影响
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wondelai/ skills --skill software-design-philosophy安装后在 Claude Code 中输入/software-design-philosophy确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/comments-as-design.mdcomplexity-symptoms.mddeep-modules.mdgeneral-vs-special.mdinformation-hiding.mdstrategic-programming.md详细介绍
软件设计哲学框架
管理软件工程根本挑战——复杂性——的实用框架。在设计模块、评审 API、重构代码或提供架构决策建议时,运用这些原则。
核心原则
**编写软件时最大的限制,是我们理解自己正在构建的系统的能力。**复杂性是敌人:它会让系统难以理解、难以修改,并成为 Bug 的来源。评估每项设计决策时,都要问:“这会增加还是降低系统的整体复杂性?”——目标不是消除所有复杂性,而是尽量减少不必要的复杂性,并将必要的复杂性集中到可管理之处。
评分
**目标:10/10。**评审或创建设计时,根据满足的快速诊断表中的 8 行来评分(每项约 1.25 分),然后对照以下区间进行合理性检查:
- 9-10 — 深模块的接口远比实现简单;没有信息泄漏(实现可以变更而无需修改调用方);接口注释阐明设计意图;设计改进是常规工作。8 项诊断全部通过。
- 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、gRPC 与队列之间的差异 |
当一项变更迫使你同步修改两个模块时,请参阅 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. 将注释作为设计文档
核心概念: 注释应描述代码中不明显的内容:设计意图、抽象的理由、不变量和假设。对于低层实现细节以外的内容,「好代码会自我说明」是一种迷思。
为何有效: 代码只能记录它做什么,却永远无法记录为什么选择这种方案而不是其他方案,也无法记录它默默依赖的假设。这些理由是系统中最容易消失的信息:它们只存在于作者脑海中,作者一离开就会消失,因此注释是记录这些信息的唯一机会。
关键要点:
- 注释分为四类:接口注释(最重要——它们定义抽象)、数据结构成员注释、实现注释、跨模块注释
- 先写注释(注释驱动设计),在编写代码之前理清思路
- 不要重复代码中已明确表达的内容;注释放在其描述的代码旁边,并与代码一同更新
- 如果注释很难写,设计可能过于复杂
代码应用:
| 场景 | 模式 | 示例 |
|---|---|---|
| 接口注释 | 描述抽象,而非实现 | 「返回最接近指定位置的 widget;如果没有任何 widget 在阈值范围内,则返回 null」 |
| 数据结构注释 | 说明不变量 | 「列表按优先级降序排列;优先级相同时按插入顺序排列」 |
| 实现注释 | 解释原因,而非内容 | // 二分查找:列表始终有序,可容纳 100k+ 个项目 |
| 跨模块注释 | 关联相关决策 | // 此超时值必须与 RetryPolicy.java 中的重试间隔一致 |
编写或审阅注释时,如果不确定该写什么,请参阅 references/comments-as-design.md:其中包含四类注释及示例、注释驱动设计流程,以及对「代码自我说明」迷思的反驳。
6. 战略编程与战术编程
核心概念: 战术编程能快速实现功能,但每一次走捷径都会累积复杂性。战略编程会多投入 10-20% 的精力做好设计,把每次改动都视为改善结构的机会。
为何有效: 战术编程的速度是借来的:每一次走捷径都会让后续改动更困难,而战略投入则会产生复利效应——经过战略设计的系统,几个月后会更容易修改。
关键见解:
- 战术龙卷风:一位交付迅速却留下满地狼藉的开发者——短期内备受赞誉,长期来看却具有破坏性
- 你的首要工作是做好设计,并让它恰好能正常运行,而不是写出恰好有设计的可运行代码
- 初创公司最需要战略编程——早期走的捷径会不断累积,随着团队壮大,最终变成沉重的技术债
- 每次改动都是投资机会:让代码变得稍微更好一点;重构是每项功能的一部分,而非特殊事件
代码应用:
| 场景 | 模式 | 示例 |
|---|---|---|
| 战术陷阱 | 抵制快速粗糙的修复 | 不要为了“就这一个特殊情况”添加布尔参数 |
| 战略投资 | 在开发功能时改善结构 | 添加功能时,重构笨拙的模块接口 |
| 设计评审 | 评估结构,而不仅是正确性 | 询问“这样能让系统更简单吗?”,而不只是“这样能运行吗?” |
在决定一项改动值得投入多少设计精力,或为投入设计精力陈述理由时,请参阅 references/strategic-programming.md——其中介绍了 10-20% 投入的计算、战术龙卷风模式,以及初创公司为何最需要战略编程。
常见错误
| 错误 | 失败原因 | 修正方法 |
|---|---|---|
| 创建太多小类 | 类膨胀会增加接口,却不增加深度;每个边界都会带来认知负担 | 将相关的浅层类合并为更深入的模块 |
| 按时间顺序拆分模块 | “先读取,再处理,最后写入”会迫使不同模块共享知识 | 将共享知识的代码归入同一模块 |
| 在接口中暴露实现细节 | 调用方会依赖内部细节;改动会层层传递 | 围绕抽象设计接口;隐藏格式和协议 |
| 把注释视为可有可无 | 设计意图和假设会丢失;新成员只能猜测 | 先写接口注释;并与代码一同维护 |
| 事事都用配置参数 | 将参数交给调用方,就意味着你放弃了做决定(参见 §4) | 自动确定行为;提供合理的默认值 |
| 快速粗糙的战术修复 | 走捷径会不断累积,直到系统无法使用 | 额外投入 10-20% 的精力;将每次改动都视为设计机会 |
| 透传方法 | 只把参数转发给另一个方法的方法增加了接口,却没有增加功能 | 将透传逻辑合并到调用方或被调用方中 |
| 针对特定用例进行设计 | 特殊用途的接口会不断累积特殊情况 | 换个角度问:能满足所有当前需求的最简单接口是什么? |
快速诊断
| 问题 | 如果否 | 行动 |
|---|---|---|
| 你能用一句话描述每个模块吗? | 模块承担的工作过多,或缺乏明确用途 | 拆分为职责连贯且易于描述的部分 |
| 接口是否比实现更简单? | 模块过于浅薄——复杂性泄漏到了外部 | 隐藏更多细节;将浅层类合并为更深的类 |
| 你能在不影响调用方的情况下更改实现吗? | 信息正在跨越边界泄漏 | 将泄漏的信息封装到一个模块中 |
| 接口注释是否描述了抽象? | 设计意图丢失;模块可能被误用 | 记录模块的承诺,而不是它的工作方式 |
| 代码评审是否包含设计讨论? | 评审能发现错误,却无法阻止复杂性增长 | 将“这是否降低了复杂性?”纳入评审标准 |
| 每个模块是否都隐藏着一个重要的设计决策? | 模块是围绕代码而非信息组织的 | 重新组织模块,让每个模块负责特定知识 |
| 新人能否在不阅读实现的情况下理解模块边界? | 抽象缺少文档说明或存在泄漏 | 改进接口注释;简化接口 |
| 你是否将 10-20% 的时间用于改进设计? | 每增加一个功能,技术债务都会累积 | 在每个 PR 中纳入设计改进 |
延伸阅读
如需查看包含详细示例的完整方法:
- "A Philosophy of Software Design",作者 John Ousterhout(第 2 版)
关于作者
John Ousterhout 是斯坦福大学计算机科学系 Bosack Lerner 教授,也是 Tcl 脚本语言和 Tk 工具包的创作者。他基于在斯坦福大学 CS 190 课程中的教学,撰写了 A Philosophy of Software Design,将数十年的系统构建经验提炼为适用于不同语言和规模的原则。