mcpField
S
software-design-philosophy

software-design-philosophy

github.com/luoling8192开发353252 次安装16 周前更新
代码仓库

关于 software-design-philosophy

一份面向软件开发者的设计理念指南,提炼自 John Ousterhout 的《A Philosophy of Software Design》。它以管理复杂性为核心,整理模块、接口、注释和错误处理等设计原则及代码审查信号。

主要功能

  1. 01识别复杂性的症状与根源
  2. 02设计接口简洁的深模块
  3. 03通过信息隐藏减少依赖
  4. 04将复杂性留在模块内部
  5. 05用设计原则检查代码

适用场景

  1. 01评估模块边界与接口
  2. 02在代码审查中发现设计风险
  3. 03规划重构以降低认知负担

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/luoling8192/software-design-philosophy-skill --skill software-design-philosophy

安装后在 Claude Code 中输入/software-design-philosophy确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
README.md
codex-skill.md

详细介绍

软件设计哲学——精要指南

来源:John Ousterhout,《软件设计哲学》 核心论点:软件设计的核心挑战是管理复杂性。


I. 复杂性:认清敌人

定义

复杂性是指软件系统结构中任何会让系统难以理解和修改的因素。复杂性不等同于系统规模——小型系统也可能很复杂,而设计良好的大型系统也可能易于管理。

复杂性的三种表现
  1. 变更放大:看似简单的变更,需要在许多不同位置修改代码。
  2. 认知负担:开发者必须掌握大量信息,才能安全地完成任务。注意:代码行数更少 ≠ 更简单——有时更多代码反而更简单,因为它降低了认知负担。
  3. 未知的未知:不清楚必须修改哪些代码,或完成任务需要哪些信息。这是最危险的表现。
两个根本原因
  1. 依赖关系:一段代码无法脱离其他代码单独理解或修改;它与其他也必须纳入考虑的代码相关联。
  2. 晦涩难懂:重要信息并不明显——名称含糊、缺少文档、约定隐含、约束隐藏。
关键认识
  • 复杂性是逐步累积的:它并非由某个灾难性的错误造成,而是通过数千个细小的决策不断累积。
  • 因此,你必须采取零容忍的态度——每一点“微不足道”的复杂性都很重要。

II. 战术编程与战略编程

战术编程(反模式)
  • 目标:尽可能快地让功能正常运行。
  • 思维方式:“先让它能用”“以后再重构。”
  • 结果:复杂性迅速累积,技术债务失控。
战略编程(推荐)
  • 目标:打造优秀的设计;可运行的代码只是副产品。
  • 思维方式:将开发时间的约 10–20% 投入设计改进。
  • 做法:
    • 每次变更都要寻找改进设计的机会。
    • 代码能运行还不够——设计质量同样重要。
    • 软件开发的增量应该是抽象,而不是功能。

III. 深模块:最重要的设计概念

核心比喻

把模块想象成一个矩形:

  • 宽度 = 接口的复杂性
  • 高度/深度 = 内部隐藏的功能量

深模块:接口简单,实现丰富。(好的设计) 浅模块:接口复杂,功能很少。(糟糕的设计 🚩)

经典示例
  • 深:Unix 文件 I/O——仅 5 个系统调用(open、read、write、lseek、close)就提供了强大的文件系统。
  • 浅:Java I/O——读取文件需要组合使用 FileInputStream、BufferedInputStream、ObjectInputStream 等。
实用原则
  • 设计接口时,应让最常见的用法尽可能简单。
  • 简单的接口比简单的实现更重要。
  • 少见的用例可以接受更复杂的调用方式,但常见路径绝不应为此付出代价。

IV. 信息隐藏与信息泄漏

信息隐藏
  • 每个模块都应封装设计决策(知识),仅暴露简化的接口。
  • 隐藏的信息包括:数据结构、算法、底层机制、策略决策。
  • 信息隐藏可以最大限度减少模块间的依赖关系。
信息泄漏 🚩 危险信号
  • 同一个设计决策体现在多个模块中时,就发生了信息泄漏。
  • 按时间顺序分解是常见原因:按照执行顺序(而非信息隐藏)拆分模块,会导致各个步骤共享过多知识。
修复信息泄漏
  • 将共享的知识合并到一个模块中。
  • 如果无法合并,就通过一个统一的深模块封装共享信息。

V. 通用模块与专用模块

核心原则:通用模块通常更深入。
  • 通用接口比专用接口更简单,因为它用更少的方法覆盖更多用例。
  • 设计新模块时,请问自己:什么是能够满足我当前需求的最通用接口?
判断标准
  • 接口应足够通用,无需修改即可支持多种用例。
  • 但实现只需满足当前需求(不要过度设计)。
  • 通用代码和专用代码应清晰分离。

VI. 不同层次,不同抽象

原则
  • 软件系统有多个层次;每个层次都应提供与相邻层次不同的抽象。
  • 如果两个层次的抽象相似,说明分层有误。
透传方法 🚩 危险信号
  • 一个方法几乎什么都不做,只是将参数转发给签名相似的另一个方法。
  • 这表明各层没有提供不同的抽象——职责划分存在问题。
透传变量 🚩 危险信号
  • 一个变量在各层之间从上到下传递,但这些层并不使用它。
  • 解决方案:使用上下文对象、依赖注入,或重新考虑模块边界。

VII. 将复杂性下移

核心原则
  • 当复杂性无法避免时,模块应在内部吸收复杂性,而不是将其推给调用方。
  • 大多数模块的用户多于开发者——让开发者多费些心,比让每位用户都费心更好。
反模式
  • 将困难的决策变成配置参数,再推给系统管理员。
  • 对不确定的情况抛出异常,让调用方处理。
  • 这些做法短期内省力,却会放大整个系统的复杂性。

