image.png
《软件架构基础(Fundamentals of Software Architecture)》被誉为和《设计数据密集型应用》一样经典的后端书籍,架构师的入门指南。

第1 章:架构师的 8 大核心能力

本篇为该书第一章的读书笔记,2021 年第一个目标就是和大家一起读完这本书。

如今,全球范围内“架构师”这一头衔炒得十分火热,但没有真正的指南来帮助开发人员成为软件架构师。
这本书主要有三部分内容:基础、架构风格、技术和软技能。基础部分是关于软件体系结构的一般概念;架构风格部分介绍了不同的架构风格,并以一些架构特征标准进行评价;技术和软技能部分涵盖了很多好的概念,包括做出健康的架构决策、风险分析技术、演讲能力、管理团队关系、谈判、架构师职业规划等。
架构就是关于重要的东西……不管那是什么。—— Ralph Johnson

1. 什么是软件架构

学习架构就像学习艺术一样,读者必须要在特定的背景下去理解它。
在一个动态系统中,不存在一劳永逸的解决方案。
学习架构时,必须放在上下文中理解。架构师做的许多决定都是基于他们所处的实际情况。
例如,在 20 世纪末的主要目标是有效地利用共享资源,因为当时所有的基础设施都是昂贵的商业化产品:操作系统、服务器和数据库等等。如果你在 2002 年告诉主管,“我有一个革命性的架构好主意,每个服务都运行在自己隔离的机器上,有自己的专用数据库……(即描述今天的微服务架构)所以,我需要 50 个 Windows 的许可证,另外 30 个服务器许可证,以及至少 50 个数据库许可证。”在 2002 年想构建这样的微服务架构成本之高难以想象。然而这几年,随着开源运动的兴起,以及 DevOps 的出现,我们可以合理地构建一个如上所述的架构。
整个行业都在努力精确定义“软件架构”,有些称为系统的蓝图,有些定义为开发的路线图。本书关于架构的定义主要从四个方面:

  • 系统的结构(Structure)
  • 系统所支持的架构特性、能力(Architecture characteristics)
  • 架构决策(Architecture decisions)
  • 设计原则(Design principles)

    系统的结构指的是系统实现架构风格的类型(如微服务、分层或微内核)。但仅仅通过结构来描述一个架构,并不能完全阐明一个架构。

    image.png

    架构特性多以 “-ility“ 结尾(例如 Availability、Scalability)

    image.png

    架构决策定义了系统应该如何构建的规则。例如,架构师可能会做出一个架构决策,即在分层架构中只有业务层和服务层可以访问数据库(见图),限制表现层直接调用数据库。架构决策形成了系统的约束,并指导开发团队什么是允许的,什么是不允许的。

    image.png

    设计原则与架构决策的不同之处在于,设计原则是一个指导方针,而不是一个硬性规定。例如,图示的设计原则指出,开发团队应该在微服务架构中的服务之间传递异步消息来提高性能。一个架构决策永远不可能涵盖服务之间通信的每一个条件和选项,设计原则为首选方法(在本例中,异步消息传递)提供指导,允许开发人员在特定情况下选择更合适的通信协议(如 REST 或 gRPC)。

    image.png

    2. 对架构师的 8 个期待

    2.1 做出架构决定

    架构师应确定架构和设计原则,用于指导团队、部门或整个企业的技术决策。
    架构师应该指导而不是指定技术选择。例如,架构师应该决定开发团队使用基于响应式 (Reactive) 的框架进行开发,从而指导开发团队在 Angular、Elm、React.js、Vue 或其他任何基于响应式的 Web 框架之间做出选择。
    架构师偶尔需要做出特定的技术决策,以保留特定的架构特性,如可扩展性、性能或可用性。
    架构师经常为找到正确的界线而苦恼。

    2.2 持续分析架构

    多数架构都会经历结构性衰变,当开发人员进行编码或设计变更时,会影响到所需的架构特性,如性能、可用性和可扩展性。架构师需要评估三年或更长时间前定义的架构在今天的可行性。
    另一方面,架构师还常常忘记测试和发布环境。敏捷性有很大的好处,如果团队需要几周的时间来测试变更,而发布又需要几个月的时间,那么架构师就无法实现整体架构的敏捷性。

    2.3 紧跟最新趋势

    架构师要跟上最新的技术和行业趋势。
    开发人员必须掌握他们每天使用的最新技术,以保持代码能力(并保住一份工作!)。架构师有一个更关键的要求,就是要掌握最新的技术和行业趋势。架构师所做的决定往往是长期的,难以改变的,了解和跟踪关键趋势有助于架构师为未来做好准备,做出正确的决定。

    2.4 确保遵守各项决定

    架构师要不断验证开发团队是否遵循架构师定义、记录和传达的架构决策和设计原则。
    考虑这样的场景:架构师做出一个决定,在分层架构中限制对数据库的访问,只限于业务层和服务层(而不是表现层)。这意味着表现层必须经过架构的所有层才能进行最简单的数据库调用。用户界面开发人员可能不同意这个决定,出于性能考虑而直接访问数据库。然而,架构师做出这个架构决策是有特定原因的:控制变化。通过保持各层独立,可以在不影响表现层的情况下进行数据库变更。如果不确保架构决策的合规性,就会发生类似这样的违规行为,架构将无法满足所需的架构特性,应用程序或系统将无法按预期工作。

    2.5 多样化的接触和经验

    架构师要接触多种多样的技术、框架、平台和环境。
    现在的大多数环境都是异构的,一个架构师至少应该知道如何与多个系统和服务对接,不管这些系统或服务是用什么语言、平台和技术编写的。
    最好方法之一是让架构师延伸自己的舒适区,只关注单一技术或平台是一个安全的避风港,一个好的软件架构师应该积极寻找机会,以获得多种语言、平台和技术的经验,关注技术广度而不只是技术深度。

    2.6 具备业务领域知识

    一个架构师要有一定的业务领域专业知识。
    有效的软件架构师不仅了解技术,还了解业务问题。如果没有业务领域的知识,就很难理解业务问题、目标和需求,很难设计出满足业务需求的有效架构。
    最成功的架构师是那些拥有广泛的、实践性的技术知识,再加上对某一特定领域的深刻了解的人。这些软件架构师能够使用这些利益相关者所能理解的领域知识和语言,与主管和业务用户进行有效的沟通。

    2.7 具备人际交往能力

    架构师应具备卓越的人际交往能力,包括团队合作、促进和领导能力。
    拥有卓越的领导力和人际交往能力是大多数开发人员和架构师难以企及的期望。作为技术专家,开发人员和架构师喜欢解决技术问题,而不是人的问题。
    架构师不仅要为团队提供技术指导,还要带领开发团队完成架构的实施。无论架构师的角色和头衔是什么,领导能力是成为一个软件架构师不可或缺的。

    2.8 理解和驾驭企业政治

    架构师要了解企业的政治氛围,并能驾驭政治。
    在一本关于软件架构的书中谈论谈判和驾驭办公室政治,可能看起来比较奇怪。主要的一点是,几乎架构师的每一个决策都会受到挑战。由于涉及到成本或工作量(时间)的增加,架构决策会受到产品、项目经理、开发和业务利益相关者的挑战,因为他们觉得自己的方法更好。无论在哪种情况下,架构师都必须驾驭公司的政治,并应用基本的谈判技巧来获得批准。
    架构师不像开发,可以自行设计代码结构、类、设计模式甚至是语言而不需要批准,架构师做出广泛而重要的决策,必须为几乎每一个决策进行论证和争取。

    3. 架构师和其它的交集

    3.1 工程实践

    将软件开发过程与软件工程实践分开是有益的。所谓过程,我们指的是如何组建和管理团队,如何进行会议以及工作流组织,指的是人们如何组织和互动的机制。而软件工程实践则是指那些已经说明了的、可重复效益的与过程无关的实践。例如,持续集成是一种经过验证的工程实践,它不依赖于特定的过程。
    注重工程实践很重要:

  • 首先,软件开发缺乏许多比较成熟的工程学科的特点。例如,土木工程可以比软件工程更准确地预测结构变化。

  • 其次,软件开发的一个致命弱点是估算—多少时间,多少资源,多少钱?这种困难一部分在于陈旧的会计无法适应软件开发的探索性;但另一部分是因为我们传统上不擅长估算,部分原因是因为 unknown unknowns。

