软件架构基础课

第19章 架构决策

2021-02-10  本文已影响0人  城里的月光_欧阳

对架构师的核心期望之一是做出架构决策。架构决策通常涉及应用或系统的结构,但也可能涉及技术决策,特别是当这些技术决策影响架构特性时。不管在什么环境下,一个好的架构决策有助于指导开发团队做出正确的技术选择。制定架构决策涉及收集足够的相关信息,证明决策的合理性,记录决策,并将决策有效地传达给合适的涉众。

架构决策反模式

架构决策是一门艺术。毫不意外地,作为架构师在做决策时会出现一些架构反模式。程序员Andrew Koenig将反模式定义为一种在开始时看起来是个好主意,但会给你带来麻烦的东西。反模式的另一个定义是产生负面结果的可重复过程。在做出架构决策时,可以(而且通常会)出现三种主要的架构反模式:“隐藏资产”(Covering Your Assets)反模式、“土拨鼠日”(Groundhog Day)反模式和“电子邮件驱动架构”(Email-Driven Architecture)反模式。这三种反模式通常遵循一个渐进的流程:克服“隐藏资产”反模式将导致“土拨鼠日”反模式,克服“土拨鼠日”反模式将导致“电子邮件驱动架构”反模式。做出有效和准确的架构决策需要架构师克服所有这三种反模式。

“隐藏资产”反模式

在尝试做出架构决策时出现的第一个反模式是“隐藏资产”反模式。当架构师由于害怕做出错误的选择而避免或推迟做出架构决策时,就会出现这种反模式。

有两种方法可以克服这种反模式。第一种方法是等到最后负责的时刻才做出重要的架构决策。最后负责时刻意味着等待,直到您有足够的信息来证明和验证你的决策,但不要等待太久,以至于耽误了开发团队或陷入分析瘫痪的反模式。避免这种反模式的第二种方法是不断与开发团队协作,以确保您所做的决策能够按预期实现。这一点非常重要,因为作为一个架构师,不可能对关于特定技术和所有相关问题的每一个细节都了解。通过与开发团队紧密合作,架构师可以在出现问题时快速修正架构决策。

为了说明这一点,假设架构师决定使用只读复制缓存将所有与产品相关的引用数据(产品描述、权重和维度)缓存在需要这些信息的所有服务实例中,其中主副本归目录服务所有。复制缓存意味着,如果产品信息发生任何更改(或添加了新产品),目录服务将更新其缓存,然后通过复制(内存中)缓存产品将其复制到需要该数据的所有其他服务。这个决定的一个正当的理由是减少服务之间的耦合,并在不必进行服务间调用的情况下有效地共享数据。然而,实现此架构决策的开发团队发现,由于一些服务的某些可扩展性需求,此决策将需要比可用内存更多的进程内内存。通过与开发团队密切合作,架构师可以迅速意识到问题,并调整架构决策以适应这些情况。

“土拨鼠日”反模式

一旦架构师克服了“隐藏资产”反模式并开始做出决策,第二个反模式就会出现:“土拨鼠日”反模式。当人们不知道为什么作出决定,所以它不断被讨论了一遍又一遍时,“土拨鼠日”反模式就会出现。“土拨鼠日”反模式得名于比尔·默里的电影《土拨鼠日》,在那里,每天都是2月2日。

“土拨鼠日”反模式之所以出现,是因为架构师当做出架构决策,但无法为决策提供理由(或完整的理由)。在证明架构决策的合理性时,为你的决策提供技术和业务合理解释是很重要的。例如,架构师可以决定将单个应用拆分为单独的服务,以解耦应用的功能方面,以便应用的每个部分使用更少的虚拟机资源,并且可以单独维护和部署。虽然这是一个很好的技术证明的例子,但是缺少的是业务证明,换句话说,业务为什么要为这种架构重构付费?这一决策的一个很好的业务理由可能是更快地交付新的业务功能,从而缩短推出市场的时间。另一个可能是降低与开发和发布新特性相关的成本。

在论证决策时提供业务价值对于任何架构决策都至关重要。它也是一个很好的试金石,用于确定是否应该首先做出架构决策。如果一个特定的架构决策没有提供任何业务价值,那么它可能不是一个好的决策,应该重新考虑。

四个最常见的业务理由包括成本、推出市场时间、用户满意度和战略定位。当关注这些常见的业务理由时,重要的是要考虑到什么对业务利益相关者是重要的。如果业务利益相关者不太关心成本,而更关心推出市场时间,那么仅基于成本节约来证明某个特定决策的合理性可能不是正确的决定。

“电子邮件驱动架构”反模式

