
重读〈软件设计的哲学〉:复杂度才是软件设计的敌人
《软件设计的哲学》只讨论一件事:如何降低复杂度。
作者 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 可以快速生成能运行的代码,却未必能判断模块边界是否合理。
它可能制造更多类、更多接口、更多特殊分支,也可能把同一条业务规则复制到多个文件。
因此,代码生成能力越强,工程师越需要判断:
- 这个模块隐藏了多少复杂度?
- 这个接口是否比功能本身还复杂?
- 这次修改会让系统更容易理解,还是更难理解?
未来,代码会越来越便宜。
真正昂贵的,是清晰的边界、稳定的抽象,以及对复杂度的判断力。
评论 (0)
还没有评论,来发第一条吧