unknown unknowns 是软件系统的克星:没有人知道会出现的东西,却又意外地出现了。例如:某个意外的 bug 出现。
所有的架构都会因为 unknown unknowns 而变成迭代式的,敏捷只是认识到了这一点,并且更早去做了。(All architectures become iterative because of unknown unknowns, Agile just recognizes this and does it sooner.)
迭代流程更符合软件架构的本质,试图使用像瀑布这样的陈旧流程来构建微服务这样的现代系统的团队会发现,一个陈旧的流程忽视了软件如何结合在一起的现实,会产生大量的摩擦。
image.png
如图所示,软件系统的架构由需求和所有其他架构特征组成
采用敏捷工程实践,如持续集成、自动机器供应和类似的实践,使构建弹性架构变得更容易。这也说明了架构与工程实践是如何相互交织在一起的。

3.2 运维/DevOps

架构和相关领域之间最近最明显的交集发生在 DevOps 的出现。许多公司认为运维是与软件开发是分离的,在 20 世纪 90 年代和 2000 年代设计的架构都假设架构师无法控制运维,架构师们被迫围绕引入的限制进行防御性设计。因此,他们构建了能够在内部处理规模、性能、弹性和其他一系列能力的架构。这种设计的副作用是大大增加了架构的复杂性。
微服务风格架构的构建者们意识到,通过在架构和运维之间建立一个联络点,架构师可以简化设计,依靠运维来处理他们最擅长的事情。因此,意识到资源的挪用导致了意外的复杂性,架构师和运维合作创建了微服务。

3.3 流程

软件架构与软件开发过程大多是正交的(相互不可替代的),大多数关于软件架构的书籍都忽略了软件开发过程。例如,在过去的几十年里,由于软件的性质,许多公司都采用了敏捷开发方法。架构师在敏捷项目中得到更快的反馈,这使得架构师可以更积极地进行实验。
所有的架构都会变成迭代式的,这只是时间问题。为此,我们要在整个过程中假设敏捷方法论的基线,并允许适当的例外。例如,许多单体架构因为年龄、政治或其他与软件无关的因素而使用旧流程的情况还是很常见的。

3.4 数据

很大一部分严肃的应用程序开发包括外部数据存储,通常采用关系型(或越来越多的 NoSQL)数据库的形式。然而,许多关于软件架构的书籍只对架构的这一重要方面进行了轻描淡写的处理。代码和数据具有共生关系:两者缺一不可。

4. 软件架构法则

软件架构第一定律:
软件架构中的所有东西都是一种权衡。(Everything in software architecture is a trade-off.)
我们对软件架构的定义超越了结构的范畴,包含了原则、特性等,架构的范围比单纯的结构更广,体现在我们的软件架构第二定律中:
为什么比怎么做更重要。(Why is more important than how.)

image.png

第2章: 架构思维

架构思维指用架构的眼光和观点来看待事物,主要包括:

  • 理解软件架构和软件设计的区别,知道与开发团队合作,并让架构发挥作用。
  • 拥有技术广度的同时,保持一定的技术深度,看到别人看不到的解决方案和可能性。
  • 理解、分析、协调各种解决方案和技术之间的权衡。
  • 理解业务驱动的重要性,以及如何将其转化为架构。

    架构与设计

    软件架构和软件设计的区别往往是一个令人困惑的问题。架构在哪里结束,设计在哪里开始?架构师与开发人员的职责分别是什么?
    下图是传统的架构师和开发的关系,架构师分析业务需求,提取和定义架构特性,选择架构风格,以及创建组件,然后交给开发团队,开发团队为每个组件创建类、用户界面,以及开发和测试源码。

image.png
这样的架构很少能够成功,因为单箭头穿过架构师和开发人员,意味着架构师的想法和决定很少传到开发团队,而开发团队碰到的架构问题也很少回到架构师那里———这样的模式是脱节的。
想要架构发挥作用,必须打破架构师和开发人员之间的物理和虚拟的障碍,形成一种双向关系。这种模式不仅有利于架构师和开发之间的双向沟通,而且可以让架构师为团队中的开发人员提供指导和辅导。
下图展示了一种双向沟通关系。
image.png
与老派的瀑布流程静态且僵化的软件架构不同,当今系统的架构在项目的每一次迭代都会发生变化和发展,架构师和开发团队之间的紧密合作是任何软件项目成功的关键。

技术广度

如果说开发人员必须具备很好的技术深度才能完成工作,那么架构师必须具备大量的技术广度来思考问题。
下图代表知识金字塔,包括三类知识:

  • 你知道的东西(Stuff you know):日常工作中使用的技术、框架、语言和工具;
  • 你知道你不知道的东西(Stuff you know you don’t know):略知一二但没有深入理解或没有专业知识的东西,例如:你可能听过 Clojure,但是不知道怎么使用这种语言进行编码;
  • 你不知道你不知道的东西(Stuff you don’t know you don’t know):你不知道这些东西的存在,是知识三角中最大的一部分;

image.png
对开发人员来说,最重要的是顶部的部分。
随着开发人员向架构师角色的过渡,知识的性质发生了变化。架构师的很大一部分价值如何使用技术解决特定问题。例如,作为一名架构师,知道针对特定问题存在五种解决方案比只在一种解决方案上拥有单一的专业知识更有利。
对架构师来说,金字塔最重要的部分是顶部和中间部分。
image.png
对于架构师来说,明智的做法是牺牲一些很难学到(hard-won)的专业知识,利用这段时间来拓宽自己的广度。这也是一种取舍。
如下图,“你知道的东西”变小了,只保留一些技术深度(渗透下来的绿色),用来换取技术广度。
image.png
知识金字塔说明了架构师和开发人员的不同。
开发人员用他们的整个职业生涯来磨练专业知识,过渡到架构师的角色意味着这种观点的转变。这对许多人来说很难,常导致两个问题:

  • 试图在每个领域都保持专业性,导致任何一个领域都不成功,把工作做得很粗糙;
  • 错误地以为自己陈旧的知识仍然是前沿的,经常在大公司看到这种情况;

