软件设计的哲学:第二章 复杂性的本质

作者简介:常柱,微信公众号【架构未来】作者,十多年一线互联网研发从业经验;前五八同城商业会员技术负责人,宝驾租车技术总监,现58到家业务中台技术负责人。

这本书是关于如何设计软件系统来最小化它们的复杂性。第一步是了解敌人。到底什么是“复杂性”?你怎么知道一个系统是不是不必要的复杂?什么导致系统变得复杂?本章将在较高的层次上讨论这些问题;接下来的章节将向您展示如何在较低的层次上,根据特定的结构特征来识别复杂性。

**识别复杂性的能力是一项重要的设计技能。**它允许您在投入大量精力之前识别问题,并且允许您在备选方案中做出正确的选择。判断一个设计是否简单要比创建一个简单的设计容易,但是一旦你认识到一个系统太复杂了,你就可以用这种能力来引导你的设计哲学走向简单。如果一个设计看起来很复杂,尝试一种不同的方法,看看是否更简单。随着时间的推移,您会注意到某些技术往往会促成更简单的设计,而其他技术则与复杂性相关。这将引导你您更快地生成更简单的设计。

本章还列出了一些基本假设,为本书的其余部分奠定了基础。后面的章节采用本章的材料,并使用它来证明各种改进和结论。

2.1定义的复杂性

出于本书的目的,我以一种实际的方式定义了“复杂性”。复杂性是与软件系统的结构有关的任何东西,它使系统难于理解和修改。 复杂性可以有多种形式。例如,可能很难理解一段代码是如何工作的;实现一个小的改进可能需要很多的努力,或者可能不清楚必须修改系统的哪些部分才能实现改进;如果不引入另一个bug,可能很难修复一个bug。如果一个软件系统难以理解和修改,那么它就是复杂的;如果它容易理解和修改,那么它就是简单的。

你也可以从成本和收益的角度来考虑复杂性。在一个复杂的系统中,即使是很小的改进也需要大量的工作来实现。在一个简单的系统中,更大的改进可以用更少的努力实现。

复杂性是开发人员在试图实现特定目标时在特定时间点所经历的。它不一定与系统的总体大小或功能相关。人们经常使用“复杂”这个词来描述具有复杂功能的大型系统,但是如果这样的系统易于操作,那么,就本书的目的而言,它并不复杂。当然,几乎所有大型和复杂的软件系统实际上都很难操作,所以它们也符合我对复杂性的定义,但情况不一定如此。一个小而不复杂的系统也可能变得相当复杂。

复杂性是由最常见的活动决定的。如果一个系统有一些非常复杂的部分,但是这些部分几乎不需要被触及,那么它们对系统的整体复杂性没有太大的影响。用粗糙的数学方法来描述它:

 

 

一个系统的总体复杂度(C)是由每个部分p (cp)的复杂度决定的,而每个部分p (cp)的复杂度是由开发人员在该部分(tp)上花费的时间所占的比例决定的。将复杂性隔离在一个永远不会被看到的地方,几乎与完全消除复杂性一样好。

读者比作者更能感受到复杂性。如果你写了一段代码,你觉得它很简单,但别人却认为它很复杂。当您发现自己处于这种情况时,有必要研究一下其他开发人员,看看为什么代码对他们来说比较复杂;从你的观点和他们的观点之间的脱节中可能会学到一些有趣的教训。作为开发人员,您的工作不仅是创建可以轻松使用的代码,而且还要创建其他人也可以轻松使用的代码。

2.2复杂性的症状

复杂性一般表现在三个方面,在下文各段加以说明。每一种表现都使执行开发任务变得更加困难。

变更放大: 复杂性的第一个症状是,一个看似简单的变更需要在许多不同的地方修改代码。例如,考虑一个包含多个页面的Web站点,每个页面都显示带有背景颜色的横幅。在许多早期的Web站点中,在每个页面上都显式地指定了颜色,如图2.1(a)所示。为了更改这样一个Web站点的背景,开发人员可能必须手工修改每个现有页面;对于一个拥有数千个页面的大型站点来说,这几乎是不可能的。幸运的是,现代Web站点使用类似于图2.1(b)的方法,其中在中心位置指定了一次banner颜色,并且所有单独的页面都引用了共享的值。使用这种方法,只需一次修改就可以更改整个Web站点的横幅颜色。好的设计的目标之一是减少每个设计决策所影响的代码量,因此设计更改不需要太多的代码修改。

