软件架构VI: 测量和控制架构特征

架构师必须处理软件项目所有不同方面的各种架构特征。 诸如性能、弹性和可伸缩性之类的运维方面与诸如模块化和可部署性之类的结构性问题融合在一起。这里着重于具体定义一些较常见的架构特征并为其建立治理机制。

测量架构特征

组织中有关架构特征的定义存在几个常见问题:

他们不是物理学
常用的许多架构特征含义不明确。例如,架构师如何设计敏捷性或可部署性?业界对通用术语的看法大相径庭,有时是由合理的不同上下文所驱动,有时是偶然的。

定义千差万别
即使在同一组织内,不同部门也可能在关键功能(例如绩效)的定义上存在分歧。在开发人员,架构和运维可以统一一个共同的定义之前,要进行适当的对话就很困难。

太复合
许多所需的架构特征在较小规模上包含许多其他特征。例如,开发人员可以将敏捷性分解为诸如模块化、可部署性和可测试性之类的特征。

架构特征的客观定义解决了所有三个问题:通过在组织范围内就架构特征的具体定义达成一致,团队围绕架构创建了一种普遍存在的语言。同样,通过鼓励客观定义,团队可以解开组合特征以发现他们可以客观定义的可测量特征。

可操作的测量

许多架构特征具有明显的直接度量,例如性能或可伸缩性。但是,即使根据团队的目标,这些也提供了许多细微的解释。例如,也许一个团队测量了某些请求的平均响应时间,这是运维架构特性测量的一个很好的例子。但是,如果团队仅测量平均值,那么如果某些边界条件导致1%的请求花费的时间比其他时间长10倍,会发生什么情况?如果该站点有足够的流量,则异常值甚至可能不会出现。因此,团队可能还需要测量最大响应时间以捕获异常值。

高水平的团队不只是建立出色的性能数字; 他们的定义基于统计分析。 例如,假设视频流服务希望监视可伸缩性。 工程师没有设置任意数字作为目标,而是随着时间的推移测量规模并建立统计模型,然后在实时指标超出预测模型时发出警报。 失败可能意味着两件事:模型不正确(哪些团队想知道)或某事不正确(哪些团队也想知道)。

结合工具和细致入微的理解,团队现在可以衡量的各种特征正在迅速发展。 例如,许多团队最近将精力集中在性能预算上,以衡量指标,例如第一幅有意义的图表和第一次发生的CPU空闲,这两者都为移动设备上的网页用户指出了性能问题。 随着设备、目标、功能和其他众多事物的变化,团队将找到新的事物和测量方法。

结构化测量

一些客观指标并不像绩效指标那么明显。 内部结构特征如何,例如定义明确的模块化呢? 遗憾的是,还没有用于内部代码质量的综合指标。 但是,某些度量标准和通用工具的确可以使架构师解决代码结构的某些关键方面,尽管范围狭窄。

代码的一个明显的可测量方面是复杂度,由循环复杂度度量定义。

架构师和开发人员普遍同意,过于复杂的代码代表着代码臭味(Code Smell)。 它实际上损害了代码库的每个令人希望的特性:模块化、可测试性、可部署性等。 但是,如果团队不关注逐渐增加的复杂性,那么这种复杂性将主导代码库。

过程测量

一些架构特征与软件开发过程相交。例如,敏捷性经常表现为理想的功能。但是,这是架构师可以分解为可测试性和可部署性等功能的复合架构特征。

可通过几乎所有评估测试完整性的平台的代码覆盖率工具来衡量可测试性。像所有软件检查一样,它不能代替思想和意图。例如,一个代码库可以具有100%的代码覆盖率,但是断言不佳,实际上并不能使人们对代码的正确性充满信心。但是,可测试性显然是客观可测量的特征。同样,团队可以通过多种指标来衡量可部署性:成功部署与失败部署的百分比,部署需要花费多长时间,部署引发的问题/错误以及许多其他问题。每个团队都有责任进行一组良好的评估,以捕获其组织在质量和数量上有用的数据。其中许多措施归结为团队的重点和目标。

敏捷及其相关部分显然与软件开发过程有关。但是,该过程可能会影响架构。例如,如果易于部署和可测试性是重中之重,那么架构师将在架构级别更加注重良好的模块化和隔离性,这是驱动结构决策的架构特征示例。实际上,如果软件项目范围内的任何事情都能够满足我们的三个标准,那么它就可能会升至架构特征的水平,从而迫使架构师做出设计决定以考虑到这一点。

治理和适应功能

一旦架构师确定了架构特征并对其进行了优先排序,他们如何确保开发人员将尊重这些优先事项?模块化是架构方面的一个重要例子,它很重要但并不紧迫。在许多软件项目中,紧迫性占主导地位,但是架构师仍然需要一种治理机制。

