能工作的代码,真的不够
读书笔记 ·《A Philosophy of Software Design》前言、介绍、第一、二章
为什么读这本书
工作不到一年,我一直保持着写博客的习惯。写博客逼着我不断搜集议题、验证方法论,用来复盘自己的工作。
在大量观点里,我发现用"复杂度"和"熵增"来指导软件设计这个视角特别好用,也特别符合我的开发风格。顺藤摸瓜,找到了这本书。
书本身篇幅不长,读起来也快,适合工作中碎片化时间、系统化学习的节奏。
这本书在说什么
核心主张可以用一句话概括:软件设计是贯穿整个软件生命周期的持续过程,复杂度随软件演化必然缓慢累积,而更好的设计能让你在相同的复杂度预算下,构建出更强大、更庞大的系统。
这个框架的起点是承认一件事:复杂度的增加是必然的,不是可以消灭的,只能被管理。
复杂度是什么,怎么识别
书里对复杂度的定义方式有点特别——它不是因果式的定义,不是"因为做了 XX 所以产生了复杂度",而是从症状入手。
为什么这么定义?因为产生复杂度的原因可以说群魔乱舞,但复杂度的症状十分统一,可以归为三类:
认知负担:理解成本增加,读代码要花更多时间才能搞清楚在干什么。
未知的未知:存在隐式的上下文,你不知道你不知道什么,改了这里不知道会影响那里。
变更放大:改一个地方要动很多地方,所有人力时间成本的增加都可以算在这里。
这种从症状定义的方式,背后是一种"没有银弹"的思想。开发中很容易有路径依赖,比如"上次用微服务拆分降低了复杂度,这次也这么用",虽然导致客观上复杂度增加了,但因为经验主义,反而得出了复杂度降低的错误结论。
但从客观症状切入,就堵死了这种自我欺骗的空间:不管用了什么技术、什么方案,只要症状加重,就是复杂度增加,无法狡辩。
复杂度可以量化
书里给出了一个公式:
C = \sum_p c_p \cdot t_p
每个模块的复杂度,乘以软件演化过程中在这个模块上投入的时间,加权求和。
举个简单例子。Spring Framework 本身的复杂度极高,但在开发一个 Spring 服务的过程中,你几乎不需要在框架模块上投入任何时间——框架的复杂度被封装起来了,对你来说时间成本的权重接近于零。所以 Spring 服务整体的复杂度反而很低。
这说明降低复杂度有两条路:要么降低模块本身的复杂度(治理那三个症状),要么通过封装把高复杂度的部分隔离出去,让它不再需要投入时间。
复杂度从哪里来
症状是可观测的,但根因主要集中在两个地方:依赖和模糊。
依赖是现代软件不可或缺的一部分——API、协议、交互约定、接口规范,到处都是依赖。但好的软件设计的目标之一,是让这些依赖尽可能简化,尽可能"一目了然"。
不合理的依赖设计会给开发者带来“认知负担”,并导致“变更放大”,从而带来复杂度的提升。
“模糊”更不用说了,认知负担,未知的未知,软件系统中,任何需要开发者投入额外成本理解,或者隐藏的影响,都会增加软件系统的复杂度。
一目了然(Obvious)是良好软件设计的核心指标之一。一个系统是否足够 Obvious,是判断它设计好坏的重要标准。
能工作的代码,真的不够
第二章的标题就是这本书最核心的立场之一。
书里把编程风格分成两种:战术式编程和战略式编程。
战术式编程的定义很简单:只交付功能正常、可以运行的代码。目标是让这个功能跑起来,其他的不管。
战略式编程的要求更高:在功能实现之后,额外投入功能实现时间的 10%~20%,去优化和梳理现有的实现,主动降低复杂度。
作者推崇战略式编程的一个核心理由是:面对复杂度已经大量积累的存量系统,主动发起成本高昂的大重构是极其困难的。所以与其等到积重难返,不如在演化过程中持续投资。
这种投资分两种形式。主动投资是主动优化设计;被动投资是在解决问题的时候,花更多时间进行更深入的思考,而不是找到一个能跑的方案就收手。
战略式编程的受益者,首先是未来的自己。大概是三个月到半年后,当你自己忘了当时写代码在想什么的时候。也差不多是你当时的投资开始回报你的时候。
所以如果你觉得没必要给未来的自己减少时间(出于职业选择或者生活安排等考虑),那投不投资是你自己的选择。
后记
前两章建立了这本书的基础框架:复杂度是什么、怎么量化、从哪里来、为什么要主动管理它。后面的章节会进入更具体的设计原则——怎么设计模块、怎么处理接口、怎么写注释……但所有这些,都是在回答同一个问题:怎么在软件演化的过程中,让复杂度的增长慢一点,再慢一点。