关于 software-design-philosophy
一份面向软件开发者的设计理念指南,提炼自 John Ousterhout 的《A Philosophy of Software Design》。它以管理复杂性为核心,整理模块、接口、注释和错误处理等设计原则及代码审查信号。
主要功能
- 01识别复杂性的症状与根源
- 02设计接口简洁的深模块
- 03通过信息隐藏减少依赖
- 04将复杂性留在模块内部
- 05用设计原则检查代码
适用场景
- 01评估模块边界与接口
- 02在代码审查中发现设计风险
- 03规划重构以降低认知负担
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ luoling8192/ software-design-philosophy-skill --skill software-design-philosophy安装后在 Claude Code 中输入/software-design-philosophy确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEREADME.mdcodex-skill.md详细介绍
软件设计哲学——精要指南
来源:John Ousterhout,《软件设计哲学》 核心论点:软件设计的核心挑战是管理复杂性。
I. 复杂性:认清敌人
定义
复杂性是指软件系统结构中任何会让系统难以理解和修改的因素。复杂性不等同于系统规模——小型系统也可能很复杂,而设计良好的大型系统也可能易于管理。
复杂性的三种表现
- 变更放大:看似简单的变更,需要在许多不同位置修改代码。
- 认知负担:开发者必须掌握大量信息,才能安全地完成任务。注意:代码行数更少 ≠ 更简单——有时更多代码反而更简单,因为它降低了认知负担。
- 未知的未知:不清楚必须修改哪些代码,或完成任务需要哪些信息。这是最危险的表现。
两个根本原因
- 依赖关系:一段代码无法脱离其他代码单独理解或修改;它与其他也必须纳入考虑的代码相关联。
- 晦涩难懂:重要信息并不明显——名称含糊、缺少文档、约定隐含、约束隐藏。
关键认识
- 复杂性是逐步累积的:它并非由某个灾难性的错误造成,而是通过数千个细小的决策不断累积。
- 因此,你必须采取零容忍的态度——每一点“微不足道”的复杂性都很重要。
II. 战术编程与战略编程
战术编程(反模式)
- 目标:尽可能快地让功能正常运行。
- 思维方式:“先让它能用”“以后再重构。”
- 结果:复杂性迅速累积,技术债务失控。
战略编程(推荐)
- 目标:打造优秀的设计;可运行的代码只是副产品。
- 思维方式:将开发时间的约 10–20% 投入设计改进。
- 做法:
- 每次变更都要寻找改进设计的机会。
- 代码能运行还不够——设计质量同样重要。
- 软件开发的增量应该是抽象,而不是功能。
III. 深模块:最重要的设计概念
核心比喻
把模块想象成一个矩形:
- 宽度 = 接口的复杂性
- 高度/深度 = 内部隐藏的功能量
深模块:接口简单,实现丰富。(好的设计) 浅模块:接口复杂,功能很少。(糟糕的设计 🚩)
经典示例
- 深:Unix 文件 I/O——仅 5 个系统调用(open、read、write、lseek、close)就提供了强大的文件系统。
- 浅:Java I/O——读取文件需要组合使用 FileInputStream、BufferedInputStream、ObjectInputStream 等。
实用原则
- 设计接口时,应让最常见的用法尽可能简单。
- 简单的接口比简单的实现更重要。
- 少见的用例可以接受更复杂的调用方式,但常见路径绝不应为此付出代价。
IV. 信息隐藏与信息泄漏
信息隐藏
- 每个模块都应封装设计决策(知识),仅暴露简化的接口。
- 隐藏的信息包括:数据结构、算法、底层机制、策略决策。
- 信息隐藏可以最大限度减少模块间的依赖关系。
信息泄漏 🚩 危险信号
- 同一个设计决策体现在多个模块中时,就发生了信息泄漏。
- 按时间顺序分解是常见原因:按照执行顺序(而非信息隐藏)拆分模块,会导致各个步骤共享过多知识。
修复信息泄漏
- 将共享的知识合并到一个模块中。
- 如果无法合并,就通过一个统一的深模块封装共享信息。
V. 通用模块与专用模块
核心原则:通用模块通常更深入。
- 通用接口比专用接口更简单,因为它用更少的方法覆盖更多用例。
- 设计新模块时,请问自己:什么是能够满足我当前需求的最通用接口?
判断标准
- 接口应足够通用,无需修改即可支持多种用例。
- 但实现只需满足当前需求(不要过度设计)。
- 通用代码和专用代码应清晰分离。
VI. 不同层次,不同抽象
原则
- 软件系统有多个层次;每个层次都应提供与相邻层次不同的抽象。
- 如果两个层次的抽象相似,说明分层有误。
透传方法 🚩 危险信号
- 一个方法几乎什么都不做,只是将参数转发给签名相似的另一个方法。
- 这表明各层没有提供不同的抽象——职责划分存在问题。
透传变量 🚩 危险信号
- 一个变量在各层之间从上到下传递,但这些层并不使用它。
- 解决方案:使用上下文对象、依赖注入,或重新考虑模块边界。
VII. 将复杂性下移
核心原则
- 当复杂性无法避免时,模块应在内部吸收复杂性,而不是将其推给调用方。
- 大多数模块的用户多于开发者——让开发者多费些心,比让每位用户都费心更好。
反模式
- 将困难的决策变成配置参数,再推给系统管理员。
- 对不确定的情况抛出异常,让调用方处理。
- 这些做法短期内省力,却会放大整个系统的复杂性。
VIII. 让错误不复存在
核心见解
- 异常处理是复杂性的最大来源之一。
- 减少必须处理的异常数量,是降低复杂性的最佳方法之一。
策略
-
重新定义语义,使错误条件不可能出现。
- 示例:即使键不存在,
unset(key)也能成功——它只保证“调用后,键不存在”。 - 示例:
substring(start, end)会自动裁剪越界参数,而不是抛出异常。
- 示例:即使键不存在,
-
异常屏蔽:在较低层检测并处理异常,避免异常传递到调用方。
-
异常汇总:在一个集中位置处理多种异常类型,而不是在每个调用点分散设置处理程序。
重要区别
- 这并不是要忽略错误,而是要设计更好的语义,让错误条件本身不再构成错误。
- 对于确实需要报告的错误(例如网络数据包丢失),仍必须妥善处理。
IX. 设计两次
- 对于任何重要的设计决策,在选择之前至少构思两种不同方案。
- 即使第一个想法看起来很棒,也要强迫自己思考一个替代方案。
- 比较维度:接口简洁性、通用性、性能、实现难度。
- 养成这个习惯能显著提升设计质量。
X. 注释的理念
为什么要写注释
- 注释记录了代码无法表达的设计决策和意图。
- 注释是抽象的一部分——良好的接口文档意味着用户不必阅读实现。
- 尽早编写注释,能在投入编写代码之前暴露设计问题。
- 好的注释能大幅减轻认知负担。
注释应描述什么
- 不明显的信息:原因、约束、边界条件、副作用——也就是代码中看不出来的内容。
- 注释不应重复代码已经表达的内容。🚩 危险信号:注释重复代码
注释层次
- 接口注释:说明是什么以及为什么——不包含实现细节。
- 实现注释:解释如何实现以及为何采用这种方式——说明代码为何这样编写。
- 跨模块注释:记录跨越模块边界的设计决策和依赖关系。
注释优先的方法
- 先写接口注释,再编写实现——将注释用作设计工具。
- 如果无法写出清晰的注释,设计本身可能就有问题。
XI. 命名的艺术
好名称的标准
- 准确:能够向读者传达含义,不产生歧义。
- 一致:同一个名称在整个代码库中始终表示同一含义。
- 信息丰富:好名称本身就是简洁的文档。
命名警示信号 🚩
- 含糊的名称:过于笼统(例如,
data、result、tmp、info)——不包含有用信息。 - 难以命名:如果很难为某个实体找到准确、直观的名称,设计本身可能存在缺陷。
XII. 一致性
- 在整个系统中,同一事物采用相同的处理方式。
- 一致性涵盖:命名、编码风格、接口模式、设计模式、不变量。
- 一致性能够大幅降低认知负担——开发者学会一种模式后,就可以在各处应用。
- 不要为了“更好”而打破一致性,除非改进足够显著,并且能够彻底替换旧方式。
XIII. 代码应当清晰易懂
目标
- 读者无需付出大量心力,就能快速理解代码的行为和意图。
- 清晰易懂是检验设计是否优秀的终极标准。
编写清晰易懂代码的技巧
- 使用恰当的命名并保持一致。
- 合理运用空白和格式来展现结构。
- 用注释解释不明显之处。
- 避免在事件驱动代码中使用隐式控制流(除非有必要)。
代码不清晰易懂 🚩 警示信号
- 如果读者无法轻松理解一段代码的行为或含义,那么它还不够清晰易懂。
XIV. 警示信号速查
在代码审查和自我审查中留意以下信号:
| 信号 | 含义 |
|---|---|
| 浅层模块 | 接口几乎与其实现一样复杂 |
| 信息泄漏 | 同一设计决策体现在多个模块中 |
| 按时间顺序分解 | 模块按照执行顺序划分,而非按照信息隐藏原则划分 |
| 过度暴露 | 常用 API 迫使调用方了解很少使用的功能 |
| 透传方法 | 方法只是将参数转发给另一个签名相似的方法 |
| 重复 | 多个位置重复了非简单代码 |
| 通用与专用混杂 | 通用代码和专用代码没有清晰分离 |
| 方法相互依赖 | 要理解一个方法,就必须理解另一个方法 |
| 注释重复代码 | 注释只是对代码的文字翻译 |
| 实现污染接口 | 接口文档暴露了用户不需要了解的实现细节 |
| 名称含糊 | 名称过于笼统,无法传达有用信息 |
| 难以取名 | 找不到准确、直观的名称——设计可能存在缺陷 |
| 难以描述 | 要完整说明就必须写很长的文档——模块可能过于复杂 |
| 代码不清晰易懂 | 代码的行为或含义难以理解 |
XV. 设计原则总结
- 复杂性是逐步累积的——关注细节。
- 代码能运行还不够。
- 持续进行小幅改进,提升系统设计。
- 模块应该足够深。
- 接口应尽可能简化最常见的用法。
- 简单的接口比简单的实现更重要。
- 通用模块更深。
- 将通用代码与特殊用途代码分开。
- 不同层应有不同的抽象。
- 将复杂性下移。
- 消除错误(和特殊情况)的定义。
- 设计两遍。
- 注释应描述代码中不明显的内容。
- 软件设计应以易于阅读为目标,而非易于编写。
- 软件开发的增量应是抽象,而非功能。
使用指南
在以下场景中参考本指南:
- 代码审查:使用“危险信号”速查表发现设计问题。
- API/接口设计:力求构建深模块;确保常见用法的接口简单易用。
- 模块分解:依据信息隐藏进行拆分,而非依据执行顺序或行数。
- 错误处理:优先采用“消除错误的定义”,减少异常传播。
- 重构:每次修改都投入 10–20% 的精力改进周边设计。
- 命名和注释:名称应准确;注释应说明代码未展示的内容。
评论
支持 Markdown