主导架构特征

源自希腊语Kubernan(转向)的治理是架构师角色的重要职责。顾名思义,架构治理的范围涵盖了架构师(包括企业架构师之类的角色)想要施加影响的软件开发过程的任何方面。例如,确保组织内的软件质量属于架构治理的范畴,因为它属于架构的范围,而过失可能导致灾难性的质量问题。

幸运的是,存在越来越复杂的解决方案,可以减轻架构师的困扰,这是软件开发生态系统中功能不断增长的一个很好的例子。极限编程催生的软件项目实现自动化,实现了持续集成,进一步实现了操作的自动化,现在我们将其称为DevOps,一直持续到架构治理。 《构建演化架构》(O’Reilly)一书描述了称为适应度函数的一系列技术,这些技术可用于自动执行架构管理的许多方面。

适应度函数

构建演化架构中的“演化”一词更多地来自于演化计算,而不是生物学。其中一位作者Rebecca Parsons博士在演化计算领域花费了一些时间,其中包括遗传算法之类的工具。遗传算法执行并产生答案,然后通过进化计算世界中定义的众所周知的技术进行变异。如果开发人员试图设计一种遗传算法以产生一些有益的结果,则他们通常希望对算法进行指导,从而提供指示结果质量的客观指标。该指导机制称为适应度函数:一种目标函数,用于评估输出与达到目标的接近程度。例如,假设开发人员需要解决移动营业员问题,这是一个著名的问题,是机器学习的基础。给定一个销售员和他们必须访问的城市列表,以及它们之间的距离,最佳路线是什么?如果开发人员设计了一种遗传算法来解决此问题,则一个适应度函数可能会评估路线的长度,因为最短的可能代表最大的成功。另一个适应性功能可能是评估与路线相关的总成本,并尝试将成本保持在最低水平。另一个可能是评估移动销售人员离开的时间,并进行优化以缩短总旅行时间。

演化架构的实践借用了这个概念来创建架构适应性功能:

架构适应度函数
提供对某些架构特征或架构特征组合进行客观完整性评估的任何机制

适应度函数不是供架构师下载的某些新框架,而是对许多现有工具的新视角。注意定义中的“任何机制”一词-用于架构特征的验证技术随其特征而变化。适应度函数与许多现有的验证机制重叠,具体取决于它们的使用方式:作为度量标准、监控、单元测试库、混乱工程等等,如图所示。

image.png

根据架构特征,可以使用许多不同的工具来实现适应度函数。例如,在“耦合”中,我们引入了度量标准,以允许架构师评估模块性。以下是适合度函数的几个示例,它们可以测试模块化的各个方面。

循环依赖

模块化是大多数架构师关心的隐式架构特征,因为维护不当的模块化会损害代码库的结构。因此,架构师应高度重视保持良好的模块化。但是,在许多平台上,团队与架构师的良好意图背道而驰。例如,在任何流行的Java或.NET开发环境中进行编码时,一旦开发人员引用了尚未导入的类,IDE就会帮助显示一个对话框,询问开发人员是否要自动导入该引用。这种情况经常发生,以至于大多数程序员习惯于像反射动作一样将自动导入对话框拖走。但是,任意地在彼此之间导入类或组件对于模块化来说是灾难。例如,图展示了一种架构师渴望避免的特别有害的反模式。

image.png

在图中,每个组件都引用了其他组件。 拥有这样的组件网络会破坏模块化,因为开发人员无法重用单个组件,而又不能使其他组件一起使用。 而且,当然,如果将其他组件耦合到其他组件,则该架构将越来越倾向于“大泥巴”反模式。 架构师如何控制这种行为,而又不需时不时地看着那些对触发器满意的开发人员? 代码审查虽然有帮助,但在开发周期中为时已晚,无法生效。 如果架构师允许开发团队在整个代码库中导入直到进行代码审查,则代码库中已经发生了严重损坏。

解决此问题的方法是编写一个适应度函数以查看周期,如示例所示。

示例 适应度函数可检测零件循环

public class CycleTest {
    private JDepend jdepend;

    @BeforeEach
    void init() {
      jdepend = new JDepend();
      jdepend.addDirectory("/path/to/project/persistence/classes");
      jdepend.addDirectory("/path/to/project/web/classes");
      jdepend.addDirectory("/path/to/project/thirdpartyjars");
    }

    @Test
    void testAllPackages() {
      Collection packages = jdepend.analyze();
      assertEquals("Cycles exist", false, jdepend.containsCycles());
    }
}