向架构师角色过渡的开发人员必须改变他们看待知识获取的方式,平衡关于深度与广度的知识组合是每个开发人员在整个职业生涯中应该考虑的问题。

分析权衡

架构就是你没法用 Google 搜索的东西。
架构中的一切都是权衡。
每个架构问题的答案都包含了“这取决于……”,你没法在 Google 搜索是 REST 还是消息传递更好,是微服务好还是单体架构更好?因为它确实取决于,取决于部署环境、业务、公司文化、预算、时间、开发人员的技能组合以及其他几十个因素。
架构之所以这么难,因为每个人的环境、情况和问题都不一样。
在架构中没有正确或错误的答案,只有权衡。
考虑一个拍卖系统,有以下两种数据消费模式。

image.png
图 6: 发布-订阅(pub-sub)消息传递。

image.png
图 7:队列,点对点消息传递。
发布-订阅模型的优势:

  • 假如我们要增加一个“竞价历史”新服务,则完全不需要对现有系统进行任何修改;而在队列模型中,我们可能需要修改生产者添加一个队列;
  • 解耦:生产者不需要知道数据有哪些服务在使用、如何去使用;而在队列模型中,生产者需要知道是什么类型的数据,发送给谁。

架构师的思维需要看到方案的好处和坏处。队列模型的优势:

  • 任何人都能访问发布-订阅模型的数据,存在数据访问和安全问题。
  • 发布-订阅模型只能接受相同格式的数据,假设新的“竞价历史”服务需要当前的售价以及竞价,但其他服务原本没有这些信息,在这种情况下需要修改数据格式,并且会影响使用该数据的所有其他服务。在队列模型中,这将是一个单独的通道,因此是一个单独的格式,不影响任何其他服务。
  • 发布-订阅模型不支持监控某个主题的消息数量,导致不支持自动缩放。在队列中很容易知道哪个队列消息量大,独立地自动伸缩。请注意,这种权衡是特定于技术的,高级消息队列协议(Advanced Message Queuing Protocol,AMQP)可以支持负载均衡和监控。

鉴于这种权衡分析,现在哪个是更好的选择?答案是什么呢? 这就要看情况了!

理解业务

架构思维就是要理解系统成功所需的业务因素,并将这些需求转化为架构特性(如可扩展性、性能和可用性)。
这是一项具有挑战性的任务,要求架构师具有一定程度的业务领域知识,并与关键业务利益相关者建立健康的协作关系。

平衡架构和编码

架构师面临的困难任务之一是如何平衡架构和编码。
每个架构师都应该进行编码,并且能够保持一定的技术深度(参见“技术广度”)。虽然这看起来似乎是一个简单的任务,但有时却相当难以完成。
架构师需要避免瓶颈陷阱。当架构师掌握项目关键路径内的代码(通常是底层框架代码)的所有权,并成为团队的瓶颈时,就会出现瓶颈陷阱。架构师不是全职开发人员,需要在开发人员(编写和测试代码)和架构师(画图、参加会议,以及参加更多的会议)之间取得平衡。
避免瓶颈陷阱的方法:将关键路径和框架代码委托给开发团队中的其他人,然后集中精力在一到三次迭代后对一个业务功能进行编码。
架构师如何才能保持亲力亲为并保持一定的技术深度呢?有四种基本方法可以让架构师在工作中练习,而不必“在家练习编码”(尽管也建议在家里练习编码):

  • 经常做 POC(proof-of-concept),通过考虑实现细节来验证架构决策。例如,如果架构师在两种缓存解决方案中无法抉择,那么可以每种缓存开发一个实例,并进行对比。
  • 处理一些技术债务或架构问题,让开发团队腾出时间来处理关键的功能开发。这些问题通常是低优先级的,一般不会影响迭代。还可以在迭代中修复 bug,在帮助开发团队的同时也保持了编码,还可以找出代码库可能存在的问题和弱点。
  • 通过创建简单的命令行工具和分析工具来帮助开发团队自动化完成日常任务,寻找开发团队执行的重复性任务,并将这个过程自动化。开发团队会感谢自动化。
  • 经常做 code review。虽然并不是实际写代码,但至少参与了源代码的编写。此外,做 code review 还能确保代码符合架构的要求,帮助和指导开发

    image.png

    第3章: 什么是好的模块化代码?高内聚、低耦合如何衡量?

0. 写在前面
什么是好的代码?好的代码应该模块化。
王垠在其《编程的智慧》中也提到,要“写模块化的代码”。(不对人做评价,这篇文章写得是非常好的。)
如果你读过《代码大全》和《代码整洁之道》等书,一定对“高内聚、低耦合”不陌生。
好的模块化代码就是要高内聚、低耦合。
事实上,内聚和耦合是 1972 年就提出的概念,由于耦合不好具体的衡量,Meilir Page-Jones 在 1992 年提出了共生性(Connascence)。本章重点就是介绍如何评估模块化架构,以及引入共生性这一概念来帮助更好的模块化。

1. 模块化

不同的平台、语言为代码提供了不同的复用机制,将相关代码组合成模块。
理解模块对于架构师来说非常重要,因为用来分析架构的工具(可视化等)常常都依赖于模块化的概念。如果一个架构师在设计一个系统时,没有注意到各个部分是如何连接在一起的,那么他们最终创建的系统会带来无数的问题。
架构师必须保持良好的结构,这不会偶然发生。
模块的代码到底是什么?我们用模块化来描述相关代码中的逻辑分组,这些模块可以用来构造一个更复杂的结构。
现代的语言有各种各样的封装机制,例如,许多语言可以在函数/方法、类、包/命名空间中定义行为,每个包都有不同的可见性和范围规则。(这有时候也会让开发人员选择困难)
架构师必须意识到开发者是如何组织包的,如果几个包紧密的耦合在一起,那么重用其中一个包就变得非常困难。
鉴于模块化的重要性,研究人员提供了各种语言无关的标准来衡量,我们专注于三个关键概念:

  • 内聚(Cohesion)
  • 耦合(Coupling)
  • 共生性(Connascence)(注:参考《UML面向对象设计基础》的翻译)

    2. 内聚(Cohesion)

    内聚性是指子程序中各种操作之间联系的紧密程度,我们的目标是让每一个模块只做好一件事,不去做其他事情。
    试图分割一个内聚的模块只会导致耦合性增加和可读性降低。(Attempting to divide a cohesive module would only result in increased coupling and decreased readability.) —— Larry Constantine
    image.png
    图来自 wikipedia
    计算机科学家们已经定义了一系列的内聚的衡量标准,从最好到最坏列出如下:

  • 功能性内聚(Functional cohesion):模块内所有元素都为完成同一个功能而存在,共同完成一个单一的功能,模块已不可再分,具有最高的内聚;

  • 顺序内聚(Sequential cohesion):模块必须顺序执行;
  • 通信内聚(Communicational cohesion):两个不同操作的模块使用同样的数据。例如,在数据库中添加一条记录,并根据该信息生成一封邮件;
  • 过程内聚(Procedural cohesion):两个模块必须以特定的次序执行。
  • 时间内聚(Temporal cohesion):把需要同时执行的动作组合在一起形成的模块。
  • 逻辑内聚(Logical cohesion):这种模块把几种相关的功能组合在一起, 每次被调用时,由传送给模块参数来确定该模块应完成哪一种功能。
  • 巧合内聚(Coincidental cohesion):模块内的各个元素之间没有任何联系,只是偶然地被凑到一起;内聚程度最低。