一旦架构师做出决策并充分证明这些决策是正确的,第三种体系结构反模式就会出现:电子邮件驱动架构。“电子邮件驱动架构”反模式是人们丢失、忘记甚至不知道已经做出了架构决策,因此不太可能实现该架构决策。这种反模式完全是为了有效地传达你的架构决策。电子邮件是一个很好的沟通工具,但它是一个糟糕的文档存储系统。

有许多方法可以提高沟通架构决策的有效性,从而避免“电子邮件驱动架构”反模式。传达架构决策的第一条规则是不要将架构决策包含在电子邮件正文中。将架构决策包含在电子邮件正文中会为该决策创建多个记录系统。很多时候,重要的细节(包括理由)被遗漏在邮件中,因此,创造了“土拨鼠日”反模式一遍又一遍。另外,如果架构决策曾经被更改或取代,人们会如何收到修改后的决策?更好的方法是在电子邮件正文中仅提及决策的性质和上下文,并提供一个指向实际架构决策的单个记录系统的链接和相应的详细信息(无论是指向wiki页面的链接还是文件系统中的文档)。

有效沟通架构决策的第二条规则是只通知那些真正关心架构决策的人。一种有效的方法是将邮件正文按如下方式来写:

“嗨,桑德拉,我做了一个关于服务间通信的重要决策,这会直接影响到你。请使用以下链接查看具体决策内容…”

注意第一句中的短语:“关于服务间通信的重要决策”。这里提到的是决策的背景,而不是实际的决策本身。第一句话的第二部分更为重要:“这会直接影响到你。”如果一个架构决策没有直接影响到这个人,那么为什么要用你的架构决策来打扰那个人呢?这是一个很好的试金石测试,用于确定哪些涉众(包括开发人员)应该被直接通知架构决策。第二句话提供了一个指向架构决策位置的链接,它只位于一个位置,因此为决策提供了一个记录系统。

架构重要性

许多架构师认为,如果架构决策涉及任何特定的技术,那么它就不是架构决策,而是技术决策。这并不总是正确的。如果架构师决定使用某种特定的技术,因为它直接支持特定的架构特性(如性能或可扩展性),那么这就是一个架构决策。Michael Nygard,著名的软件架构师和《发布!》(Release it!)的作者(实用书架),通过创造一个术语“架构重要性”,解决了架构师应该负责哪些决策(因此什么是架构决策)的问题。Michael认为,架构上重要的决策是那些影响结构、非功能特性、依赖关系、接口或构建技术的决策。

结构是指影响正在使用的架构的模式或风格的决策。这方面的一个例子是决定在一组微服务之间共享数据。这个决定影响微服务的有界上下文,因此也影响应用的结构。

非功能特性是对正在开发或维护的应用或系统很重要的架构特性(“-ilities”)。如果一个技术的选择影响性能,并且性能是应用的一个重要方面,那么它就变成了一个架构决策。

依赖关系是指系统中组件和/或服务之间的耦合点,这反过来又会影响整体的可扩展性、模块化、敏捷性、可测试性、可靠性等。

接口是指如何访问和编排服务和组件,通常通过网关、集成中心、服务总线或API代理。接口通常涉及定义契约,包括这些契约的版本控制和弃用策略。接口影响使用系统的其他人,因此在架构上具有重要意义。

最后,构建技术指的是关于平台、框架、工具甚至过程的决策,尽管这些决策本质上是技术性的,但可能会影响架构的某些方面。

架构决策记录

记录架构决策的最有效方法之一是通过架构决策记录(Architecture Decision Records,ADRs)。ADRs最初是由Michael

Nygard在一篇博客文章中宣传的,后来在ThoughtWorks技术雷达上标记为“采用”。ADR由描述特定架构决策的短文本文件(通常一到两页长)组成。虽然ADRs可以使用纯文本编写,但它们通常是以某种文本文档格式编写的,比如AsciiDoc或Markdown。或者,也可以使用wiki页面模板编写一个ADR。

工具也可以用来管理ADR。Nat Pryce是《Growing Object-Oriented Software Guided by Tests》(Addison-Wesley)的合著者,他为ADRs编写了一个开源工具ADR-tools。ADR-tools提供了一个命令行界面来管理ADRs,包括编号方案、位置和替代逻辑。来自德国的软件工程师Micha Kops写了一篇关于使用ADR-tools的博客文章,提供了一些关于如何使用ADR-tools来管理架构决策记录的好例子。

基本结构

