Files
A-Philosophy-of-Software-De…/docs/ch2.md
2020-07-03 00:14:54 +08:00

66 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 第二章:复杂性的本质
这本书是关于如何设计软件系统以最小化其复杂性。第一步是了解敌人。究竟什么是“复杂性”?您如何判断系统是否过于复杂?是什么导致系统变得复杂?本章将在较高层次上解决这些问题。后续章节将向您展示如何从较低的层次上根据特定的结构特征来识别复杂性。
识别复杂性的能力是至关重要的设计技能。它使您可以先找出问题,然后再付出大量努力,并可以在其他选择中做出正确的选择。判断一个设计是否简单比创建一个简单的设计要容易得多,但是一旦您认识到一个系统过于复杂,就可以使用该功能指导您的设计哲学走向简单。如果设计看起来很复杂,请尝试其他方法,看看是否更简单。随着时间的流逝,您会注意到某些技术往往会导致设计更简单,而其他技术则与复杂性相关。这将使您更快地制作更简单的设计。
本章还列出了一些基本假设,这些基本假设为本书的其余部分奠定了基础。后面的章节将采用本章的内容,并用其论证各种改进和结论。
## 2.1 复杂性的定义
出于本书的目的,我以实用的方式定义“复杂性”。复杂性与软件系统的结构有关,这使它很难理解和修改系统。复杂性可以采取多种形式。例如,可能很难理解一段代码是如何工作的。可能需要花费很多精力才能实现较小的改进,或者可能不清楚必须修改系统的哪些部分才能进行改进;如果不引入其他错误,可能很难修复。如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。
您还可以考虑成本和收益方面的复杂性。在复杂的系统中,要实施甚至很小的改进都需要大量的工作。在一个简单的系统中,可以用更少的精力实现更大的改进。
复杂性是开发人员在尝试实现特定目标时在特定时间点所经历的。它不一定与系统的整体大小或功能有关。人们通常使用“复杂”一词来描述具有复杂功能的大型系统,但是如果这样的系统易于使用,那么就本书而言,它并不复杂。当然,实际上几乎所有大型复杂的软件系统都很难使用,因此它们也符合我对复杂性的定义,但这不一定是事实。小型而不复杂的系统也可能非常复杂。
复杂性取决于最常见的活动。如果系统中有一些非常复杂的部分,但是几乎不需要触摸这些部分,那么它们对系统的整体复杂性不会有太大影响。为了用粗略的数学方法来表征:
![NL672F.png](https://s1.ax1x.com/2020/07/03/NL672F.png)
系统的总体复杂度C由每个部分的复杂度cp乘以开发人员在该部分上花费的时间tp加权。在一个永远不会被看到的地方隔离复杂性几乎和完全消除复杂性一样好。
读者比作家更容易理解复杂性。如果您编写了一段代码,对您来说似乎很简单,但是其他人则认为它很复杂,那么它就是复杂的。当您遇到这种情况时,有必要对其他开发人员进行调查,以找出为什么代码对他们而言似乎很复杂;从您的观点与观点之间的脱节中可能可以学习一些有趣的课程。作为开发人员,您的工作不仅是创建可以轻松使用的代码,而且还要创建其他人也可以轻松使用的代码
## 2.2 复杂性的症状
复杂性通过以下三种段落中描述的三种一般方式体现出来。这些表现形式中的每一个都使执行开发任务变得更加困难。
**变更放大:**复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。例如,考虑一个包含几个页面的网站,每个页面显示带有背景色的横幅。在许多早期的网站中,颜色是在每个页面上明确指定的,如图 2.1a所示。为了更改此类网站的背景开发人员可能必须手动修改每个现有页面对于拥有数千个页面的大型网站而言这几乎是不可能的。幸运的是现代网站使用的方法类似于图 2.1b其中横幅颜色一次在中心位置指定并且所有各个页面均引用该共享值。使用这种方法可以通过一次修改来更改整个网站的标题颜色。
**认知负荷:**复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。较高的认知负担意味着开发人员必须花更多的时间来学习所需的信息,并且由于错过了重要的东西而导致错误的风险也更大。例如,假设 C 中的一个函数分配了内存,返回了指向该内存的指针,并假定调用者将释放该内存。这增加了使用该功能的开发人员的认知负担。如果开发人员无法释放内存,则会发生内存泄漏。如果可以对系统进行重组,以使调用者不必担心释放内存(分配内存的同一模块也负责释放内存),它将减少认知负担。
系统设计人员有时会假设可以通过代码行来衡量复杂性。他们认为,如果一个实现比另一个实现短,那么它必须更简单;如果只需要几行代码就可以进行更改,那么更改必须很容易。但是,这种观点忽略了与认知负荷相关的成本。我已经看到了仅允许使用几行代码编写应用程序的框架,但是要弄清楚这些行是什么极其困难。有时,需要更多代码行的方法实际上更简单,因为它减少了认知负担。
![NLcSPK.png](https://s1.ax1x.com/2020/07/03/NLcSPK.png)
> 图 2.1网站中的每个页面都显示一个彩色横幅。在a横幅的背景色在每页中都明确指定。在b共享变量保留背景色并且每个页面都引用该变量。在c某些页面会显示其他用于强调的颜色即横幅背景颜色的暗色如果背景颜色改变则强调颜色也必须改变。
**未知的未知:** 复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。图 2.1(c)说明了这个问题。网站使用一个中心变量来确定横幅的背景颜色,所以它看起来很容易改变。但是,一些 Web 页面使用较暗的背景色来强调,并且在各个页面中明确指定了较暗的颜色。如果背景颜色改变,那么强调的颜色必须改变以匹配。不幸的是,开发人员不太可能意识到这一点,所以他们可能会更改中央 bannerBg 变量而不更新强调颜色。即使开发人员意识到这个问题,也不清楚哪些页面使用了强调色,因此开发人员可能必须搜索 Web 站点中的每个页面。
在复杂性的三种表现形式中,未知的未知是最糟糕的。一个未知的未知意味着你需要知道一些事情,但是你没有办法找到它是什么,甚至是否有一个问题。你不会发现它,直到错误出现后,你做了一个改变。更改放大是令人恼火的,但是只要清楚哪些代码需要修改,一旦更改完成,系统就会工作。同样,高的认知负荷会增加改变的成本,但如果明确要阅读哪些信息,改变仍然可能是正确的。对于未知的未知,不清楚该做什么,或者提出的解决方案是否有效。唯一确定的方法是读取系统中的每一行代码,这对于任何大小的系统都是不可能的。甚至这可能还不够,因为更改可能依赖于一个从未记录的细微设计决策。
良好设计的最重要目标之一就是使系统显而易见。这与高认知负荷和未知未知数相反。在一个显而易见的系统中,开发人员可以快速了解现有代码的工作方式以及进行更改所需的内容。一个显而易见的系统是,开发人员可以在不费力地思考的情况下快速猜测要做什么,同时又可以确信该猜测是正确的。第 18 章讨论使代码更明显的技术。
## 2.3 复杂性的原因
既然您已经了解了复杂性的高级症状以及为什么复杂性会使软件开发变得困难,那么下一步就是了解导致复杂性的原因,以便我们设计系统来避免这些问题。复杂性是由两件事引起的:依赖性和模糊性。本节从高层次讨论这些因素。随后的章节将讨论它们与低级设计决策之间的关系。
就本书而言,当无法孤立地理解和修改给定的一段代码时,便存在依赖关系。该代码以某种方式与其他代码相关,如果更改了给定代码,则必须考虑和/或修改其他代码。在图 2.1a的网站示例中背景色在所有页面之间创建了依赖关系。所有页面都必须具有相同的背景因此如果更改一页的背景则必须更改所有背景。依赖关系的另一个示例发生在网络协议中。通常协议的发送方和接收方有单独的代码但是它们必须分别符合协议。更改发送方的代码几乎总是需要在接收方进行相应的更改反之亦然。
依赖关系是软件的基本组成部分,不能完全消除。实际上,我们在软件设计过程中有意引入了依赖性。每次编写新类时,都会围绕该类的 API 创建依赖关系。但是,软件设计的目标之一是减少依赖关系的数量,并使依赖关系保持尽可能简单和明显。
考虑网站示例。在每个页面分别指定背景的旧网站中,所有网页都是相互依赖的。新的网站通过在中心位置指定背景色并提供一个 API供各个页面在呈现它们时检索该颜色从而解决了该问题。新的网站消除了页面之间的依赖关系但是它围绕 API 创建了一个新的依赖关系以检索背景色。幸运的是,新的依赖性更加明显:很明显,每个单独的网页都取决于 bannerBg 颜色,并且开发人员可以通过搜索其名称轻松找到使用该变量的所有位置。此外,编译器还有助于管理 API 依赖性:如果共享变量的名称发生变化,任何仍使用旧名称的代码都将发生编译错误。新的网站用一种更简单,更明显的方式代替了一种不明显且难以管理的依赖性。
复杂性的第二个原因是晦涩。当重要的信息不明显时,就会发生模糊。一个简单的例子是一个变量名,它是如此的通用,以至于它没有携带太多有用的信息(例如,时间)。或者,一个变量的文档可能没有指定它的单位,所以找到它的惟一方法是扫描代码,查找使用该变量的位置。晦涩常常与依赖项相关联,在这种情况下,依赖项的存在并不明显。例如,如果向系统添加了一个新的错误状态,可能需要向一个包含每个状态的字符串消息的表添加一个条目,但是对于查看状态声明的程序员来说,消息表的存在可能并不明显。不一致性也是造成不透明性的一个主要原因:如果同一个变量名用于两个不同的目的,那么开发人员就无法清楚地知道某个特定变量的目的是什么。
在许多情况下,由于文档不足而导致模糊不清。第 13 章讨论了这个主题。但是,模糊性也是设计问题。如果系统设计简洁明了,则所需的文档将更少。对大量文档的需求通常是一个警告,即设计不正确。减少模糊性的最佳方法是简化系统设计。
依赖性和模糊性共同构成了第 2.2 节中描述的三种复杂性表现。依赖性导致变化放大和高认知负荷。晦涩会产生未知的未知数,还会增加认知负担。如果我们找到最小化依赖关系和模糊性的设计技术,那么我们就可以降低软件的复杂性。
## 2.4 复杂度是递增的
复杂性不是由单个灾难性错误引起的;它堆积成许多小块。单个依赖项或模糊性本身不太可能显着影响软件系统的可维护性。之所以会出现复杂性,是因为随着时间的流逝,成千上万的小依赖性和模糊性逐渐形成。最终,这些小问题太多了,以至于对系统的每次可能更改都会受到其中几个问题的影响。
复杂性的增量性质使其难以控制。可以很容易地说服自己,当前更改所带来的一点点复杂性没什么大不了的。但是,如果每个开发人员对每种更改都采用这种方法,那么复杂性就会迅速累积。一旦积累了复杂性,就很难消除它,因为修复单个依赖项或模糊性本身不会产生很大的变化。为了减缓复杂性的增长,您必须采用第 3 章中讨论的“零容忍”理念
## 2.5 结论
复杂性来自于依赖性和模糊性的积累。随着复杂性的增加,它会导致变化放大,高认知负荷和未知的未知数。结果,需要更多的代码修改才能实现每个新功能。此外,开发人员花费更多时间获取足够的信息以安全地进行更改,在最坏的情况下,他们甚至找不到所需的所有信息。最重要的是,复杂性使得修改现有代码库变得困难且冒险。