重读〈软件设计的哲学〉:复杂度才是软件设计的敌人
钢镚 · 2026-07-31
《软件设计的哲学》只讨论一件事:如何降低复杂度。 作者 John Ousterhout 是斯坦福大学教授,参与过 Tcl、Tk、Sprite 和 RAMCloud 等项目。他发现,很多工程师会写代码、会测试、会重构,却说不清什么是好设计。 他的判断标准很简单: 如果一个设计让系统更容易理解和修改,它就是好设计。 01 复杂度是什么 复杂度不是代码行数,而是 人理解和修改代码时付出的成本 。 它通常表现为三种症状: 变化放大。 一个简单需求要修改很多文件。 认知负荷。 改一处代码前,必须理解大量上下文。 未知的未知。 你不知道哪些地方会受到影响。 第三种最危险。前两种至少知道问题在哪里,未知的未知意味着风险根本看不见。 复杂度主要来自两个根源:依赖和晦涩。 依赖是修改一段代码时,必须同时考虑其他代码。 晦涩是重要信息没有被清楚表达,例如命名模糊、约定隐蔽、设计决策分散。 02 好模块应该足够深 好模块的标准不是小,而是接口简单、功能强大。 Unix 文件系统就是典型的深模块。调用者只需要理解 open、read、write、close,背后复杂的缓存、权限和磁盘管理都被隐藏了。 浅模块恰好相反。接口很多,却没有隐藏多少复杂度。 很多项目中的 Controller、Service、DAO 层层转发,就是典型例子。每一层都只有一行代码,没有产生新的抽象,只增加了跳转成本。 所以,拆分模块不是目的。 真正的问题是:拆分后,调用者需要理解的东西变少了吗? 03 信息隐藏比 private 更重要 信息隐藏不是把字段设为 private,而是 把设计决策封装在模块内部 。 如果两个模块必须同时理解文件格式、业务规则或状态变化,它们即使物理上分开,也依然存在隐性耦合。 一个常见错误是按执行顺序拆模块。例如,把读取文件和解析文件分别放进两个类,但两个类都要理解文件格式。格式一改,两边都要修改。 好的边界应该按照知识划分。 谁需要知道同一个设计决策,谁就更可能属于同一个模块。 04 战略编程胜过战术编程 战术编程只关心功能能不能尽快上线。 战略编程还会考虑,这次实现会不会让未来更难修改。 技术债通常不是一次错误决定造成的,而是无数次“先这样,以后再改”叠加的结果。 作者建议把约 10% 到 20% 的开发时间用于设计。 不是提前写完整架构,而是在每次修改时多想一步:接口是否合理,复杂度是否扩散,是否存在更简单的方案。 05 五个值得长期坚持的习惯 第一,把复杂度往下拉。 能由模块内部统一处理的细节,不要让每个调用者重复承担。 第二,消除特殊情况。 比起到处写异常处理,更好的办法是重新设计接口,让部分异常不再存在。 第三,先写接口注释。 注释不是解释代码做了什么,而是说明为什么这样设计。 第四,设计两次。 重要决策至少比较两个方案,第一个想到的方案通常不是最好的。 第五,保持一致性。 相同问题使用相同命名、结构和处理方式,让读者依靠模式理解代码。 06 AI 时代更需要设计判断 AI 可以快速生成能运行的代码,却未必能判断模块边界是否合理。 它可能制造更多类、更多接口、更多特殊分支,也可能把同一条业务规则复制到多个文件。 因此,代码生成能力越强,工程师越需要判断: 这个模块隐藏了多少复杂度? 这个接口是否比功能本身还复杂? 这次修改会让系统更容易理解,还是更难理解? 未来,代码会越来越便宜。 真正昂贵的,是清晰的边界、稳定的抽象,以及对复杂度的判断力。