
奥斯特豪特的软件哲学:驯服复杂性
张天枭
2
8-27苏哲: 我觉得写软件最让人头疼的一件事,就是项目越做越大,代码就变得越来越乱,像一团缠在一起的耳机线,想改动一个地方,结果牵一发而动全身。
晓曼: 哈哈,这个比喻太形象了。其实这背后就是软件工程里那个永恒的敌人:复杂性。斯坦福的著名教授约翰·奥斯特豪特,在他那本软件设计的哲学里就开宗明义地讲,软件设计的核心目标,有且只有一个,就是对抗复杂性。
苏哲: 对,他把复杂性具体化成了三个东西:变更放大、认知负荷,还有“未知的未知”。简单说就是,改一处代码,结果要动十个地方;为了搞懂一段逻辑,脑子快烧了;以及,你根本不知道你的修改会捅出什么篓子。
晓曼: 嗯,而这些问题的根源,说白了就是两点:代码模块之间乱七八糟的依赖,和信息表达得不清不楚,也就是他说的“依赖性”和“模糊性”。
苏哲: 那奥斯特豪特提出的“深度模块”和“浅模块”这两个概念,又是怎么来解决这个问题的呢?
晓曼: 这个概念特别精彩。你可以把“深度模块”想象成一个包装精美的礼盒。它的接口,也就是盒子外面,看起来非常简洁,可能就一个漂亮的蝴蝶结。但盒子里面,功能却异常强大。它通过信息隐藏,把所有复杂的实现细节,比如里面是怎么折叠、怎么固定的,全都藏起来了。你作为使用者,只需要拉一下那个蝴蝶结,就能得到想要的结果。
苏哲: 我明白了,就是接口简单,但功能强大。为了实现这种“深度模块”,他特别强调了“信息隐藏”这个原则。就是说,每个模块应该把自己的设计决策和实现细节都封装好,别让外面的人知道。他还提到了一个很有趣的词,叫“类炎”综合症。
晓曼: 没错,这个“类炎”就是吐槽那种盲目追求“小类”和“短方法”的风气。很多人觉得类越小越好,方法越短越好,结果制造出一大堆接口本身就很复杂的“浅模块”。最后整个系统里全是这种小而浅的类,互相调用,反而让整体的复杂性爆炸了。他直接反驳了那个误区:如果一个模块的接口本身很复杂,那它就是“浅”的,不管它内部实现有多简单,都会增加维护成本。
苏哲: 所以他提倡一种叫“战略性编程”的思维,而不是那种只顾眼前,让代码先跑起来就行的“战术性编程”。他甚至建议,开发者应该把百分之十到二十的时间,专门用在设计改进和重构上。
晓曼: 是的,这是一种投资心态。其中“定义不存在的错误”这个原则就特别有意思。举个例子,你去删除一个根本不存在的变量,大多数系统会给你报个错。但他的理念是,这种情况应该静默成功。因为你的目的——“确保这个变量不存在”——已经达到了。这种设计能让代码的行为变得极其简单和可预测。
苏哲: 在这些实践原则里,你觉得哪一个对我们日常开发影响最大,或者说最容易被我们忽略?
晓曼: 我觉得是“先写注释”和“选择好名字”。这两点听起来像是编程入门第一课讲的东西,特别基础,但恰恰是最容易被当成“有空再说”的杂活。很多开发者觉得,功能实现了最重要,注释和命名是后面的事。但奥斯特豪特的观点是,它们本身就是设计过程的一部分。你在写代码之前,先用清晰的语言把这个模块的接口、功能、参数写成注释,这个过程会逼着你提前思考设计上的问题。一个好名字,更是能瞬间降低别人的理解成本。这其实是一种成本极低的“投资”。
苏哲: 没错,这些原则听起来简单,但真正做到并坚持下去,就能显著降低软件的复杂性,提升开发效率和代码质量。那在微观实践层面,有哪些具体的例子能帮我们更好地理解这些原则呢?
晓曼: 当然,微观操作上的例子更能说明问题。
苏哲: 好,那我们具体来看。比如在设计方法和类的时候,奥斯特豪特说要避免设计那些浅层的方法。他举了一个文本编辑器的例子,说与其提供一个专门处理“退格键”的方法,再来一个处理“删除键”的方法……
晓曼: 不如直接提供一个更通用的方法,比如 `insert(Position, String)` 负责插入,和 `delete(Position, Position)` 负责删除。这样一来,接口数量大大减少,而且功能更强大,也降低了不同模块间的依赖。这才是“深度”的体现。
苏哲: 还有信息隐藏,他提到处理HTTP请求的例子。不应该把所有解析出来的参数一股脑地用一个Map丢给用户。
晓曼: 对,正确的做法是把所有读取和解析的逻辑都封装在一个类里,隐藏掉所有底层的细节,然后只提供一个像 `getParameter(String name)` 这样的清爽接口。这样,使用者用起来非常简单,而且你内部将来想换一种解析方式,外面完全不受影响。这就是典型的“将相关代码放在一起”和“隐藏实现细节”。
苏哲: 刚刚提到的“定义不存在的错误”,他在异常处理上也有实践。比如那个Tcl语言的 `unset` 命令,你让它去删除一个不存在的变量,它不会报错,而是静默成功。
晓曼: 这个设计哲学真的很高明。它的核心思想是,与其抛出一堆异常让调用者去头疼地处理,不如从设计上就让这些异常情况根本不发生,或者在底层就被“掩盖”掉了。这能极大地降低上层业务逻辑的复杂度和维护成本。
苏哲: 另外,他还特别强调了命名。比如变量名,要避免用 `x`、`y` 这种模糊的名称。
晓曼: 是的,必须用像 `charIndex`(字符索引)、`cursorVisible`(光标是否可见)这样能准确传达信息的名称。一个好的名字本身就像一份微型文档,别人读你的代码时,理解成本会直线下降,也能避免很多低级错误。
苏哲: 聊到这,我发现这些原则不仅适用于大的架构设计,也完全能用在一些具体的开发小事上,比如日志和配置。
晓曼: 完全正确。就拿日志来说,如果每个模块都自己写一套日志记录的代码,那简直是灾难。代码到处重复,而且每个模块可能还会不小心泄露一些不该被记录的内部信息。
苏哲: 这就是他说的“重复”和“信息泄漏”。所以按照他的原则,应该把所有日志逻辑都抽出来,做一个独立的、集中的日志模块。
晓曼: 对,就提供一个统一的接口,比如 `Logger.log(Level, String)`。所有的业务模块都只跟这个接口打交道,完全不用关心日志到底是怎么写的、写到哪里的。这样一来,通用的日志功能和具体的业务逻辑就分开了,日志模块本身也变得更“深度”了。
苏哲: 配置参数也是个很好的例子。我见过很多网络服务,恨不得把TCP协议的所有参数都暴露给用户去配置,什么超时时间、重试次数、缓冲区大小……
晓曼: 这就是典型的增加了用户的认知负荷。用户为了用你的模块,还得先去学习一大堆网络知识,配置起来非常痛苦,还容易出错。奥斯特豪特就建议,模块应该自己根据“通常情况”做出最合理的默认选择。
苏哲: 比如网络服务模块完全可以自己根据当前的网络状况,动态地去调整超时和重试次数,根本不需要用户来操心。
晓曼: 正是如此。这背后体现的就是“简单的接口”和“定义不存在的错误”的思想。通过智能的默认值和动态调整,让用户根本没有机会犯错,这极大地提高了系统的鲁棒性,也让接口变得干净利落。
苏哲: 好了,聊了这么多,感觉收获非常大。晓曼,如果让你来总结一下今天我们聊的这些核心思想,你会怎么概括?
晓曼: 嗯,我觉得可以浓缩成几个点。首先,最核心的,软件设计的终极目标就是管理复杂性,而复杂性的根源就是依赖和模糊。其次,我们追求的武器,是那种接口简单但功能强大的“深度模块”,它善于隐藏内部实现。第三,要有一种“战略性编程”的心态,舍得花10%到20%的时间去做设计和重构,这是一种投资。最后,在具体的实践中,无论是“先写注释”、“起好名字”这种基础功,还是“定义不存在的错误”、“提供通用接口”这种设计技巧,以及像日志集中化、配置最小化这些细节,所有这些做法,最终都指向同一个目标:化繁为简,让我们的软件更清晰、更健壮、也更容易维护。