image.png
内聚不容易考量,特定的模块需要架构师来具体决定,例如,考虑一个模块定义了:
Customer:

  • add customer
  • update customer
  • get customer
  • notify customer
  • get customer orders
  • cancel customer orders

或者说将后两个函数剥离出来,分成两个模块:
Customer:

  • add customer
  • update customer
  • get customer
  • notify customer

Order:

  • get customer orders
  • cancel customer orders

哪个更好?一如既往,这要看情况:

  • 订单只有这两个操作吗?如果是这样,将这些操作放在客户包中维护可能是有意义的;
  • 客户包按预期是否会变得更大?
  • 订单是否需要如此多的客户信息?

这些问题代表了软件架构师工作核心的权衡分析。
由于内聚非常主观,计算机科学家制定了一个标准来衡量内聚性,其中 LCOM(Lack of Cohesion in Methods) 为著名。这里涉及到的数学公式平时很少用到,在此不再展开,只需要知道有这么一个公式,在需要的时候可以再查询拿出来用。想进一步了解的读者可以查看:https://en.wikipedia.org/wiki/Programming_complexity

3. 耦合(Coupling)

我们常常谈到要“解耦”,弱耦合是系统可维护的关键。
耦合其实也有多种类型,但在此不再介绍,因为它们已经被共生性(Connascence)所取代。

4. 共生性(Connascence)

1996 年 Meilir Page-Jones 发表了
《What Every Programmer Should Know About Object-Oriented Design》,完善了耦合的度量,并命名为:Connascence。
他是这样定义的:
如果一个组件的改变会要求另一个组件进行修改,才能保持系统的整体正确性,那么这两个组件就是共生的。 —— Meilir Page-Jones
共生性分为静态的和动态的。我们将分别介绍各种类型的共生性,对于部分重要的、不易理解的,我将补充一些代码案例,作为具体的参考来帮助理解。
静态共生性:

4.1 名称共生性(Connascence of Name, CoN)

methodA() 改名为 methodB() 时, 调用 methodA() 的地方都要改名,这是代码库中最常见的耦合方式,现代的 IDE 的检索功能使修改代码的名称变得很容易,这是最理想的耦合方式;
image.png
4.2 类型共生性(Connascence of Type, CoT)
如果一个变量从值 100 变成了一个很大的数,变量的类型可能要从 int 改成 BigInteger

4.3 意义共生性(Connascence of Meaning, CoM)

例如,在很多语言中,通常会把大于 0 的数字认为是 True,0 认为是 False。下面是 Java 中的一个具体例子:
a.compareTo(b) // 如果 a = b,则返回值 0; // 如果 a > b,则返回大于 0 的值; // 如果 a < b,则返回小于 0 的值。

4.4 位置共生性(Connascence of Position, CoP)

函数的参数的位置顺序或个数耦合,例如下面的函数增加一个参数后,函数调用将会出错。

image.png

image.png

针对这个例子,我们可以通过下面的办法,将位置共生性转为名称共生性来降低耦合性:
class User { FirstName, LastName, Address } void SaveUser(User); myrepo.SaveUser(new User{ FirstName = “bob”, LastName = “Marley”, Address = “Jamaica”});

4.5 算法共生性(Connascence of Algorithm, CoA)

多个组件必须就一个特定的算法达成一致。例如:客户端和服务端用相同的算法验证用户身份。这代表一种较高的耦合形式——如果算法细节改变,验证将不再有效。


动态共生性:

4.6 执行共生性(Connascence of Execution, CoE)

代码的执行顺序上的耦合。例如下面的代码,在设置主题之前就发送了,明显在顺序上有问题。
email = new Email(); email.setRecipient(“foo@example.com”); email.setSender(“me@me.com”); email.send(); email.setSubject(“whoops”);

4.7 时间共生性(Connascence of Timing, CoT)

常见情况是两个线程同时执行造成的竞赛条件。
这里我们可以看一个有趣的例子,发生在 bootstrap 的一个 issue:https://github.com/twbs/bootstrap/issues/3902
// using bootstrap modal $(element).modal(‘hide’) $(element).modal(‘show’) // Error! // 隐藏一个 modal 大约需要 500ms 的动画, // 如果你在这时候直接调用了 ‘show’,将会发生异常 // 我们必须这样做 $(element).modal(‘hide’) $(element).on(‘hidden.bs.modal’, ()=>{ $(element).modal(‘show’) // ok })

4.8 值共生性(Connascence of Values, CoV)

常见的情况在分布式事务中,例如需要在多个独立的数据库中做分布式事务。

4.9 身份共生性(Connascence of Identity, CoI)

两个独立的模块需要共享和更新同一个数据结构,例如:分布式队列。

5. 共生性的属性

5.1 强度(Strength)

Page-Jones 指出,共生性有明确的强弱谱系,如下图所示,按强度递增排序。identity 具有最强的共生性,name 具有最弱的共生性。——也就是说用 name 的方式耦合则为最弱的耦合方式。
image.png
架构师应该倾向于静态共生性而不是动态共生性,因为开发人员可以通过现代的 IDE 来很快地确定它。

5.2 局部性(Locality)

局部性指两个模块的之间的远近程度。
通常情况下,在同一模块中、距离较近的类比在不同模块中、距离距离较远的类具有更高的共生性。换句话说,随着两个模块在代码中的距离增加,共生性会减弱。

5.3 程度(Degree)

共生性的程度与模块的影响大小有关——它影响了几个类还是几十个类?影响较小的共生性对代码库的损坏就较小。

6. 如何通过共生性来提高系统模块化

讲了这么多,我们到底如何实践共生性呢?
Page-Jones 提供了三个使用共生性来提高系统模块化的指南:
1.通过将系统拆分成封装的元素,使得整体的共生性达到最弱
2.最大限度地减少任何跨越封装边界的共生性
3.最大限度地提高封装边界的共生性
Jim Weirich (传奇的软件架构创新者,Ruby 社区活跃人士)简化了上面较为抽象的指导,提供了两个更具体的建议:

  • 程度法则(Rule of Degree):将强共生性转化为弱共生性。
  • 局部性规则(Rule of Locality):随着软件元素之间距离的增加,应使用较弱的共生性。

    7. 耦合性和共生性

    从架构师的角度来看,耦合和共生是有所重叠的,这是不同时代的产物,下图列出两者重叠的部分:
    image.png
    共生性提供了更精细化的考量,例如左边的数据耦合,在右边的静态共生性提供了更具体的建议。

    8. 局限性

    尽管如此,架构师在应用这些指标来分析和设计系统时,存在几个问题:

  • 这些度量从代码层面考察细节,关注代码质量,而不一定是架构。架构师更关注模块如何耦合,而不是耦合程度,例如,架构师关心的是同步或异步通信,而不关心如何实现。

  • 共生性并没有真正解决许多现代架构师必须做出的一个基本决定—在分布式架构(例如:微服务)中,使用同步还是异步通信?在后面会介绍新的方法来思考现代的共生性。

