软件设计指导-复杂性

Aug 8, 2018 19:05 · 3132 words · 7 minute read 软件设计指导

复杂性的定义

如果从现实的角度来定义复杂性,可以定义为下:

复杂性是任何与软件系统结构相关的,能够使得理解、修改该系统变得更加复杂和困难的事物。

复杂性与软件系统的结构有关,使得难以理解和修改系统。复杂性可以采取多种形式。例如,可能很难理解一段代码是如何工作的、实施一项小改进可能需要付出很多努力,或者可能不清楚系统的哪些部分必须进行修改才能实现改进、如果不引入另一个 bug,可能很难修复一个 bug。如果软件系统难以理解和修改,那么它很复杂,如果它易于理解和修改,那么它很简单。

您还可以考虑成本效益方面的复杂性。在一个复杂的系统中,它需要大量的工作才能完成改进。在非实现系统中,可以用更少的努力实现更大的改进。

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

复杂性的症状

复杂性通过三种方式表现出来,每种方式都会使得开发难以为继。

  • 更改放大: 复杂性的第一个症状是看似简单的变化需要在许多不同的地方进行代码修改。 例如,考虑包含多个页面的网站,每个页面都显示带有背景颜色的横幅。 在许多早期 Web 中为了更改此类 Web 站点的背景,开发人员可能必须手动修改每个现有页面; 对于拥有数千页的大型网站来说,这几乎是不可能的。 幸运的是,现代网站通过共用 CSS 的方法,其中横幅颜色在中心位置指定一次,并且所有单个页面都引用该共享值。 使用此方法,只需一次修改即可更改整个Web站点的标题颜色。 良好设计的目标之一是减少每个设计决策影响的代码量,因此设计更改不需要任何代码修改。

  • 认知负担: 复杂性的第二个症状是认知负担,它指的是开发人员为完成任务需要知道多少。 更高的认知负担意味着开发人员必须花更多的时间来学习所需的信息,并且因为他们错过了一些重要的错误而存在更大的错误风险。 例如,假设C中的函数分配内存,返回指向该内存的指针,并假定调用者将释放内存。 这增加了使用该功能的开发人员的认知负担; 如果开发人员无法释放内存,则会出现内存泄漏。 如果系统可以重构,以便调用者不需要担心释放内存(分配内存的同一模块也负责释放内存),它将减少认知负担。 认知负担以多种方式产生,例如具有许多方法的 API,全局变量,不一致性以及模块之间的依赖性。系统设计人员有时会假设复杂性可以通过代码行来衡量。 他们假设如果一个实现比另一个更短,那么它必须更简单; 如果它只需要几行代码来进行更改,那么更改必须很容易。 但是,这种观点忽略了与认知负担相关的成本。 我已经看到了允许只用几行代码编写应用程序的框架,但要弄清楚这些代码是什么非常困难。 一些需要更多代码行的方法实际上更简单,因为它减少了认知负担

  • 未知的未知: 复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须具有哪些信息才能成功执行任务。网站使用中心变量来确定横幅背景颜色,因此它似乎很容易更改。但是,一些网页使用较暗的背景色调进行强调,并且在各个页面中明确指定较暗的颜色。如果背景颜色发生变化,则强调颜色必须更改才能匹配。不幸的是,开发人员不太可能意识到这一点,因此他们可能会更改中央 bannerBg 变量而不更新强调颜色。即使开发人员意识到问题,但是哪些页面使用强调颜色并不明显,因此开发人员可能必须搜索 Web 站点中的每个页面。

在复杂性的三种表现形式中,未知的未知是最差的。未知的未知意味着您需要知道某些事情,但是您无法找到它是什么,甚至是否存在问题。在您进行更改后出现错误之前,您不会发现它。更改放大是令人讨厌的,但只要清楚哪些代码需要修改,系统将在更改完成后工作。类似地,高认知负担会增加变化的成本,但如果清楚要读取哪些信息,则变化仍然可能是正确的。由于未知未知,目前还不清楚该做什么,或者提议的解决方案是否可行。唯一可以确定的方法是读取系统中的每一行代码,这对于任何规模的系统都是不可能的。即使这可能还不够,因为改变可能取决于从未记录的微妙设计决策。

良好设计的最重要目标之一是使系统显而易见。这与高认知负担和未知的未知相反。在一个明显的系统中,开发人员可以快速了解现有代码的工作原理以及进行更改所需的内容。一个显而易见的系统是开发人员可以快速猜测要做什么,而不必非常努力地思考,并且确信猜测是正确的。

复杂性的原因

复杂性是由两件事引起的:依赖模糊不清

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

考虑一下 Web 站点示例。 在每个页面上单独指定背景的旧网站中,所有网页都相互依赖。 新网站通过在中心位置指定背景颜色并提供各个页面用于在渲染时检索该颜色的 API 来解决此问题。 新的 Web 站点消除了页面之间的依赖关系,但它创建了一个新的依赖 API,用于检索背景颜色。 幸运的是,新的依赖关系更加明显:每个 Web 页面都依赖于 bannerBg 颜色,开发人员可以通过搜索其名称轻松找到使用该变量的所有位置。 此外,编译器有助于管理 API 依赖性拒绝:如果共享变量的名称发生更改,则编译错误将出现在仍使用旧名称的任何代码中。 新网站用一个更简单,更明显的依赖取代了一个非显而易见且难以管理的依赖。

复杂性的第二个原因是模糊不清。当重要信息不明显时,就会出现不安全因素。一个简单的例子是一个变量名,它是如此通用,以至于它不带有太多有用的信息。或者变量的文档可能未指定其单位,因此找出的唯一方法是扫描使用变量的位置的代码。 模糊不清通常与依赖关联,其中依赖存在并不明显。例如,如果向系统添加了新的错误状态,则可能需要向包含每个状态的字符串消息的表添加条目,但对于查看状态的程序员来说,消息表的存在可能并不明显。宣言。不一致也是模糊不清的主要原因:如果相同的变量名称用于两个不同的目的,那么对于开发人员而言,特定变量用于这些目的并不明显。

在许多情况下,由于文档不充分而导致模糊不清。然而模糊不清也是一个设计问题。如果系统具有干净且明显的设计,那么它将需要更少的文档。对于大量文档的需求往往是一个警告,设计不太恰当。减少隐匿性的最佳方法是简化系统设计。

依赖性和模糊性共同构成了三种复杂性表现形式。依赖性导致更改放大和高认知负担。模糊不清造成未知的未知因素,也有助于认知负担。如果我们能找到最小化依赖性和模糊性的设计技术,那么我们就可以降低软件的复杂性。

复杂性的增量性质

复杂性不是由单一的灾难性错误引起的,它累积在很多小块中。 单个依赖或默默无闻本身不太可能显着影响软件系统的可维护性。 复杂性的产生是因为随着时间的推移,成百上千的小依赖性和晦涩难关。 最终,有这么多小问题,系统的每一个可能的变化都会受到其中几个的影响。

复杂性的增量性质使其难以控制。 自己很容易觉得,当前的变化带来的一点点复杂性并不重要。 但是,如果每个开发人员都采用这种方法进行每次更改,则复杂性会迅速累积。一旦复杂性积累起来,就很难消除,因为修复单个依赖或模糊不清本身并不会产生很大的影响。 为了减缓复杂性的增长,您必须采用“零容忍”理念。

结论

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

tweet Share