架构决策记录
记录架构决策的最有效方法之一是进行架构决策记录( ADR )。ADR 最开始由 Michael Nygard 在博客中进行宣传,随后在 ThoughtWorks 技术雷达中标为“采纳”。一条 ADR 是个描述特定架构决策的短文本文件(通常为 1~2 页)。虽然可以使用纯文本编写ADR,但通常用某种文本文档格式(如 AsciiDoc 或 Markdown)来编写。当然,也可以使用 wiki 页面模板编写 ADR。
也有一些管理 ADR 的工具。Nat Pryce—Growing Object-Oriented Software Guided by Tests( Addison-Wesley 出版社)的合著者— 编写了一个 ADR 开源工具,叫作 ADR- tools。ADR-tools 提供了一组命令行接口来管理 ADR,包括编号方案、位置以及取代逻辑。来自德国的软件工程师 Micha Kops 写了一篇关于如何使用 ADR-tools 的文章,提供了一些使用 ADR-tools 来管理架构决策记录的优秀示例。
基本结构
ADR 的基本结构包括五个主要部分:标题、状态、背景、决策和后果。我们通常会在基本结构中再添加两个内容:合规性和备注。提供模板是为了保持一致和简洁,在这个基本结构(如图 19-1 所示)之上可以添加任何其他必要的内容。例如,如有必要,可以添加一个替代节,用来对其他替代解决方案进行分析
图 19-1:基本的 ADR 结构
标题
ADR 的标题通常按顺序编号,是一句简短的描述架构决策的话。例如,在订单服务和付款服务之间使用异步消息传递的决策标题可能是:“42. 在订单服务和付款服务之间使用异步消息传递。”标题应具有足够的描述性,可以消除决策性质和背景的歧义,但同时又要简短明了。
状态
ADR 的状态分记为已提议、已接受或已取代。已提议意味着该决策必须由一个更高级别的决策者或某种架构管理机构(例如架构审查委员会)批准。已接受表示该决策已被批准并且可以执行。已取代表示该决策已被更改并被另一个 ADR 取代。已取代始终假定该 ADR 之前已被接受。换句话说,已提议的 ADR 永远不会被另一个 ADR 取代,而是会继续进行修改直到被接受。
已取代是一种保留决策历史的非常有用的方式,能够记录当时做出的决策是什么、为什么做出这样的决策以及为何做出更改。通常,当 ADR 被标为已取代时,会标注上取代它的决策。同样,取代另一个 ADR 的决策也应当用其取代的 ADR 进行标记。例如,假设对于先前已被接受的 ADR 42(“在订单服务和付款服务之间使用异步消息传递”),由
于后来对付款服务的实现和地址进行了修改,因此现在必须使用 REST( ADR 68 )。此时,ADR 42 和 68 的状态如下所示:
ADR 状态的另一个重要意义在于,它迫使架构师与老板或首席架构师进行必要的对话, 以讨论他们可以自行批准架构决策的标准,或者是否必须通过更高级别架构师、架构审查委员会或其他架构管理机构的批准。
成本、跨团队影响和安全性这三个标准能够为此类对话开个好头。成本可包括软件的购买或许可的费用、额外的硬件成本以及实施架构决策所需的工作。可以通过将预估的实施架构决策的小时数乘以公司的标准全时等效( Full-Time Equivalency,FTE)费率来估算工作成本。项目所有者或项目经理通常了解 FTE 的数量。如果架构决策的成本超过一定限额,则必须将它设置为“已提议”状态并经由其他人批准。如果架构决策会对其他团队或系统产生影响,或存在任何安全隐患,则该决策不能由架构师自行批准,必须由更高级别的管理机构或首席架构师批准。
一旦建立了标准及相应的限制并达成一致(例如,“若成本超过 5000 欧元,则必须由架构审查委员会批准”),则应进行充分记录,以便所有创建 ADR 的架构师都知道什么时候可以自行批准架构决策,什么时候不可以。
背景
ADR 的背景部分指明了决策背后的作用力。换句话说,“什么情况迫使我做出这个决策?”ADR 的这一部分让架构师描述具体情况或问题,并简要阐述可能的替代方案。如果需要架构师详细记录每个替代方案的分析,则可以将“其他替代方案”部分添加到ADR 中,而不是将该分析添加到背景当中。
背景部分还是一种记录架构的方法。描述背景的同时,架构师也在描述架构。这是一种以清晰简明的方式记录架构特定部分的有效方法。继续上一节中的例子,背景可能是:“订单服务必须将信息传递给付款服务才能完成当前订单的付款。这里的信息传递可以使用 REST 或异步消息传递来完成。”请注意,这条简洁的声明不仅说明了方案,还说明了替代方案。
决策
ADR 的决策部分包含架构决策及该决策的合理性分析。Michael Nygard 介绍了一种陈述架构决策的好方法,即使用非常正面、指挥性的表达而不是消极的表达。例如,在服务之间使用异步消息传递的决策可表达为“我们将在服务之间使用异步消息传递”,与“我认为服务之间的异步消息传递是最好的选择”相比,这是一种更好的陈述决策的方式。请注意,第二种表述没有说清楚决策是什么,甚至没有说明是否已有决策,仅仅只是陈述了架构师的看法。
ADR 决策部分最强大的一个地方可能是它让架构师将重点更多地放在为什么( why)而不是如何( how)上。理解为什么要做出某个决策比了解其工作原理重要得多。大多数架构师和开发人员可以通过查看上下文图来确定事物的工作方式,但看不出为什么要做出某个决策。知道做出决策的原因以及相应的依据有助于更好地理解问题的背景,并避免重构到其他解决方案,进而导致问题。
为了说明这一点,假设几年前原本的架构决策是使用 Google 的远程过程调用( gRPC) 作为两种服务之间的通信方式。几年后,另一位架构师在不理解为何做出该决策的情况下,选择取代该决策,并使用消息传递以便更好地分离服务。但是,这样的重构使得延迟突然急剧增加,进而导致上游系统发生超时。如果了解使用 gRPC 的初衷是显著减少延迟(以紧密耦合的服务为代价),就能从一开始防止这样的重构发生。
后果
后果是 ADR 另一个非常强大的部分。它记录了架构决策的总体影响。架构师做出的每个架构决策都会产生某种影响,无论好坏。必须明确架构决策的影响迫使架构师去思考这些影响是否超过了决策带来的收益。
后果的另一个好的用法是记录与架构决策相关的权衡分析。这些权衡可以是基于成本的,也可以是与其他架构特征之间的权衡。例如,假设一个决策说:使用异步(即发即弃)消息传递来将评论发布到网站上。制定该决策的合理性在于,它可将评论请求的响应时间从 3100 毫秒降低到 25 毫秒,因为用户不再需要等待评论的发布完成(仅需要将消息发送到队列中)。尽管这是一个很好的理由,但考虑到与异步请求相关的错误处理的复杂性(“如果有人发表带有一些不良词汇的评论该怎么办?”),其他人可能会认为这是一个馊主意,从而对决策提出质疑。对此决策提出质疑的人不知道,这个问题已经在业务利益相关者和其他架构师中间得到了讨论,并且是权衡之后的决定,相比较于等同步完成之后再告诉用户评论已发布,在处理复杂错误的代价下提高响应能力更为重要。通过利用 ADR,可以在“后果”部分中加入权衡分析,从而全面了解架构决策的背景
(以及相关权衡),从而避免出现这类情况。
合规性
合规性不是 ADR 的标准部分之一,但我们强烈建议添加。合规性迫使架构师考虑如何从合规性角度度量和治理架构决策。架构师必须决定决策的合规性检查是手动进行,还是可以使用适应度函数来自动进行检查。如果可以使用适应度函数进行自动化检查,那么架构师可以在本部分中指明如何编写这个适应度函数,以及是否需要对代码库进行修改,以便对此架构决策的合规性进行度量。
例如,在图 19-2 所示的传统 n 层分层架构中,有这样一个架构决策:“在业务层中,业务对象所使用的所有共享对象都将被放在共享服务层中,以隔离和保留共享功能。”
图 19-2:架构决策示例
可以使用 Java 中的 ArchUnit 或 C #中的 NetArchTest,来自动度量和治理此架构决策。例如使用 Java 中的 ArchUnit,自动化的适应度函数测试可能如下所示:
请注意,此自动化的适应度函数需要添加新的用户故事来创建新的 Java 注解(@Shared- Service),并将该注解添加到所有共享类当中。合规性部分还会指定相关测试是什么、可以在哪里找到、如何执行以及何时进行。
备注
备注是另一个不属于标准 ADR 的部分,但我们强烈建议添加。它包含了有关该 ADR 的各种元数据,例如:
- 原作者
- 通过时间
- 通过人
- 取代时间
- 最近一次修改时间
- 修改人
- 最近一次修改
即使使用版本控制系统(例如 Git)来存储 ADR,除了库所支持的内容外,其他元信息也很有用,因此无论如何存储 ADR 以及在何处存储,我们都建议添加此部分。
ADR 的存储
架构师创建 ADR 后,必须将其存储在某处。无论在哪里存储 ADR,每个架构决策都应具有自己的文件或 wiki 页面。一些架构师喜欢将 ADR 与源代码一起保留在 Git 存储库中, 这样还可以对 ADR 进行版本控制和跟踪。但是,对于大型组织,出于以下几个原因,我们不能这样做。首先,每个需要查看架构决策的人都有可能无法访问 Git 存储库。其次, 对于应用程序的 Git 代码库之外的上下文(例如,集成架构决策、企业架构决策或在应用程序之间共享的决策)而言,这不是好的存储 ADR 的地方。由于这些原因,我们建议将ADR 存储在 wiki(使用 wiki 模板)中或共享文件服务器上的共享目录中,可通过 wiki 或其他文档软件轻松访问。图 19-3 是一个此类目录结构(或 wiki 页面导航结构)的示例。
图 19-3:用于存储 ADR 的目录结构示例
application 目录中包含了与某些应用程序上下文相关的架构决策,该目录可细分为更多的目录。common 目录用于存放适用于所有应用程序的架构决策,例如“所有与框架相关的类都将包含一个注解( Java 中为 @Framework)或属性( C #中为 [Framework]),以将该类标识为基础框架代码的一部分。application 目录下的子目录与特定应用或系统上下文相对应,包含特定于该应用或系统的架构决策(即本例中的 ATP 和 PSTD 应用)。integration 目录包含与应用、系统或服务之间通信相关的 ADR。企业架构 ADR 包含在 enterprise 目录中,表明这是影响所有系统和应用的全局架构决策。举一个企业架构 ADR 的例子:“所有对系统数据库的访问,只能由拥有该数据库的系统进行”,这条 ADR 是为了防止多个系统共享数据库。
当在 wiki(我们的建议)中存储 ADR 时,前面描述的结构同样适用,每个目录结构都表示一个导航页面。每个 ADR 都被表示为导航页面( Application、Integration 或Enterprise)中的单个 wiki 页面。
本节所示的目录或页面名称仅作为建议。只要能够在团队中保持一致,每个公司都可以选择适合其情况的任何名称。
以 ADR 为文档
记录软件架构一直是一个难题。尽管现在出现了一些绘制架构的标准(例如,软件架构师 Simon Brown 的 C4 模型或 The Open Group ArchiMate 标准),但对于记录软件架构却还没有标准。这就是 ADR 可以帮忙的地方。
ADR 是一个记录软件架构的有效手段。ADR 的“背景”部分可以极好地描述系统中的某个特定部分,也就是需要做出架构决策的部分。“背景”部分还提供了描述替代方案的机会。更重要的是,“决策”部分描述了做出特定决策的原因,而这也是迄今为止软件架构文档的最佳形式。通过描述架构决策的其他方面(比如舍弃可扩展性、选择性能
的原因分析),“后果”部分使得软件架构文档更加完善。
用 ADR 描述标准
很少有人喜欢标准。多数时候,标准似乎被用于控制人及人们如何做事,除此之外别无他用。用 ADR 描述标准可以改变这种不良做法。例如,ADR 的“背景”部分描述了强制执行特定标准的场合。ADR 的“决策”部分不仅可以用来说明标准是什么,而且更重要的是说明了为什么需要该标准。这是能够确定一个标准是否应该存在的绝妙方法。如果架构师无法证明一个标准的合理性,那么制定和执行该标准可能并不好。此外,开发人员越明白为什么需要某个标准,他们遵循该标准的可能性就越大(因此也就不会挑战它)。ADR 的“后果”部分是架构师判断一个标准是否有效以及是否应该被制定的另一个好地方。在“后果”中,架构师必须考虑并记录他们正在制定的特定标准的含义和后果。通过分析后果,架构师可能会决定放弃推行该标准。
范例
本书 7.2.1 节中存在许多架构决策。使用事件驱动的微服务、拆分出价者和拍卖者用户界面、在视频捕获中采用实时传输协议( RTP)、使用单个 API 层、使用发布和订阅消息传递,这些只是为这个拍卖系统做出的数十种架构决策中的几个。无论多么的显而易见,系统中的每个架构决策都应记录在案并证明其合理性。
图 19-4 展示了 GGG 拍卖系统中的一个架构决策,即在出价捕获、出价传输和出价跟踪服务之间使用发布和订阅(pub/sub)消息传递。
图 19-4:服务间使用发布和订阅模式
该架构决策的 ADR 可能如图 19-5 所示。
图 19-5:ADR 76. 出价服务之间采用异步发布和订阅消息传递机制
本文摘选自《软件架构:架构模式、特征及实践指南》
本书是美亚广泛好评的英文原书《Fundamentals of Software Architecture》的中文版,是畅销书《卓有成效的程序员》作者Neal Ford的全新力作,NETSTARS CTO 陈斌等资深架构师鼎力推荐。本书全面概述了软件架构的方方面面,涉及架构特征、架构模式、组件识别、图表化和展示架构、演进架构,以及其他许多主题。