虽然对模块化进行了大量的介绍和思考,开发人员和架构师在实际实施过程中,还是会遇到很多的困难。
纸上得来终觉浅,绝知此事要躬行。
设计良好的架构,并非易事!

image.png

第4章: 你的架构需要考虑的架构特性

一个公司决定用软件解决一个特定的问题,公司会收集该系统的需求清单。需求可以说是软件开发的基础,但除了需求以外,架构师有很多因素需要去考虑。
下图在第 1 章出现过:
image.png
架构师可能会参与收集需求,但架构师一个关键的职责是要发现、定义其它和具体需求没有直接关系的东西,这些东西称之为架构特性(architectural characteristics)。
一个架构特性满足三个标准:

  • 指定了一个非领域(业务)设计的考虑因素:例如,一个重要的架构特性是应用程序的性能水平,而这一点往往不会出现在需求文档中
  • 影响结构上的设计:这个架构特性是否需要特殊的结构才能成功?例如,考虑要支持支付功能的系统,可以选择第三方支付(只需要符合安全即可嵌入),还是应用自己处理支付(这需要设计特定的支付模块),这将导致不同的架构设计;
  • 对应用的成功至关重要:应用程序可以支持大量的架构特性,但每个架构特性的支持都会增加设计的复杂性,因此,架构师的一项工作是选择最少的架构特性

image.png

架构特性主要分为显性的和隐性的。
隐性的很少出现在需求中,但它们却是项目成功的必要条件。例如,可用性、可靠性和安全性几乎是所有应用的基础,然而它们很少在设计文档中被提及。
架构师必须在分析阶段利用他们的知识来发现这些架构特性。

架构特性列表

架构特性存在于软件系统的广泛范围内,从代码特性如模块化,到复杂的问题如可扩展性和弹性。由于软件生态系统变化如此之快,新的概念、术语、措施和验证不断出现,并不存在真正的标准。但是架构师通常将架构特性分为下面几大类:

运行架构特性

运行架构特性涵盖了性能、可扩展性、弹性、可用性和可靠性等能力。

类型 定义
可用性(Availability) 系统可用时间,如果是24/7,则需要使系统在发生任何故障时能够迅速启动和运行
持续性(Continuity) 灾难恢复能力
性能(Performance) 包括压力测试、峰值分析、分析功能的使用频率、所需容量和响应时间。性能报告有时需要自行演练,需要几个月才能完成。
可恢复性(Recoverability) 业务持续性要求(例如,在发生灾难的情况下,系统需要多快才能重新上线?)。这将影响备份策略和对冗余硬件的要求
可靠性/安全性(Reliability/safety) 评估系统是否需要具备某些安全功能,如果发生故障,是否会给公司带来大笔资金损失?
稳健性(Robustness) 在网络连接中断、断电或硬件故障时,系统是否能够处理运行中的错误和边界条件
可扩展性(Scalability) 随着用户或请求数量的增加,系统执行和运行的能力

结构架构特性

架构师也必须关注代码结构。在许多情况下,架构师对代码质量问题负有单独或共同的责任,如良好的模块化、组件之间的可控耦合、可读的代码以及一系列其他内部质量评估。

类型 定义
可配置性(Configurability) 能够轻松地变更软件配置
可扩展性(Extensibility) 添加新的功能是多么的重要
可安装性(Installability) 易于在所有必要的平台上安装
可利用性/重复使用(Leverageability/reuse) 能够重复利用通用组件
本地化(Localization) 在文字输入、显示上支持多种语言;在报表、计量单位、货币上支持各种字符
可维护性(Maintainability) 如何轻松地进行应用变更和系统维护
可移植性(Portability) 系统是否需要运行在超过一个平台上
支持性(Supportability) 应用程序需要什么级别的技术支持?需要什么级别的日志和其他设施来调试系统中的错误?
可升级性(Upgradeability) 能够在服务器和客户端上轻松、快速地升级

交叉架构特性

许多特征不在分类范围内或无法分类,却形成了重要的设计限制和考虑因素。

类型 定义
可访问性(Accessibility) 让您的所有用户,包括色盲或听障等残疾用户都能访问(例如 Iphone 就有类似的支持)
数据是否需要在一段时间后归档或删除?例如,客户账户在三个月后要删除或标记为过时,并归档到二级数据库,以便将来访问
归档性(Archivability) 安全要求,确保用户就是那个人
权限(Authorization) 安全性要求,以确保用户只能访问应用程序中的某些功能
法律要求(Legal) 系统在什么法律约束下运行(数据保护、萨班斯法案、GDPR 等)?公司需要哪些保留权限?关于应用程序的构建或部署方式有什么规定?
隐私(Privacy) 能够对公司内部员工隐藏交易(加密的交易即使是 DBA 和网络架构师也无法看到)
安全性(Security) 数据库中的数据是否需要加密?内部系统之间的网络通信是否需要加密?远程用户访问时需要进行哪种类型的认证?
可用性/可实现性(Usability/achievability) 你的用户使用应用程序需要的培训程度,是否易用?。需要像对待其他架构问题一样认真对待可用性要求。

其它架构特性

ISO 也发布过软件质量的列表,参见:https://iso25000.com/index.php/en/iso-25000-standards/iso-25010
主要分为下图的各大类,在此不再展开。
image.png

任何架构特征列表必然是不完整的,任何软件都可能根据独特的因素发明重要的架构特征。

权衡架构

由于各种原因,应用程序只能支持列出的几个架构特性:

  • 每一个被支持的特性都需要设计上的努力,或许还需要结构上的支持;
  • 更大的问题在于,每个架构特性往往会对其他特性产生影响。例如,如果架构师想要提高安全性,几乎肯定会对性能产生负面影响;

因此,架构师很少会遇到这样的情况:他们能够设计一个系统,并将每一个架构特性最大化。更多的情况是,决定要在几个相互竞争的问题之间进行权衡。
过多的架构特性导致设计变得笨重。
架构师应该努力使架构设计尽可能地迭代。如果能更容易地对架构进行修改,就可以减少在第一次尝试中就设计出完全正确的东西的压力。敏捷软件开发最重要的经验之一就是迭代的价值,这在软件开发的各个层面都适用,包括架构。
(这本书一直在强调,在软件架构和开发中,快速迭代和敏捷的重要性。)
永远不要追求最佳的架构。

image.png

第5章: 架构特性识别与实战: 订餐系统

image.png
多颗糖
公众号:多颗糖
关注他
12 人赞同了该文章
架构师一般从三个方面来发现架构特性:

  • 领域关注(Domain Concerns)
  • 需求
  • 隐性领域知识

在《软件架构基础 4》中我们讨论了隐性领域知识,这里将介绍另外两种。
搬运下 wikipedia 对于领域(Domain)的解释:
A domain is the targeted subject area of a computer program. It is a term used in software engineering. Formally it represents the target subject of a specific programming project, whether narrowly or broadly defined. For example, a particular programming project might have had as a goal the creation of a program for a particular hospital, and that hospital would be the domain.
简而言之,领域就是指程序的目标主题领域,假如为医院开发某个程序,那么医院就是领域。Domain 这个词常常出现在软件工程中,例如领域驱动架构(DDD)、领域模型(Domain model)。

