Philosophy of Software Design 第二章 复杂度的天性

这本书是关于如何设计软件系统使得它们的复杂度最小化。第一步是了解敌人。到底什么是“复杂性”?你怎么知道一个系统是不必要的复杂?什么会导致系统变复杂?这章会在较高层级解决这些问题;余下的章节会就具体结构特征向你展示如何在较低层级识别复杂度。

识别复杂性是设计技能中一项关键的能力。它使得你可以在投入大量精力前发现问题,并且在许多可选项中做出好选择。分辨出一个设计是否简洁要比创造一个简洁的设计容易,但是一旦你可以识别出一个系统过于复杂,你就可以使用这个能力来指导你的设计哲学向着简单性出发。如果一个设计看上去很复杂,尝试一种不同的方式然后看看是否变简单了。随着时间流逝,你会注意到一些特定的技术会得到更简单的设计,另外一些则和复杂性相关。这将使得你能更快地产出更简洁的设计。

本章也会给出一些基本假设,这些基本假设为本书的其余部分奠定了基础。后面的章节会使用本章的材料来评判各种不同的改善方法和结论。

2.1 定义复杂性

为了这本书的目的,我以一种实践方式定义“复杂性”。复杂性是和软件系统结构相关的、使得理解和修改系统更困难的任何东西。 复杂性可能会以很多形式出现。比如,可能难以理解一段代码是如何运行的;可能一点小的改进需要花费很大精力,或者为了做出改进需要修改系统的哪些部分难以确定;可能很难在不引入其他问题的情况下修复一个 bug。如果很难理解和修改一个软件系统,那么它就是复杂的;如果很容易理解和修改,那它就是简单的。

也可以从成本和收益的角度考虑复杂性。在复杂系统中,哪怕是实现一个很小的改进都需要花费很多工作。在简单系统中,可以用更少的工作完成更大的改进。

复杂性是开发者在尝试完成一个特定目标时在特定时间点的经历。并不一定和系统整体的大小或功能相关。人们经常用“复杂(complex)”一词来描述有着复杂(sophisticated)功能的大型系统,但是如果这样的系统开发很容易上手的话,那么,从本书的角度来看,并不复杂(complex)。当然,几乎所有大型和复杂(sophisticated)的软件系统实际上也难以开发,所以它们也符合我这里复杂的定义。但是这并不一定是事实。小型并不复杂(unsophisticated)的系统也有可能非常复杂(complex)。1

复杂性由最常见的活动决定。如果系统中包括一些非常复杂的部分,但是这些基本不会用到,那它们对系统整体的复杂性几乎没影响。用粗略的数学方式来表示:

$$C = ∑c_pt_p$$

系统整体复杂性(C)由每部分的复杂性 ($c_p$)加权开发者在这部分花费的时间($t_p$)决定。把复杂性隔离在一个从不会被看到的地方基本就和完全消除了复杂性一样。

相较于编写者,复杂性对阅读者来说更明显。如果你编写了一段对你来说似乎很简单的代码,但是其他人认为它复杂,那么它就是复杂的。当你发现你处于这样的境况中,请其他开发者来看一下为什么他们觉得代码很复杂就很值得一试;在你和他们不同的观点中很有可能学到有趣的东西。作为开发者的工作不仅仅是写出你觉得容易的代码,也要让其他人觉得容易。

2.2 复杂性的征兆

复杂性通过三种一般的方式表现出来,下面的段落将会描述到。每一种都会使得完成研发任务变得困难。

figure 2.1

图2.1: 网站的每个页面都会展示有颜色的横幅。(a) 中横幅的背景色是在每个页面中显示指定的。(b) 中共享变量保存背景色的值,每个页面都指向这个变量。(c) 中一些页面会展示额外的颜色表示强调,这个颜色是背景色带阴影的较暗的转换;如果背景色变化了,强调色也必须跟着变化。