认知负荷: 复杂性的第二个症状是认知负荷,指的是开发人员为了完成一项任务需要知道多少。较高的认知负荷意味着开发人员必须花费更多的时间来学习所需的信息,而且由于他们错过了一些重要的内容,因此存在更大的bug风险。例如,假设C中的一个函数分配内存,返回一个指向该内存的指针,并假设调用者将释放内存。这增加了使用该函数的开发人员的认知负荷;如果开发人员未能释放内存,就会出现内存泄漏。如果系统可以重新构造,这样调用者就不必担心释放内存的问题(分配内存的同一个模块也负责释放内存),那么将减少认知负荷。认知负荷以多种方式出现,如api具有多种方法、全局变量、不一致性和模块之间的依赖性。

 

 

系统设计者有时认为复杂性可以用代码行来度量。他们认为,如果一个实现比另一个短,那么它一定更简单;如果只需要几行代码就可以进行更改,那么更改必须很容易。然而,这种观点忽略了与认知负荷相关的成本。我曾见过一些框架,它们只允许用几行代码来编写应用程序,但要弄清这些代码是什么却非常困难。有时需要更多行的代码的方法实际上更简单,因为它减少了认知负荷。

图2.1:Web站点中的每个页面都显示一个彩色的横幅。在(a)中,旗帜的背景颜色在每个页面中明确指定。在(b)中,共享变量保存背景颜色,每个页面引用该变量。在(c)部分网页显示另一种颜色以作强调,即为横额背景颜色的较暗色调;如果背景颜色改变了,强调的颜色也必须改变。

未知的未知: 复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。图2.1(c)说明了这个问题。网站使用一个中心变量来确定横幅的背景颜色,所以它看起来很容易改变。但是,一些Web页面使用较暗的背景色来强调,并且在各个页面中明确指定了较暗的颜色。如果背景颜色改变,那么强调的颜色必须改变以匹配。不幸的是,开发人员不太可能意识到这一点,所以他们可能会更改中央bannerBg变量而不更新强调颜色。即使开发人员意识到这个问题,也不清楚哪些页面使用了强调色,因此开发人员可能必须搜索Web站点中的每个页面。

在复杂性的三种表现形式中,未知的未知是最糟糕的。一个未知的未知意味着你需要知道一些事情,但是你没有办法找到它是什么,甚至是否有一个问题。你不会发现它,直到错误出现后,你做了一个改变。更改放大是令人恼火的,但是只要清楚哪些代码需要修改,一旦更改完成,系统就会工作。同样,高的认知负荷会增加改变的成本,但如果明确要阅读哪些信息,改变仍然可能是正确的。对于未知的未知,不清楚该做什么,或者提出的解决方案是否有效。唯一确定的方法是读取系统中的每一行代码,这对于任何大小的系统都是不可能的。甚至这可能还不够,因为更改可能依赖于一个从未记录的细微设计决策。

好的设计最重要的目标之一就是让系统变得显而易见。 这与高认知负荷和未知的未知相反。在一个明显的系统中,开发人员可以快速地理解现有代码是如何工作的,以及需要做哪些更改。一个明显的系统是这样的:开发人员可以快速地猜测应该做什么,而不需要非常仔细地思考,并且确信猜测是正确的。第18章讨论了使代码更明显的技术。

2.3 复杂性的原因

既然您已经了解了复杂性的高级症状,以及复杂性使软件开发变得困难的原因,那么下一步就是了解导致复杂性的原因,这样我们就可以设计系统来避免这些问题。复杂性是由两件事引起的:依赖性和模糊性。本节从较高的层次讨论这些因素;后续章节将讨论它们如何与底层设计决策相关联。

对于本书而言,当不能独立地理解和修改给定的代码段时,就会存在依赖项;代码在某种程度上与其他代码相关,如果给定代码发生更改,则必须考虑和/或修改其他代码。在图2.1(a)的Web站点示例中,背景颜色创建了所有页面之间的依赖关系。所有的页面都需要有相同的背景,所以如果一个页面的背景被改变了,那么所有的页面都必须被改变。另一个依赖的例子发生在网络协议中。通常协议有单独的发送方和接收方代码,但它们必须各自遵守协议;更改发送方的代码几乎总是需要在接收方进行相应的更改,反之亦然。方法的签名在该方法的实现和调用它的代码之间创建了一个依赖关系:如果向方法添加了一个新参数,则必须修改该方法的所有调用来指定该参数。