领域关注(Domain Concerns)

架构师必须能够将领域关注的问题转化为确定、正确的架构特性。例如,可扩展性是最重要的关注点,还是容错、安全或性能?也许系统需要所有四个特性的结合。
有一个小窍门,那就是努力让最终的清单尽可能的简短。架构中一个常见的反模式是试图设计一个通用架构,支持所有的架构特性。架构所支持的每一个架构特性都会使整个系统设计变得复杂,支持过多的架构特性会导致架构师和开发人员在还没有开始解决问题之前就变得越来越复杂。
不要纠结于架构特性的数量,而是要保持设计简单的动机。
一开始就考虑系统最终的架构特性列表是愚蠢的,这不仅浪费时间还会产生挫败感和分歧。更好的办法是选出最重要的三个特性(顺序不限)
架构师谈论的是可扩展性、互操作性、容错性、可学习性和可用性,业务利益相关者谈论的是合并和收购、用户满意度、销售时间和竞争优势。两者之间可能互相不理解,下表提供一个领域关注到架构特性的转换。

领域关注 架构特性
兼并和收购 互操作性、可扩展性、适应性、可扩展性
销售时间 敏捷性、可测试性、可部署性
用户满意度 性能、可用性、容错性、可测试性、可部署性、敏捷性、安全性
竞争优势 敏捷性、可测试性、可部署性、可扩展性、可用性、容错性
时间和预算 简单性、可行性

需要注意的一个重要问题是,敏捷性不等于销售时间。相反,销售时间是敏捷性+可测试性+可部署性。这是很多架构师在翻译领域关注点时陷入的陷阱——只关注其中一个点。
例如,一个业务利益相关者可能会说:“由于监管要求,我们必须按时完成基金的日终计价”。一个无效的架构师可能只会关注性能,因为这似乎是该领域关注的主要焦点。然而,该架构师会因为很多原因而失败:

  • 首先,如果系统在需要的时候无法使用,那么系统的速度有多快都不重要;
  • 第二,随着业务的发展和更多资金,系统必须也能扩展到及时完成日终处理;
  • 第三,系统不仅要可用,而且要可靠,以便在计算日终基金价格时不会崩溃;
  • 第四,如果日终基金定价完成了 85%,系统崩溃了怎么办?它必须能够恢复并继续计算。
  • 最后,系统可能很快,但基金价格是否计算正确?

所以,除了性能之外,架构师还必须同样关注可用性、可扩展性、可靠性、可恢复性和可审计性。

从需求提取架构特


一些架构特征来自需求文档中的明确声明。例如,明确的预期用户数和规模。另一些则来自架构师的领域知识,这是领域知识对架构师总是有益的众多原因之一。
例如,假设一个架构师设计了一个为大学生处理班级注册的应用程序,为了便于计算,假设学校有 1000 名学生,注册时间为 10 小时。架构师在设计系统时,是否应该假设注册过程中的学生会随着时间推移均匀分布?或者,基于对大学生习惯和癖好的了解,架构师是否应该设计一个能够处理所有 1000 名学生在最后 10 分钟注册的系统?任何一个了解学生有多么拖延的人都知道这个问题的答案! 像这样的细节很少会出现在需求文档中,但它们确实为设计决策提供了信息。

案例学习:三明治店网上订餐系统

image.png

描述

一家全国性的三明治店想实现网上订餐(除了目前的呼叫服务外)。

用户

数千人,也许有一天会到数百万人。

需求

  • 用户下单,然后会得到一个领取三明治的时间和到店的方向(必须与几个外部地图服务整合,包括交通信息);
  • 如果店家提供送餐服务,就派司机带着三明治给用户送去;
  • 移动设备的可访问性;
  • 提供全国性的每日促销/特价活动;
  • 提供本地每日促销/特价活动;
  • 接受在线、当面或货到付款;

    其它需求

  • 三明治店都是加盟店,每个店都有不同的老板;

  • 母公司近期有海外扩张计划;
  • 企业的目标是雇佣廉价的劳动力,以实现利润最大化”;

鉴于以上这种情况,架构师将如何推导出架构特征?
需求的每一部分都可能有助于架构的一个或多个方面,架构师在这里并不设计整个系统,相反,架构师要寻找影响或冲击设计的东西,特别是结构性的。
首先,将候选架构特性分为显性和隐性特性。

显性架构特性

明确的架构特性作为必要的一部分出现在需求规范中,例如,一个购物网站可能支持的并发用户数量,需求中明确了这一点。架构师应该考虑需求的每个部分,看看它是否有助于架构特性。但首先,架构师应该考虑领域级的关于预期指标的估计。
首先应该引起架构师注意的细节之一是用户数量:目前是数千人,也许有一天会到数百万人(这是一家雄心勃勃的三明治商店!)。因此,可扩展性—处理大量并发用户而不严重降低性能的能力—是架构的首要特性之一。请注意,需求陈述并没有明确要求可扩展性,而是将该要求表述为预期的用户数。架构师必须经常将业务语言解码成工程等价物。
然而,我们可能还需要弹性—处理突发请求的能力。这两个特性经常出现在一起,但它们有不同的限制。
可扩展性看起来像下图所示的图形:
image.png

弹性则是流量的爆发,如下图所示:
image.png

有些系统是可扩展的,但不是弹性的。例如,考虑一个酒店预订系统,如果没有特殊的销售或活动,并发用户数量可能是一致的。相反,考虑一个音乐会门票预订系统,随着新票的发售,狂热的粉丝会涌入网站,这就需要高度的弹性。通常情况下,弹性系统需要可扩展性:处理突发事件和大量并发用户的能力。
弹性的要求并没有出现在需求中,然而架构师应该将其确定为一个重要的考虑因素。一个三明治店的流量是全天一致的吗?还是在用餐时间前后会有爆棚的客流?几乎可以肯定是后者。因此,一个好的架构师应该识别这种潜在的架构特征。
架构师应该依次考虑这些业务需求中的每一个,看看是否存在架构特征。

  1. 用户下单,然后得到一个领取三明治的时间和去商店的方向(必须提供与外部地图服务整合的选项,包括交通信息)。外部地图服务意味着集成,这可能会影响可靠性等方面。例如,如果开发人员构建了一个依赖于第三方系统的系统,然而调用它却失败了,这就会影响调用系统的可靠性。但是,架构师也要警惕架构特性的过度规范,如果外部流量服务出现故障怎么办?三明治网站是否应该失败,或者只是在没有流量信息的情况下提供稍低的效率?
  2. 如果店家提供送餐服务,就派司机带着三明治给用户送去。看起来不需要特殊的架构特性来支持这个需求。
  3. 移动设备的可访问性。这一要求将主要影响到应用程序的设计,要做一个 portable web application 或是几个 native web applications。考虑到预算限制和应用程序的简单性,一个移动端优化的 web 应用更好。因此,架构师还要考虑为页面加载时间和移动端的性能架构特性。注意,架构师在这样的情况下不应该单独行动,而是应该与用户体验设计师、业务利益相关者和其他相关方合作,审核这样的决策。
  4. 提供全国性的每日促销/特价。
  5. 提供本地每日促销/特价活动。这两个要求都规定了促销和特价商品的可定制性。注意,需求 1 还意味着基于地址的可定制信息,基于这三个需求,架构师可以考虑将可定制性作为一种架构特性。例如,微内核架构这样的架构风格,通过定义一个插件架构,可以极好地支持自定义行为。传统的设计也可以通过设计模式(如模板)来满足这种需求。需要架构师权衡取舍。
  6. 接受在线、当面或货到付款。网上支付意味着安全性,但这一要求没有任何内容表明安全程度特别高。 7. 三明治店都是加盟店,每个店都有不同的老板。这个要求可能会对架构造成成本限制—架构师应该检查可行性(应用成本、时间和员工技能培训等约束条件),看看是否需要一个简单性或牺牲性的架构。
  7. 母公司近期有向海外扩张的计划。这个要求意味着国际化,也就是 i18n。许多技术可以处理这一要求,应该不需要特殊的结构来适应。
  8. 企业的目标是雇佣廉价的劳动力以实现利润最大化。这个要求表明可用性将是重要的,但同样是更关注设计而不是架构特点。