放大改变: 复杂性的第一个征兆是,一个看上去简单的改变需要在许多不同的地方修改代码。比如,考虑有着不同页面的网站,每一个页面都会展示有着背景色的横幅。在很多早期网站中,颜色是由每个页面显示地指定的,如图 2.1(a)。为了改变这个网站的背景色,开发者可能需要手动地修改每个已有页面;对于有着上千张页面的大型网站来说这几乎是不可能的。幸运的是,现代的网站使用了图 2.1(b) 中的方式,横幅颜色在一个集中的地方一次性指定,所有页面都指向这个共享的值。使用这种方式时,整个网站的横幅颜色变化可以由单个修改完成。好的设计的目标之一是,减少每个设计决定会影响到的代码数量,这样修改设计时不需要改动很多代码。

心智负担: 复杂性第二个征兆是心智负担,心智负担是指开发者为了完成任务需要了解多少东西。更高的心智负担意味着开发者不得不花费更多时间学习必需了解的信息,而且可能会由于他们遗漏了一些重要信息而增加出现 bug 的风险。比如,C 中分配内存的函数返回指向内存的指针,并且假设调用者会释放内存。这增加了使用这个函数的开发者的心智负担;如果开发者没能释放内存,那么就会出现内存泄漏。如果可以重构系统使得开发者无需关心内存释放(分配内存的模块同时负责释放它),就可以降低心智负担。心智负担以多种方式出现,比如有许多方法的 API,全局变量,不一致性,模块间依赖。

系统设计者有时候认为可以用代码行数衡量复杂性。他们认为如果一个实现比另外的短,那么肯定就更简单;如果只需要很少几行就可以做出改变,那这个改变肯定很容易。然而,这个视角忽略了和心智负担有关的成本。我曾经见过只需要几行就可以完成一个应用的框架,但是想要搞懂这几行做了什么事情却及其困难。有时候需要更多行代码的方式其实更简单,因为它降低了心智负担。

未知的未知: 复杂性的第三个征兆是,为了完成任务需要修改哪些代码或者开发者需要了解哪些信息并不明显。图 2.1(c) 展示了这个问题。网站使用集中变量来决定横幅背景色,所以看上去很容易修改。然而,一些网页使用了带阴影加深的背景色来表示强调,而这个加深的颜色是在每个页面单独指定的。如果背景色变化了,那强调色也必须相匹配地跟着变化。不幸的是,开发者不太可能意识到这个颜色的存在,所以他们可能修改了集中变量 bannerBg 但是没有更新强调色。即使开发者意识到了这个问题,哪些页面使用了强调色也不明显,所以开发者不得不搜索网站中的所有页面。

复杂性的三种表现方式中,未知的未知是最糟糕的。未知的未知意味着你需要了解一些东西,但是你却无法知道需要了解什么,或者甚至这些东西是否存在。在你修改后,直到出现 bug 之前都无法知道。改变放大很烦人,但是只要知道需要修改哪些代码,一旦完成修改系统还是可以工作。类似的,高的心智负担会增加做出改变的成本,但是如果知道哪些信息需要掌握,改变仍旧很可能是正确的。而未知的未知的情况,你将不知道需要做什么,或者提议的方案是否可以工作。唯一可以确定的方式,是阅读系统的每一行代码,这对任何规模的系统都是不切实际的。即使这样也还不够,因为修改可能依赖于一个从未记录的微小的设计决定。

对于系统来说,好设计的最重要的目标之一是使系统变得浅显易懂。这是高心智负担和未知的未知的对立面。在浅显易懂的系统中,开发者可以快速了解已有代码是如何工作的以及做出修改需要什么。浅显易懂的系统中,开发者可以快速的猜出要做什么,不需要非常认真地思考,而且还可以对猜测很有信心。第十八章讨论了使得代码更浅显易懂的技术。

2.3 复杂性的原因

既然你已经知道了复杂性的高层级的征兆,还有为什么复杂性会使得软件开发变困难,下一步就是搞懂什么导致了复杂性,这样我们就可以在设计系统时避免这些问题。复杂性是由两种东西导致的:依赖和模糊。这部分从高层次讨论了这些因素;余下的章节会讨论它们如何与低层次的设计决定相关。