依赖是软件的基本组成部分,不能完全消除。事实上,我们有意将依赖关系作为软件设计过程的一部分。每次编写新类时,都要围绕该类的API创建依赖项。然而,软件设计的目标之一是减少依赖项的数量,并使依赖项尽可能简单和明显。

以Web站点为例。在每个页面上单独指定背景的旧Web站点中,所有Web页面都是相互依赖的。新的Web站点通过在中心位置指定背景颜色并提供一个API来修复这个问题,这个API用于各个页面在呈现时检索该颜色。新的Web站点消除了页面之间的依赖关系,但是围绕API创建了一个新的依赖关系来检索背景颜色。幸运的是,新的依赖关系更加明显:很明显,每个单独的Web页面都依赖于bannerBg颜色,开发人员可以通过搜索变量名轻松地找到该变量使用的所有位置。此外,编译器还有助于管理API依赖关系:如果共享变量的名称发生更改,则在仍然使用旧名称的任何代码中都会出现编译错误。新的Web站点用一个更简单、更明显的依赖项代替了一个不明显、难于管理的依赖项。

复杂性的第二个原因是晦涩。 当重要的信息不明显时,就会发生模糊。一个简单的例子是一个变量名,它是如此的通用,以至于它没有携带太多有用的信息(例如,时间)。或者,一个变量的文档可能没有指定它的单位,所以找到它的惟一方法是扫描代码,查找变量使用的位置。晦涩常常与依赖项相关,而依赖项的存在并不明显。例如,如果向系统添加了一个新的错误状态,可能需要向一个包含每个状态的字符串消息的表添加一个条目,但是对于查看状态声明的程序员来说,消息表的存在可能并不明显。不一致性也是造成不透明性的一个主要原因:如果同一个变量名用于两个不同的目的,那么开发人员就无法清楚地知道特定变量的作用。

在许多情况下,含糊不清是因为文档不充分;第13章讨论这个主题。然而,晦涩也是一个设计问题。如果一个系统有一个清晰而明显的设计,那么它将需要更少的文档。需要大量的文档通常是设计不太正确的一个危险信号。减少模糊的最佳方法是简化系统设计。

相关性和模糊性共同解释了2.2节中描述的复杂性的三种表现形式。依赖会导致变化放大和高认知负荷。晦涩创造了未知的未知,也增加了认知负荷。如果我们能找到最小化依赖和模糊的设计技术,那么我们就能降低软件的复杂性。

2.4 复杂性是递增的

复杂性不是由单个灾难性错误造成的,而是由许多小问题积累而成。 单个依赖项或晦涩本身不太可能对软件系统的可维护性产生重大影响。复杂性的产生是因为成百上千的小的依赖和模糊随着时间的推移而累积。最终,这些小问题如此之多,以至于对系统的每一个可能的更改都会受到其中几个问题的影响。

复杂性的递增性使其难以控制。 很容易让自己相信,您当前的更改所引入的一点复杂性并不是什么大问题。但是,如果每个开发人员对每个更改都采用这种方法,那么复杂性会迅速增加。一旦复杂性积累起来,就很难消除,因为修复单个依赖项或晦涩本身不会产生很大的影响。为了减缓复杂性的增长,您必须采用“零容忍”的哲学,如第3章所述。

2.5 结论

复杂性来自于依赖和模糊的积累。随着复杂性的增加,它会导致变化的扩大、高的认知负荷和未知的未知。 因此,需要进行更多的代码修改来实现每个新特性。此外,开发人员花费更多的时间来获取足够的信息以安全地进行更改,在最坏的情况下,他们甚至无法找到所需的所有信息。底线是,复杂性使得修改现有代码库变得困难和危险。


免责声明:本文翻译仅供学习使用,本文的版权归英文原作者或出版方,若有侵权,请联系删除。

posted @ 2019-12-16 15:05  peida  阅读(1986)  评论(0编辑  收藏  举报