还有一个架构特征是性能:没有人愿意从一个性能差的三明治店购买,尤其是在高峰期。然而,性能是一个有着不同差别的概念—架构师应该为什么样的性能而设计?我们将在后续章节中介绍性能的各种细微差别。
我们还希望将性能数字与可扩展性数字结合起来定义。换句话说,我们必须在没有特定规模的情况下建立一个性能基线,并确定在一定数量的用户下,可接受的性能水平是多少。

隐形架构特性

许多没有在需求文档中指定的架构特性却非常重要。
系统可能支持的一个隐含的架构特性是可用性:确保用户可以访问三明治网站。与可用性密切相关的是可靠性:确保网站在交互过程中保持运行—没有人想从一个不断掉线的网站上购买
安全性在每个系统中都是一个隐含的特性:没有人愿意使用不安全的软件。然而,它可能会根据关键性来确定优先级
对于该三明治店的支付应该由第三方处理,因此,只要开发者遵循一般的安全(不将信用卡号码以纯文本的形式传递,不存储太多信息等),架构师应该不需要任何特殊的结构设计来适应安全问题,在应用中做好设计就足够了。
三明治店需要支持的最后一个主要架构特性:可定制性。需求的几个部分提供了自定义行为:食谱、本地销售等。通常这应该属于应用程序的设计,这个设计元素对应用的成功并不关键。
在选择架构特性的过程中,没有正确的答案,只有不正确的答案:
架构没有错误的答案,只有昂贵的答案。
架构师可以设计一个在结构上不适应可定制性的架构,要求应用本身来支持这种行为。架构师不应该过于强调发现完全正确的架构特性集—开发人员可以用各种方式来实现功能。当然,正确识别重要的结构元素可能会促进更简单或更优雅的设计。
架构师还必须优先朝着找到最简单的集合的方向发展。团队在确定架构特性方面一个有用的尝试是找到最不重要的一个特性,如果你必须消除一个,那会是哪个?一般来说,架构师更有可能剔除显性的架构特征,因为许多隐性的特征是一个应用想成功最基本的特性。
在三明治店的案例中,哪个架构特性是最不重要的?(不存在绝对正确的答案)
在这种情况下,解决方案可能会失去可定制性性能。我们可以取消可定制性作为架构特性,并计划将该行为作为应用设计的一部分来实现。性能可能是成功的最不关键因素,当然,开发人员并不是要构建一个性能糟糕的应用,而是要构建一个不将性能优先于其他特性(如可扩展性或可用性)的应用。

第6章: 衡量和治理架构特性

关注
由于软件项目的架构特性异常广泛,性能、弹性、可扩展性等操作方面与模块化、可部署性等结构性问题交织在一起。本章重点是具体定义一些比较常见的架构特性。

衡量架构特性

软件架构特性的定义存在几个常见的问题。

  • 它们不是物理学:很多常用的架构特性都有模糊的含义。例如,架构师如何设计敏捷性或可部署性?业界对常用术语的看法大相径庭,有时是受合法的不同语境驱动,有时是偶然的。
  • 迥异的定义:即使同一家公司的不同部门也可能对性能等关键特性的定义产生分歧。
  • 过于复合(Too composite):有些架构特性由其他特性组成。例如,敏捷性可以分解为模块化、可部署性和可测试性等特性。

架构特性的定义目标解决这三个问题:通过在全组织范围内对架构特性的具体定义达成一致,团队可以围绕架构创建一种无处不在的语言。同时,通过鼓励客观定义,团队可以解读复合特性,发现他们可以客观定义的可测量特性。

业务措施

许多架构特性都有明显的直接衡量标准,如性能或可扩展性。这些架构特性也提供了许多细微的解释,这取决于团队的目标。例如,是测量平均响应时间还是最大响应时间?
高层团队不只是建立硬性的性能数字,需要基于统计分析。例如,假设一个视频流服务想要监控可扩展性。工程师们不是将一个任意的数字设定为目标,而是随着时间的推移测量规模并建立统计模型,然后在实时指标超出预测模型时发出警报。失败可能意味着两件事:模型不正确(团队喜欢知道),或者有什么问题(团队也喜欢知道)。
现在团队可以衡量的特征种类正在快速发展。随着设备、目标、能力和其他无数事物的变化,团队需要找到新的事物和方法来衡量。

结构性措施

一些客观的衡量标准并不像性能那么明显。比如模块化这样的内部结构特征呢?不幸的是,内部代码质量的全面衡量标准还不存在。然而,一些度量标准和通用工具确实允许架构师处理代码结构,尽管是沿着狭窄的维度。
代码的一个明显的可衡量方面是复杂度,由循环复杂度指标(cyclomatic complexity)定义。
Cyclomatic Complexity (CC)是一个代码级别的度量标准,表示程序的复杂度,由Thomas McCabe, Sr.于 1976 年开发。它是基于图论,特别是导致不同执行路径的条件语句来计算的。例如,如果一个函数没有条件语句(如if语句),那么 CC=1。如果该函数只有一个条件,那么 CC=2,因为存在两个可能的执行路径。CC = E − N + 2P,E 为图中边的个数,N 为图中节点的个数,P 为连接组件的个数(也可以视为结束点的个数)。
架构师和开发人员普遍认为,过于复杂的代码几乎损害了代码库的每一个理想特性:模块化、可测试性、可部署性等等。然而,如果团队不关注逐渐增长的复杂性,这种复杂性将主导代码库。

流程措施