一个ADR的基本结构包括五个主要部分:标题(Title)、状态(Status)、背景(Context)、决定(Decision)和结果(Consequences)。作为基本结构的一部分,我们通常会添加两个附加部分:合规(Compliance)和备注(Notes)。这个基本结构(如图19-1所示)可以扩展到包括任何其他认为需要的部分,只要模板保持一致和简洁。这方面的一个很好的例子是,如果有必要,可以添加一个替代部分,以提供对所有其他可能的替代解决方案的分析。

图19-1. ADR基本结构

标题

ADR的标题通常按顺序编号,并包含描述架构决策的简短表述。例如,在订单服务和支付服务之间使用异步消息传递的决策可以这样阅读:“42. 在订单和支付服务之间使用异步消息传递。”。标题应具有足够的描述性,以消除对决策的性质和背景的任何歧义,但同时应简短明了。

状态

ADR的状态可以标记为已建议(Proposed)、已接受(Accepted)或已取代(Superseded)。已建议状态意味着决策必须由更高级别的决策者或某种架构治理机构(如架构审查委员会)批准。已接受状态表示决策已被批准并准备好执行。已取代状态意味着该决策已被另一ADR更改和取代。已取代状态总是假定先前的ADR状态为已接受;换句话说,一个已建议的ADR永远不会被另一个ADR取代,而是继续被修改直到被接受。

已取代状态是一种强有力的方法,它可以记录做出了哪些决策、当时为什么做出这些决策、新的决策是什么以及为什么改变了这些决策。通常,当一个ADR被取代时,它会被标上取代它的决策。同样,取代另一个ADR的决策也标有被其取代的ADR。例如,假设ADR 42(“在订单和支付服务之间使用异步消息传递”)先前已获得批准,但由于支付服务的实现和地位后来发生了变化,现在必须在两个服务之间使用REST(ADR 68)。状态如下所示:

ADR 42. 在订单和支付服务之间使用异步消息传递

状态:被68取代

ADR 68. 在订单和支付服务之间使用REST

状态:已接受,取代42

ADR 42和68之间的链接和历史轨迹避免了必然发生的关于ADR 68 “怎么使用消息传递?”的问题。

ADRS和征求意见(RFC)

如果架构师希望发送ADR草案征求意见(当架构师希望与更多的参与人员验证各种假设和断言时,这有时是个好主意),我们建议创建一个名为征求意见(RFC)的新状态,并指定完成评审的截止日期。这种做法避免了惯常的“分析瘫痪”反模式,即决策永远被讨论,但从来没有真正落实。一旦达到该日期,架构师可以分析对ADR的所有评论,对决策进行任何必要的调整,做出最终决策,并将状态设置为“已建议”(如果架构师能够自己批准决策,则在这种情况下,状态将被设置为“已接受”)。一个ADR的RFC状态示例如下:

状态

征求意见,截止日期2010年1月9日

ADR状态部分的另一个重要方面是,它迫使架构师与其上司或首席架构师进行必要的对话,讨论他们可以自行批准架构决策的标准,或者是否必须通过更高级别的架构师、架构评审委员会批准,或者其他架构管理机构。

成本、跨团队影响和安全这三个准则形成这些对话的良好开端。成本可以包括软件购买或许可费、额外的硬件成本,以及实现架构决策的总体花费。工作量成本可以通过将执行架构决策的预估小时数乘以公司的标准全职当量(FTE)率来估计。项目所有者或项目经理通常拥有FTE的金额。如果架构决策的成本超过了某个数量,那么必须将其设置为已建议状态并由其他人批准。如果架构决策影响其他团队或系统或具有任何类型的安全影响,那么它不能由架构师自行批准,必须由更高级别的管理机构或首席架构师批准。

一旦确定了标准和相应的限制并达成一致(例如“超过5000欧元的成本必须得到架构评审委员会的批准”),就应该很好地记录这些标准,以便创建ADRs的所有架构师知道他们何时可以和不可以批准自己的架构决策。

背景

ADR的背景部分指定了起作用的力量。换句话说,“什么情况迫使我做出这个决策?” ADR的这一部分允许架构师描述具体情况或问题,并简要阐述可能的替代方案。如果要求架构师详细记录每个备选方案的分析,那么可以在ADR中添加一个附加的备选方案部分,而不是将该分析添加到背景部分。

背景部分还提供了一种记录架构的方法。通过对背景进行阐述,架构师也是在描述架构。这是一种以清晰简洁的方式记录架构的一个特定领域的有效方法。继续上一节中的示例,背景可能如下所示:“订单服务必须将信息传递给支付服务,以便为当前下的订单付款。这可以使用REST或异步消息传递来完成。”请注意,这个简洁的语句不仅指定了场景,还指定了备选方案。

决策