VIII. 让错误不复存在

核心见解
  • 异常处理是复杂性的最大来源之一。
  • 减少必须处理的异常数量,是降低复杂性的最佳方法之一。
策略
  1. 重新定义语义,使错误条件不可能出现。

    • 示例:即使键不存在,unset(key) 也能成功——它只保证“调用后,键不存在”。
    • 示例:substring(start, end) 会自动裁剪越界参数,而不是抛出异常。
  2. 异常屏蔽:在较低层检测并处理异常,避免异常传递到调用方。

  3. 异常汇总:在一个集中位置处理多种异常类型,而不是在每个调用点分散设置处理程序。

重要区别
  • 这并不是要忽略错误,而是要设计更好的语义,让错误条件本身不再构成错误。
  • 对于确实需要报告的错误(例如网络数据包丢失),仍必须妥善处理。

IX. 设计两次

  • 对于任何重要的设计决策,在选择之前至少构思两种不同方案。
  • 即使第一个想法看起来很棒,也要强迫自己思考一个替代方案。
  • 比较维度:接口简洁性、通用性、性能、实现难度。
  • 养成这个习惯能显著提升设计质量。

X. 注释的理念

为什么要写注释
  1. 注释记录了代码无法表达的设计决策和意图。
  2. 注释是抽象的一部分——良好的接口文档意味着用户不必阅读实现。
  3. 尽早编写注释,能在投入编写代码之前暴露设计问题。
  4. 好的注释能大幅减轻认知负担。
注释应描述什么
  • 不明显的信息:原因、约束、边界条件、副作用——也就是代码中看不出来的内容。
  • 注释不应重复代码已经表达的内容。🚩 危险信号:注释重复代码
注释层次
  • 接口注释:说明是什么以及为什么——不包含实现细节。
  • 实现注释:解释如何实现以及为何采用这种方式——说明代码为何这样编写。
  • 跨模块注释:记录跨越模块边界的设计决策和依赖关系。
注释优先的方法
  • 先写接口注释,再编写实现——将注释用作设计工具。
  • 如果无法写出清晰的注释,设计本身可能就有问题。

XI. 命名的艺术

好名称的标准
  • 准确:能够向读者传达含义,不产生歧义。
  • 一致:同一个名称在整个代码库中始终表示同一含义。
  • 信息丰富:好名称本身就是简洁的文档。
命名警示信号 🚩
  • 含糊的名称:过于笼统(例如,data、result、tmp、info)——不包含有用信息。
  • 难以命名:如果很难为某个实体找到准确、直观的名称,设计本身可能存在缺陷。

XII. 一致性

  • 在整个系统中,同一事物采用相同的处理方式。
  • 一致性涵盖:命名、编码风格、接口模式、设计模式、不变量。
  • 一致性能够大幅降低认知负担——开发者学会一种模式后,就可以在各处应用。
  • 不要为了“更好”而打破一致性,除非改进足够显著,并且能够彻底替换旧方式。

XIII. 代码应当清晰易懂

目标
  • 读者无需付出大量心力,就能快速理解代码的行为和意图。
  • 清晰易懂是检验设计是否优秀的终极标准。
编写清晰易懂代码的技巧
  • 使用恰当的命名并保持一致。
  • 合理运用空白和格式来展现结构。
  • 用注释解释不明显之处。
  • 避免在事件驱动代码中使用隐式控制流(除非有必要)。
代码不清晰易懂 🚩 警示信号
  • 如果读者无法轻松理解一段代码的行为或含义,那么它还不够清晰易懂。

XIV. 警示信号速查

在代码审查和自我审查中留意以下信号:

信号含义
浅层模块接口几乎与其实现一样复杂
信息泄漏同一设计决策体现在多个模块中
按时间顺序分解模块按照执行顺序划分,而非按照信息隐藏原则划分
过度暴露常用 API 迫使调用方了解很少使用的功能
透传方法方法只是将参数转发给另一个签名相似的方法
重复多个位置重复了非简单代码
通用与专用混杂通用代码和专用代码没有清晰分离
方法相互依赖要理解一个方法,就必须理解另一个方法
注释重复代码注释只是对代码的文字翻译
实现污染接口接口文档暴露了用户不需要了解的实现细节
名称含糊名称过于笼统,无法传达有用信息
难以取名找不到准确、直观的名称——设计可能存在缺陷
难以描述要完整说明就必须写很长的文档——模块可能过于复杂
代码不清晰易懂代码的行为或含义难以理解

XV. 设计原则总结

  1. 复杂性是逐步累积的——关注细节。
  2. 代码能运行还不够。
  3. 持续进行小幅改进,提升系统设计。
  4. 模块应该足够深。
  5. 接口应尽可能简化最常见的用法。
  6. 简单的接口比简单的实现更重要。
  7. 通用模块更深。
  8. 将通用代码与特殊用途代码分开。
  9. 不同层应有不同的抽象。
  10. 将复杂性下移。
  11. 消除错误(和特殊情况)的定义。
  12. 设计两遍。
  13. 注释应描述代码中不明显的内容。
  14. 软件设计应以易于阅读为目标,而非易于编写。
  15. 软件开发的增量应是抽象,而非功能。

使用指南

在以下场景中参考本指南:

  • 代码审查:使用“危险信号”速查表发现设计问题。
  • API/接口设计:力求构建深模块;确保常见用法的接口简单易用。
  • 模块分解:依据信息隐藏进行拆分,而非依据执行顺序或行数。
  • 错误处理:优先采用“消除错误的定义”,减少异常传播。
  • 重构:每次修改都投入 10–20% 的精力改进周边设计。
  • 命名和注释:名称应准确;注释应说明代码未展示的内容。

评论

支持 Markdown