一些架构特征与软件开发过程相互交叉。例如,敏捷性经常作为一个理想的特征出现。然而,这是一个复合的架构特性,架构师可以将其分解为可测试性、可部署性等特性。
可测试性是可以通过代码覆盖率工具来衡量的,几乎所有的平台都可以评估测试的完整性。但它不能取代思考和意图。例如,一个代码库可以有 100% 的代码覆盖率,却有糟糕的断言,实际上并不能提供对代码正确性的信心。尽管如此,可测试性显然是一个客观可测量的特性。
同样,团队可以通过各种指标来衡量可部署性:成功部署与失败部署的百分比、部署所需的时间、部署所引发的问题/错误,以及其他一系列的指标。每个团队都有责任得出一套好的衡量标准,在质量和数量上为他们的组织和捕捉有用的数据。

治理和适应性函数(Governance and Fitness Functions)

一旦架构师建立了架构特性并确定了它们的优先级,他们如何确保开发人员会尊重这些优先级?模块化是一个很好的例子,都知道模块化是很重要的,但并不紧急;在许多项目上,紧急性占主导地位。
架构师仍然需要一个治理机制。

治理架构的特点

治理(Governance),源于希腊语 kubernan 一词,是架构师角色的一项重要职责。顾名思义,架构治理的范围涵盖了架构师(包括企业架构师)想要对软件开发过程施加影响的任何方面。例如,确保组织内部的软件质量就属于架构治理的范畴,而疏忽会导致灾难性的质量问题。
Building Evolutionary Architectures 一书描述了一系列技术,称为适应性函数(fitness functions),用于自动化架构治理。

适应性函数(Fitness Functions)

“Building Evolutionary Architectures” 中的 “进化” 一词更多地来自计算,而不是生物学。如果开发者试图设计一个遗传算法来产生一些有益的结果,他们往往希望对算法进行引导,提供一个客观的衡量标准来表明结果的质量。这种指导机制被称为适应性函数函数:用于评估输出与实现目标的接近程度的对象函数。
例如,假设一个开发者需要解决旅行商问题,这是一个著名的问题,常常作为机器学习的基础。给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。如果开发者设计一个遗传算法来解决这个问题,适应性函数可能会评估路线的长度,因为最短的路线代表最高的成功率。另一个适应性函数可能是评估与路线相关的总体成本,并试图将成本保持在最低水平。还有一个适应性函数可能是评估出差的销售人员离开的时间,并优化以缩短总的旅行时间。
进化架构的借用了这个概念,创造了适应性函数。
架构适应性函数:对某些架构特征或架构特征组合进行客观完整性评估的所有机制。
适应性函数并不是架构师要下载的一些新框架,而是对许多现有工具的新观点。请注意定义中的短语所有机制—架构特性的验证技术和特性一样多种多样。适应性函数与许多现有的验证机制重叠,这取决于它们的使用方式:度量、监控、单元测试、混沌工程等,如下图所示。
image.png

根据架构特点,可以使用许多不同的工具来实现适应性函数。例如,在 “耦合 “中,我们引入了让架构师评估模块化的指标。下面是几个测试模块化各方面的健身函数的例子。

循环依赖

模块化是大多数架构师关心的隐性架构特性,因为维护不好的模块化会损害代码库的结构。然而,在很多平台上,各种力量会影响架构师的良好意图。例如,当在任何流行的 Java 或 .NET 开发环境中编码时,只要开发人员引用了一个尚未导入的类,IDE 就会很有帮助地呈现一个对话框,询问开发人员是否愿意自动导入该引用。这种情况经常发生,以至于大多数程序员养成了习惯,像反射动作一样将自动导入对话框甩开。然而,在类或组件之间任意导入类或组件对模块化来说是灾难。例如,下图说明了一种特别具有破坏性的反模式,这是架构师们渴望避免的。
image.png

在图中,每个组件都会引用其他组件中的某些内容。拥有这样的组件网络会破坏模块化,因为开发人员在重用一个组件的同时,不能不把其他组件也带上。架构师如何才能治理这种行为,而不需要不断地去看那些开发人员的肩膀?Code review 有帮助,但发生在开发周期太晚的阶段,难以奏效。如果架构师允许一个开发团队在代码库中横冲直撞地导入一周,直到 Code review,代码库已经发生了严重的破坏。
解决这个问题的方法是写一个适应性函数来照顾循环依赖,如下所示。
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 包的结构,如果存在任何循环,则测试失败。架构师可以将这个测试接入到项目的持续构建中。这是一个很好的适应性函数的例子,它守护着软件开发的重要而非紧急的实践:它是架构师的重要关注点,却对日常编码影响不大。

Distance from the main sequence 适应性函数

在耦合中有公式:D = | A + I - 1 |
其中,D = Distance from the main sequence ,A = Abstractness, I = Instability。这是衡量包在抽象性和稳定性之间平衡的指标,一个垂直于主序列的包在抽象性和稳定性方面是最佳平衡的。
image.png

@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 建立了一个可接受值的阈值,如果一个类落在范围之外,则测试失败。
这既是一个对架构特性进行客观衡量的例子,也说明了开发者和架构师在设计和实现适应性函数时合作的重要性。
Architects must ensure that developers understand the purpose of the fitness function before imposing it on them.
在过去的几年里,适应性函数工具的复杂程度不断提高,包括一些特殊用途的工具。其中一个这样的工具是 ArchUnit,它是一个 Java 测试框架,受到 JUnit 生态系统的启发,并使用了 JUnit 生态系统的几个部分。ArchUnit 提供了各种预定义的治理规则,这些规则被编纂为单元测试,并允许架构师编写特定的测试来解决模块化问题。考虑下图所示的分层架构。
image.png

在设计这样的分层时,架构师定义各层是有充分理由的。然而,架构师如何确保开发人员会尊重这些分层?允许实现者破架构构会伤害架构的长期健康。
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 允许对该平台进行类似的测试;如下所示。
_// 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 的 Chaos MonkeySimian Army。特别是 Conformity、Security 和 Janitor Monkeys 是这种方法的典范。

  • Conformity Monkey 允许 Netflix 的架构师定义生产中猴子执行的治理规则。例如,如果架构师决定每个服务应该对所有 RESTful 动词做出有用的响应,他们就会将该检查建立在 Conformity Monkey 中。
  • Security Monkey 检查每个服务是否存在众所周知的安全缺陷,比如不该被激活的端口和配置错误。
  • Janitor Monkey 会寻找没有其他服务的实例。Netflix 有一个进化的架构,所以开发人员经常迁移到更新的服务。因为在云上运行的服务会消耗资金,所以 Janitor Monkey 会寻找孤儿服务,并将它们从生产中解体出来。

    小结

    几年前,Atul Gawande(Picador) 所著的《清单宣言》(The Checklist Manifesto)这本颇具影响力的书描述了航空公司飞行员和外科医生等职业如何使用清单(有时是法律规定的)。这并不是因为这些专业人士不了解自己的工作,或者健忘。而是当专业人员反复做一项高度细致的工作时,细节就很容易被忽略,简洁的检查表就会形成有效的提醒。这是对适应性函数的正确看法—与其说适应性函数是一种重量级的治理机制,不如说适应性函数为架构师提供了一种表达重要架构原则并自动验证的机制。开发人员知道他们不应该发布不安全的代码,但对于繁忙的开发人员来说,这个优先级与其他几十个或几百个优先级竞争。特别是像 Security Monkey 这样的工具,以及一般的适应性函数,允许架构师将重要的治理检查刻在架构的底层。