从本书的目的来说,当一段代码无法单独被理解和修改时,就说明存在依赖;这段代码和其他代码以某种方式相关,如果这段代码修改了,也必须考虑和/或修改其他代码。在图 2.1(a) 的网站例子中,背景色创建了所有页面之间的依赖关系。所有页面必须有相同的背景,所以如果一个页面的背景修改了,那么所有其他的页面也必须修改。依赖的另外一个例子出现在网络协议中。一般来说,协议的发送方和接收方代码是独立的,但是它们都必须遵循这个协议;修改发送方的代码几乎总是要求接收方也做出相应修改,反过来也一样。方法的签名创建了这个方法的实现和调用者之间的依赖关系:如果这个方法新增了一个参数,这个方法的所有调用者都必须加以修改来指定这个参数。

依赖是软件中一个基础部分,不能被完全消除。实际上,在软件设计过程中我们会故意引入依赖。你每次新增类的时候,都会创建围绕这个类的 API 的依赖。然而,软件设计的目标之一是减少依赖,并且尽量保持依赖简单和明显。

考虑网站的例子。在每个页面中单独指定背景色的老网站中,所有的网页互相依赖。新网站通过在一个集中的地方指定背景色并提供 API 使得每个页面渲染时可以获取到颜色解决了这个问题。新网站消除了页面之间的依赖,但是创建了围绕获取背景色的 API 的新依赖。幸运的是,新依赖更明显了:每个网页依赖于 bannerBg 是很明显的,开发者可以很容易的找到所有使用这个变量的地方。更进一步的,编译器可以帮助管理 API 依赖:如果共享变量的名字变了,仍然使用原来名字的代码会报编译错误。新的网站用一个更简单更明显的依赖替换了原来不明显、难以管理的依赖。

复杂性的第二个原因是模糊。当重要信息不明显时,模糊就出现了。一个简单的例子是,变量名过于简单,无法携带很多有用信息(比如 时间)。或者,变量的文档可能没有它的单位,所以只能到使用这个变量的地方查看。当依赖的存在不明显时,模糊经常和依赖相关。比如,如果系统中要新增一个错误状态码,可能也需要在存储了每个状态码的文字消息的表中新增一条记录,但是对于看状态定义的程序员来说,消息表的存在可能不够明显。不一致也是模糊的一个重要来源:如果相同的变量名用于两种不同目的,对开发者来说,一个具体的变量服务于哪个目的就不明显了。

在许多情况中,模糊是由于文档不足;第十三章会处理这个主题。然而,模糊也是一个设计问题。如果系统有着干净明显的设计,那么就会需要更少的文档。需要大量文档经常是一个红色警告,说明设计不太对。减少模糊最好的方式是简化系统设计。

总体来说,依赖和模糊要为 2.2 节中描述的复杂性的三种表现方式负责。依赖导致改变放大和高心智负担。模糊创造了未知的未知,同时对心智负担也有贡献。如果我们能找到最小化依赖和模糊的设计技术,那么我们就可以降低系统复杂度。

2.4 复杂性是增量的

复杂性不是由单个灾难性的错误导致的;它由许多小块积累而成。单个依赖或模糊很难对软件系统的维护造成明显的影响。随着时间而累积的成百上千的小依赖和模糊导致了复杂性。最终,太多的小问题存在以至于系统每个可能的修改都会被这些问题中的几个所影响。

复杂性增量的天性使得它很难控制。很容易说服自己当前改动引入一点复杂性没什么大不了。然而,如果每个开发者每个改动都采用这种方式,复杂度会迅速累积。一旦复杂度累积到一定程度,就很难消除,因为修复单个依赖或模糊不会有什么大的作为。为了减缓复杂性的增速,必须采取“零容忍”的哲学,在第三章中会讨论到。

2.5 结论

复杂性由依赖和模糊积累而来。随着复杂性增长,会导致改变放大,高心智负担和未知的未知。最终,每个新功能都需要更多代码修改实现。而且,开发者要花费更多时间获取安全变更的足够的信息,在最糟的情况中,他们甚至无法找到所需的所有信息。结果是,复杂性使得修改已有的代码变得困难而且冒险。


  1. 我已当场去世,sophisticated 和 complex 完全不知道怎么区分。。 ↩︎