ADR的决策部分包含架构决策内容,以及作出决策的充分理由。Michael Nygard介绍了一种很好的方式,通过使用非常肯定的、命令性的而不是被动的语态来陈述架构决策。例如,在服务之间使用异步消息传递的决策将表述为“我们将在服务之间使用异步消息传递”。与“我认为服务之间的异步消息传递将是最佳选择”相比,这是一种更好的说明决策的方式。注意,这里不清楚决策是什么,甚至不清楚是否已经做出决策,只说明了架构师的意见。

也许ADRs决策部分最强大的一个方面是它允许架构师更多地强调为什么要这样而不是如何去做。理解为什么要做决策比理解事情是如何运作的重要得多。大多数架构师和开发人员可以通过查看上下文图来确定事情是如何工作的,但不能确定为什么要做出决策。了解做出决策的原因和相应的理由有助于人们更好地理解问题的背景,并通过重构到可能产生问题的另一个解决方案来避免可能的错误。

为了说明这一点,请考虑几年前的一个原始架构决策,即使用Google的远程过程调用(gRPC)作为两个服务之间通信的手段。几年后,另一个架构师在不理解为什么要做出这个决策的情况下,选择覆盖这个决策,转而使用消息传递来更好地解耦服务。然而,实现这种重构会突然导致延迟的显著增加,进而最终导致上游系统出现超时。如果理解gRPC最初的用途是显著减少延迟(以紧密耦合服务为代价),那么第一时间就可以防止重构的发生。

结果

ADR的结果部分是另一个非常强大的部分。这一部分记录了一个架构决策的总体影响。架构师做出的每一个架构决策都会产生某种影响,无论是好是坏。必须指定架构决策的影响迫使架构师考虑这些影响是否超过决策的好处。

这个部分的另一个好的用法是记录与架构决策相关联的权衡分析。这些权衡可以是基于成本的,也可以是针对其他架构特性的权衡。例如,考虑使用异步(即发即弃)消息传递在网站上发布评论的决策。这个决定的理由是将发布评论请求的响应时间从3100毫秒显著提高到25毫秒,因为用户不需要等待实际的评论被发布(只需要消息被发送到队列)。虽然这是一个很好的理由,但其他人可能会认为这是一个坏主意,因为与异步请求相关联的错误处理非常复杂(“如果有人用脏话来发布评论,会发生什么?”)。对这个决定提出质疑的人不知道,这个问题已经与业务相关人员和其他架构师讨论过了,从权衡的角度来看,更重要的是提高响应能力并处理复杂的错误处理,而不是等待同步地向用户提供评论成功发布的反馈。通过利用ADRs,权衡分析可以包含在结果部分,提供架构决策的背景(和权衡)的全面情况,从而避免这些情况。

合规

ADR的合规部分不是ADR的标准部分之一,但我们强烈建议添加该部分。法规遵从性部分迫使架构师考虑如何从法规遵从性的角度来度量和管理架构决策。架构师必须决定此决策的合规性检查必须是手动的,还是可以使用适应性函数(fitness function)自动进行。如果可以使用适应性函数实现自动化,那么架构师可以在本部分中指定如何编写适应性函数,以及是否需要对代码库进行任何其他更改,以度量此架构决策的合规性。

例如,在图19-2所示的传统n层架构中考虑以下架构决策。业务层中业务对象使用的所有共享对象将驻留在共享服务层中,以隔离和包含共享功能。

图19-2.一个架构决策示例

通过使用Java中的ArchUnit或C#中的NetArchTest,可以自动度量和管理这种架构决策。例如,使用Java中的ArchUnit,自动适应性函数测试可能如下所示:

@Test

public voidshared_services_should_reside_in_services_layer() {

   classes().that().areAnnotatedWith(SharedService.class)

       .should().resideInAPackage("..services..")

        .because("All shared servicesclasses used by business " +

                 "objects in the businesslayer should reside in the services " +

                 "layer to isolate andcontain shared logic")

        .check(myClasses);

}

注意,这个自动适应性函数需要编写新的故事来创建一个新的Java注释(@SharedService),然后将这个注释添加到所有共享类中。这部分还指定了什么是测试、在哪里可以找到测试、如何执行测试以及何时执行测试。

备注

另一个不是标准ADR的一部分,但我们强烈建议添加的是备注部分。这部分包括关于ADR的各种元数据,例如:

- 原作者

- 批准日期

- 批准人

- 取代日期

- 最后修改日期

- 修改人

- 上次修改内容

即使在版本控制系统(如Git)中存储ADRs时,附加的元信息也非常有用,超出了存储库所能支持的范围,因此无论ADRs如何存储以及存储在何处,我们都建议添加此部分。

存储ADRs