在代码中,架构师使用指标工具JDepend来检查包之间的依赖关系。 该工具了解Java包的结构,如果存在任何循环,则测试失败。 架构师可以将此测试连接到项目的连续构建中,而不必担心触发满意的开发人员意外引入周期。 这是一个适应度函数,可以保护重要而不是紧迫的软件开发实践的一个很好的例子:这是对架构师的重要关注,但对日常编码几乎没有影响。

与主要序列适应度函数的距离

在“耦合”中,我们引入了距主序列更深奥的距离度量,架构师也可以使用适应度函数进行验证,如示例所示。

示例—与主序列适应度函数的距离

@Test
void AllPackages() {
    double ideal = 0.0;
    double tolerance = 0.5; // project-dependent
    Collection packages = jdepend.analyze();
    Iterator iter = packages.iterator();
    while (iter.hasNext()) {
      JavaPackage p = (JavaPackage)iter.next();
      assertEquals("Distance exceeded: " + p.getName(),
        ideal, p.distance(), tolerance);
    }
}

在代码中,架构师使用JDepend为可接受的值建立阈值,如果类超出范围,则测试失败。

这既是针对架构特性的客观度量的示例,又是设计和实现适应性功能时开发人员与架构师之间协作的重要性的示例。这样做的目的不是让一群架构师登上象牙塔并开发开发人员无法理解的深奥适应度函数。

提示
架构师必须确保开发人员在将适应度函数强加给他们之前了解其功能。

在过去的几年中,适应度函数工具(包括一些专用工具)的复杂性有所提高。一个这样的工具就是ArchUnit,这是一个Java测试框架,其灵感来自于JUnit生态系统的各个部分并使用了这些部分。 ArchUnit提供了各种预定义的管理规则,这些规则被编码为单元测试,并允许架构师编写解决模块化的特定测试。考虑图中所示的分层架构。

image.png

设计分层整体结构(例如图6-4中的整体结构)时,架构师有充分的理由定义层(动机,权衡和分层体系结构的其他方面在第10章中进行了描述)。 但是,架构师如何确保开发人员会尊重这些层? 一些开发人员可能不了解这些模式的重要性,而其他开发人员则可能会采用“更好地请求宽恕而不是允许”的态度,这是因为一些诸如性能之类的对本地问题的关注。 但是,允许实施者侵蚀架构的原因会损害架构的长期健康。

ArchUnit允许架构师通过适应性函数解决此问题,如示例所示。

示例—ArchUnit适应度函数可控制图层

layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")

    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

在例中,架构师定义了各层之间的理想关系,并编写了验证适应度函数来对其进行控制。

.NET空间中的类似工具NetArchTest允许对该平台进行类似的测试。 例中显示了C#中的依赖验证。

示例—NetArchTest用于层依赖性

// Classes in the presentation should not directly reference repositories
var result = Types.InCurrentDomain()
    .That()
    .ResideInNamespace("NetArchTest.SampleLibrary.Presentation")
    .ShouldNot()
    .HaveDependencyOn("NetArchTest.SampleLibrary.Data")
    .GetResult()
    .IsSuccessful;

适应度函数的另一个例子来自Netflix的混乱的猴子和猴子部队。 整合安全性和管理猴子就是这种方法的例证。 从中猴子允许Netflix的架构师定义生产中由猴子执行的治理规则。 例如,如果架构师决定每个服务都应该对所有RESTful动词做出有用的响应,那么他们会将检查内容构建到从众猴子中。 同样,安全猴子会检查每个服务是否存在众所周知的安全缺陷,例如不应激活的端口和配置错误。 最终,看门人猴子寻找不再有其他服务路由到的实例。 Netflix具有不断发展的架构,因此开发人员通常会迁移到较新的服务,而无需协作者即可运行旧服务。 由于在云上运行的服务会消耗金钱,因此看门人猴子会寻找孤立的服务并将其分解到生产环境之外。

几年前,Atul Gawande撰写的颇有影响力的书《清单宣言》描述了航空飞行员和外科医生等职业如何使用清单(有时是法律规定的)。 这不是因为这些专业人士不了解工作或健忘。 相反,当专业人士一遍又一遍地完成非常详细的工作时,细节容易流失。 简洁的清单可以有效地提醒您。 这是适应度函数的正确视角—适应度函数不是重量级的治理机制,而是为架构师提供了一种机制,可以表达重要的架构原理并自动进行验证。 开发人员知道他们不应该发布不安全的代码,但是对于繁忙的开发人员来说,优先级会与数十或数百个其他优先级竞争。 特别是像安全猴子这样的工具,以及一般的适应度函数,允许架构师将重要的治理检查编入架构的基础。

你可能感兴趣的:(软件架构VI: 测量和控制架构特征)