演进式架构(原书第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1 什么是适应度函数

我们从演进计算领域借鉴了这个概念来定义架构里的适应度函数:

架构适应度函数为某些架构特征提供了客观的完整性评估。

架构适应度函数构成了实现演进式架构的主要机制。

随着解决方案的领域部分演进,团队开发出了广泛的工具和技术保证在开发新功能的同时不破坏原有功能:单元、功能和用户接受度测试。事实上,大部分在一定规模以上的公司都有一个部门专门负责领域演进,叫作质量保证(Quality Assurance,QA):保证变化对现有功能不会有负面影响。

因此,高效团队通常有机制来管理问题域的演进变化:新功能的增加、行为的改变等。领域通常是在一个相当连贯的技术栈中编写的:Java、.NET或者许多其他平台。因此,团队可以下载并使用适用于他们技术栈的测试库。

适应度函数之于架构特征,就如同单元测试之于领域。然而,想要验证名目繁多的可能的领域特征,团队无法仅使用单一工具做到。相反,适应度函数包含了生态系统里不同部分的各种各样的工具,取决于团队需要治理的架构特征,如图2-1所示。

图2-1:适应度函数包含各种各样的工具和技术

如图2-1所示,架构师可以使用多种不同的工具来定义适应度函数。

监控

DevOps和其他运维工具(例如监控器)允许团队验证性能、可伸缩性等关注点。

代码指标

架构师可以在单元测试中嵌入指标检查和其他验证来校验各种架构关注点,包括设计标准(第4章会有很多示例)。

混沌工程

这是最近发展起来的工程实践分支,通过注入故障,人为地对远程环境施加压力,迫使团队建立有韧性的系统。

架构测试框架

近几年,专注于测试架构结构的测试框架已经崭露头角,帮助架构师把各种校验写入自动化测试。

安全扫描

安全——即使是由组织的另一部门监督,也会影响架构师的设计决策,因此也属于架构师想要治理的问题范畴。

在定义适应度函数和其他因素的分类之前,我们先用一个例子让这个概念不那么抽象。组件循环是所有带有组件的平台中常见的反模式。考虑一下图2-2中的三个组件。

图2-2:当组件有循环依赖的时候就产生了一个环

架构师认为图2-2所示的循环依赖是一种反模式,因为当开发人员想要复用其中一个组件时,它会造成一些困难——每个缠在一起的组件都会被引入。因此,普遍来说,架构师希望循环的数量越少越好。然而,这个宇宙竭尽所能地阻止架构师通过简单的工具解决这个问题。当开发人员在现代集成开发环境(Integrated Development Environment,IDE)里引用一个还未引入的命名空间或包的时候,会发生什么?它会自动弹出一个对话框,自动引入所需的包。

开发人员对此功能如此熟稔,以至于顺手就确认关掉,从来不会仔细查看到底引入了什么。大部分时间,自动引入非常便利,也不会带来任何问题。然而,偶尔那么一次,它会产生组件循环。架构师应该如何预防此类问题?

考虑图2-3中的这些包。

图2-3:组件循环在Java中以包的方式出现

ArchUnit(https://www.archunit.org)是一个测试工具,灵感来源于JUnit,并使用了其中的部分工具,不过它是用来测试各种架构特征的,包括在某个范围内校验循环,如图2-3所示。

例2-1演示了如何使用ArchUnit防止循环。

例2-1:使用ArchUnit防止循环

在这个例子里,测试工具“认识”循环。想要防止代码库里滋生循环的架构师可以把这个测试接入持续构建流程,从此就可以高枕无忧了。我们会在第4章演示更多ArchUnit的例子以及同类工具。

首先让我们给适应度函数一个更严格的定义,然后再细细品鉴它们是如何指导架构演进的。

不要把定义里的“函数”部分错误地理解成架构师必须用代码实现所有适应度函数。从数学角度来说,函数从一个允许的输入值集合里拿到输入,产生输出到允许的输出集合中。在软件领域,我们一般用函数这个术语指代代码中的实现。然而,在敏捷软件开发的验收标准中,演进式架构的适应度函数可以不在软件中实现(例如,出于监管原因所需的人工步骤)。架构适应度函数是一种客观的度量标准,但是架构师可以用不同的方式实现这个度量标准。

如第1章所述,现实世界的架构包含很多不同的维度,包括性能需求、可靠性、安全、可运维性、编码标准和集成。我们希望适应度函数可以代表架构的各种需求,这就要求我们去寻找(或创造)方式方法,以度量需要治理的对象。我们先看几个例子,然后开拓思维来理解不同种类的函数。

性能需求可以很好地体现适应度函数的用途。思考一个这样的需求,它要求所有的服务请求必须在100ms内响应。我们可以实现一个测试(如适应度函数),测量服务请求的响应,如果返回值大于100ms则失败。为此,每个新的服务都要有与之对应的性能测试添加到测试集中(你将在第3章了解更多触发式适应度函数)。性能也是一个说明架构师可以从很多不同的角度来考量一些场景的度量标准的很好的例子。例如,性能指的可以是用监控工具度量请求响应时间,也可以是其他指标,例如首次内容绘制,这是一个由Lighthouse(https://oreil.ly/7EHeZ)提供的手机端性能指标。性能适应度函数的目的不是度量性能的方方面面,而是度量架构师认为值得治理的那些。

适应度函数还可以用来保持编码标准。拿一个常见的代码指标圈复杂度(Cyclomatic Complexity,C C)来说,它是一种度量所有结构型编程语言中函数或方法的复杂性的方式。架构师可以通过任一支持该维度测量的工具来设置一个上限,通过持续集成里的单元测试来守护该指标。

无论需求如何,由于复杂性或其他的限制,开发人员并不总是能完全达到某些适应度函数的要求。比如数据库的硬件故障转移。虽然恢复本身可能是完全自动化的(并且应该是这样),但触发测试本身最好手动完成。此外,尽管开发人员应该鼓励脚本化和自动化,但在一些情况下,人工判定测试是否通过可能会更加高效。

这些例子凸显了适应度函数可以采取的不同的形式,当适应度函数失败时的即时响应,以及开发人员可以运行它们的时机和方式。虽然我们不一定能做到运行一个脚本就得出“当前架构综合适应度得分是42”这样的结论,但我们可以就架构的状况进行准确无误的讨论。我们也可以讨论可能会影响架构适应度的变更。

最后,当我们说适应度函数指导了演进式架构时,我们的意思是根据个体和整体的适应度函数来评估各个架构选择,以确定变更的影响。适应度函数总体上表示在架构中对我们重要的东西,这使我们能在软件系统开发过程中做出至关重要又令人头疼的权衡决策。

你可能会想:“等等!这不就是我们多年来一直在持续集成的过程中运用的代码指标吗?没什么新鲜的!”你说的也没错:在自动化过程中验证软件的某些部分的想法早已不是什么新鲜事。然而,我们以前把不同的架构验证机制孤立看待——代码质量、DevOps指标与安全性等。适应度函数将许多现有的概念统一成一个机制,使架构师可以用一样的方式考虑许多现有的(常常是临时的)“非功能性需求”测试。收集重要的架构阈值和需求作为适应度函数允许对以前模糊的、主观的评估标准进行更精确的表示。我们利用大量现有的机制来构建适应度函数,包括传统测试、监控和其他工具。并非所有测试都是适应度函数,但某些测试是——如果测试有助于验证架构问题的完整性,则将其视为适应度函数。