一旦架构师创建了一个ADR,它就必须存储在某个地方。无论ADRs存储在哪里,每个架构决策都应该有自己的文件或wiki页面。一些架构师喜欢将ADRs与源代码一起保存在Git存储库中。将ADRs保存在Git存储库中还允许对ADR进行版本控制和跟踪。但是,对于大型组织,我们出于几个原因告诫不要使用这种做法。首先,每个需要了解架构决策的人可能无权访问Git存储库。其次,这不是存储ADRs的好地方,因为ADR的上下文在应用的Git存储库之外(比如集成架构决策、企业架构决策或每个应用通用的决策)。基于这些原因,我们建议将ADRs存储在wiki(使用wiki模板)或共享文件服务器上的共享目录中,wiki或其他文档呈现软件可以轻松访问这些目录。图19-3显示了这个目录结构(或wiki页面导航结构)的示例。

图19-3.存储ADRs的目录结构示例

应用目录包含特定于某种应用上下文的架构决策。此目录被进一步细分为多个目录。公共子目录用于所有应用程序的架构决策,例如“所有与框架相关的类都将包含一个注释(@framework在Java中)或属性([framework]在C#)来标识属于底层框架代码的类。”应用目录下的子目录对应于特定的应用或系统上下文,并包含特定于该应用或系统的架构决策(在本例中,是ATP和PSTD应用)。集成目录包含那些涉及应用、系统或服务之间通信的ADRs。企业架构ADRs包含在企业目录中,表示这些是影响所有系统和应用的全局架构决策。企业架构ADR的一个例子是“对系统数据库的所有访问都只能从所属系统进行”,从而防止在多个系统之间共享数据库。

在wiki中存储ADRs时(我们的建议),适用前面描述的相同结构,每个目录结构表示一个导航页。每个ADR将被表示为每个导航页(应用、集成或企业)中的单个wiki页面。

本节中指出的目录或导航页名称只是一个建议。每个公司都可以选择任何适合自己情况的名字,只要这些名字在团队中是一致的。

ADRs作为文档

记录软件架构一直是一个困难的话题。虽然正在出现一些用于绘制架构的标准(如软件架构师Simon Brown的C4模型或The Open Group ArchiMate标准),但是还没有用于记录软件架构的标准。这就是ADRs的用武之地。

架构决策记录是记录软件架构的有效手段。ADR的背景部分提供了一个极好的机会来描述需要做出架构决策的系统的特定领域。这部分还提供了一个描述替代方案的机会。也许更重要的是决策部分描述了做出特定决策的原因,这是迄今为止最好的架构文档形式。结果部分通过描述特定决策的附加的方面,将最后一部分添加到架构文档中,例如选择性能而不是可扩展性的权衡分析。

使用ADRs作为标准

很少有人喜欢标准。大多数时候,标准似乎更适合于控制人们和他们做事的方式,而不是任何有用的东西。将ADRs用于标准可以改变这种不良做法。例如,ADR的背景部分描述了强制执行特定标准的情况。ADR的决策部分不仅可以用来说明标准是什么,更重要的是,为什么需要存在标准。这是一种很好的方法,可以用来判断特定标准是否应该首先存在。如果一个架构师不能证明这个标准是合理的,那么也许它不是一个好的标准来制定和执行。此外,开发人员越了解某个特定标准存在的原因,他们就越有可能遵循它(相应地,也就不会质疑它)。ADR的结果部分是架构师判断标准是否有效和是否应该制定的另一个好地方。在这一部分中,架构师必须考虑并记录他们正在制定的特定标准的含义和结果。通过分析结果,架构师可能会决定该标准终究不应该被应用。

示例

许多架构决策存在于我们正在进行的“案例研究:Going, Going, Gone”中。使用事件驱动的微服务、拆分竞拍者和拍卖者的用户界面、使用实时传输协议(RTP)进行视频捕获、使用单个API层以及使用发布和订阅消息传递只是为这个拍卖系统做出的几十个架构决策中的几个。在一个系统中做出的每一个架构决策,无论多么明显,都应该被记录和证明。

图19-4说明了“Going,Going,Gone”拍卖系统中的一个架构决策,即在投标捕获、投标横幅和投标跟踪器服务之间使用发布和订阅(pub/sub)消息传递。

图19-4.在服务之间使用发布/订阅

此架构决策的ADR可能类似于图19-5:

图19-5.  ADR76. 投标服务之间的异步发布/订阅消息传递

原文参考:https://www.jianshu.com/p/54fbb9f56367

全书翻译目录:https://www.jianshu.com/p/05711d172dfa

上一篇下一篇

猜你喜欢

热点阅读