《演进式架构》学习笔记(三)

(此书写得有点杂,但部分内容还可以,以此几篇记录下笔记。)

六、构建可演进的架构

演进机制

架构师可以通过下面 3 步来构建演进式架构:

  • 1.识别受演进影响的架构维度:其中一定包含技术架构,通常还有数据设计、安全、可伸缩性和其他一些他们认为重要的特征。该过程会涉及组织内部其他相关团队,包括业务、运营、安全和其他受影响的团队。逆康威时刻对此很有帮助,因为它鼓励组织多角色团队。基本上,这是架构师在项目初期确定需要支持的架构特征的常规工作。
  • 2.为每个维度定义适应度函数:单个架构维度通常包括多个适应度函数。例如,为了保证代码的架构特征,架构师通常将一系列代码衡量指标构建到部署流水线中,例如避免组件循环依赖。架构师通过轻量级的方式记录那些需要持续关注的架构维度,例如 wiki。接着,针对每个维度,他们确定在演进过程中可能出现错误行为的部分,最终定义出适应度函数。适应度函数可以自动运行或手动触发,并且在某些情况下需要设计得更加巧妙。
  • 3.使用部署流水线自动化适应度函数:最后,架构师必须在项目中推进增量变更,在部署流水线中定义不同阶段来执行适应度函数并管理部署实践,例如环境准备、测试和其他 DevOps 问题。增量变更是演进式架构的引擎,它让我们可以通过部署流水线主动验证适应度函数,并通过高度自动化隐藏一些单调的任务,例如无感知部署。生产周期是在持续交付中衡量工程效率的指标。在支持演进式架构的项目中,开发人员的职责之一就是保持良好的生产周期。生产周期是增量变更的重要部分,因为其他很多度量指标亦来源于此。例如某架构中新版本的发布速度和其生产周期成正比。换句话说,一旦项目的生产周期延长,那么它将使项目交付新版本的速度减慢,进而影响演进能力。

开发人员无法预料所有事情,因此软件会受到未知的未知问题的困扰。在构建软件的过程中,架构的某些部分有时会显露出不好的迹象,构建适应度函数能阻止问题进一步恶化。虽然有些适应度函数会在项目初期自然显现,但还有一些适应度函数在架构经受压力时才会显现。架构师尤其需要注意那些非功能性需求被破坏的情况,并通过适应度函数更新架构来避免可能出现的问题。

全新的项目

为新项目构建演进能力远比改造已有项目容易。
新项目在处理意外变更时会更容易。

改良现有架构

赋予现有架构演进能力取决于三个因素:组件耦合度工程实践成熟度,以及开发人员构建适应度函数的难易程度

适当的耦合和内聚

组件间的耦合很大程度上决定了技术架构的演进能力。清晰解耦的系统易于演进,充满耦合的系统则会妨碍演进。想构建出真正可演进的系统,架构师必须考虑架构中所有受影响的维度。

除了技术层面的耦合,架构师还必须考虑和保护系统中组件的功能内聚。当从一种架构迁移到另一种架构时,功能内聚性决定了组件重构后的最终粒度

这并不意味架构师可以随心所欲地分解组件,而是说基于特定的问题上下文,组件的大小应该是适当的。

选择架构前,需要理解面临的业务问题。

工程实践

工程实践对定义架构的可演进性至关重要。虽然持续交付实践无法保证架构能实现演进,但它依然不可或缺。

虽然这些手工测试会延长生产周期,但在部署流水线中包含一些手动阶段很重要。第一,这样会将应用构建的每个阶段都置入部署流水线中。第二,随着团队逐步将更多部署工作自动化,手动阶段也会实现自动化,不再中断部署过程。第三,阐明每个阶段有助于我们更好地理解构建的各个手工部分,创造更好的反馈环并推动改进。

通常,构建演进式架构的最大障碍是棘手的运维工作。如果开发人员无法轻松地部署变更,反馈环的各个部分都会受阻。

适应度函数

适应度函数是演进式架构的保护层。如果架构师围绕特定的架构特征构建系统,那么这些特征可能和可测试性形成正交关系。

希望架构师将各种架构验证机制视为适应度函数,包括那些临时考虑的事情。

关于商业成品软件

COTS(商业成品软件)和套装软件在大型企业中非常普遍,它们给架构师构建可演进的系统带来了挑战。

COTS 必须随着企业中的其他应用一同演进,然而这些系统没有很好地支持演进。

  • 增量变更: 可悲的是,大多数商用软件的自动化和测试都落后于行业标准。架构师和开发人员必须经常隔离集成点并尽可能地构建测试,并将整个系统视为黑盒。在实施敏捷性时,COTS 在部署流水线、DevOps 及其他现代实践方面给开发团队带来了很多挑战。
  • 适当的耦合: 套装软件经常在耦合上出错。通常,这类系统是不透明的,开发者使用预定义的 API 进行集成。这些 API 不可避免地会遭遇反模式的问题,它们给开发人员些许(但不太够的)灵活性来完成重要的工作。
  • 适应度函数: 在赋予系统演进能力的征途上,最大的障碍可能是为套装软件添加适应度函数。通常,这类软件不会暴露太多内部细节,因此难以进行单元测试和组件测试,只能诉诸于基于行为的集成测试。但这类测试并不理想,因为它们粒度太大,必须在复杂的环境中运行并覆盖大部分系统行为。

如果不可避免地受到套装软件的困扰,架构师应该尽可能地构建强大的适应度函数,并使其自动化地运行。

架构迁移

当架构师想迁移架构时,通常会考虑类和组件间的耦合特征,但可能忽略其他很多影响演进的维度,例如数据。和类之间的耦合一样,也存在事务性耦合,而且在重建架构时难以消除。当尝试将现有模块分解得更小时,这些额外的耦合点带来了巨大的负担。

很多资深开发人员年复一年地构建相同类型的应用,因单调而厌倦。于是,很多开发人员倾向于编写框架,而不是使用现成的框架来构建应用,这便是所谓的“元工作比工作更有趣”。工作是无趣、平凡且重复的,而构建新事物则令人兴奋。

当开源工具可以提供这些能力时,他们已经拥有了钟爱的自研基础设施。由于方法上存在细微差别,他们决定坚持使用自研的工具,而不是标准技术。十年后,他们最优秀的开发人员忙于维护这些工具、修复应用服务器、为 Web 框架添加新特性和其他杂事。他们长期困于维护,无暇创新以构建更好的应用。

架构师无法对“元工作比工作更有趣”综合征免疫,这种综合征表现为采用时髦但并不合适的架构,例如微服务。

不要仅仅因为元工作有趣而构建架构。

 迁移步骤

在分解代码时,首要任务便是理解它们之间的联系。当分解单体应用时,架构师必须考虑耦合和内聚,寻求两者间的平衡。

在重建架构时,需要考虑所有受影响的维度。

架构师必须清楚实施该迁移的原因,并且确保不是盲目地赶时髦。将架构划分为领域,加上更好的团队结构和运维的隔离,会使增量变更更容易,这是演进式架构的关键组成之一,因为工作的重点和实际的产出是相互匹配的。

在分解单体架构时,确定正确的服务粒度是关键。

开发人员要定义新的服务边界。团队可以通过多种划分方式将单体应用分解成服务:

  • 业务功能分组: 企业可能有清晰的业务划分直接对应于 IT 能力。模仿当前业务沟通层级构建的软件无疑应验了康威定律。
  • 事务边界: 许多业务需要依附于大量事务边界。当分解单体应用时,架构师经常发现事务耦合是最难解开的。
  • 部署目标: 增量变更使得开发人员可以按照不同的计划有选择地发布代码。例如,相比库存部门,市场部门可能希望更新频率更高。如果运维准则非常重要,例如发布速度,那么围绕运维问题划分服务是合理的。类似地,系统的某个部分可能对运维特征有极致的要求(例如伸缩性)。围绕运维目标划分需求使得开发人员能够(通过适应度函数)跟踪服务的健康状态和其他运维服务指标。

较大的服务粒度意味着微服务中许多固有的协调问题都不会存在,因为服务越大,单个服务所包含的业务上下文就越多,但同时操作难度也越大。

演进模块间的交互

共享就是耦合的一种形式,在微服务架构中这是非常不可取的。

在分布式环境中,开发人员可以使用消息或服务调用来实现相同形式的共享。

当开发人员确定了正确的服务划分,下一步便是分离业务层和 UI。

开发人员通常会在迁移早期分离 UI,在界面组件和后端服务间构建映射代理层。在分离 UI 时,还会构建防腐层来将户界面的变更和架构变更隔离开。

服务发现让服务能够相互查找和调用。最终,架构将由必须相互协调的服务所组成。通过尽早地构建服务发现机制,开发人员可以从容地迁移系统中需要变更的部分。开发人员经常将服务发现构建成一个简单的代理层,每个组件调用代理,然后代理再将请求映射到指定的实现。

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,除非该问题是由间接层太多导致的。 ——Dave Wheeler 和 Kevlin Henney

当然,开发人员增加的间接层越多,服务间的导航就会变得越复杂。

在将应用从单体架构迁移到基于服务的架构时,架构师必须注意现有系统中模块的连接方式。不成熟的划分方式会带来严重的性能问题。

当从单体应用迁移时,首先构建少量大型服务。 ——Sam Newman,《微服务设计》

接下来,开发人员从单体应用中分离选中的服务并修复集成点。适应度函数在这里能起到关键作用,开发人员应构建适应度函数以确保新引入的集成点不会改变已有行为,并添加消费者驱动的契约。

演进式架构构建指南

与其重建各项能力,大多数企业会努力适应现有的一切。尽管我们喜欢在纯净的理想环境中讨论架构,但现实世界往往展现出相反的混乱状态,技术债、优先级冲突和有限的预算很常见。在大型企业中,架构正如人脑一样:底层系统依旧处理着关键的业务细节,但也伴随着过去的包袱。企业不愿意放弃还在工作的系统,这导致集成架构的挑战不断升级。

赋予现有架构演进能力极具挑战。这是因为,如果开发人员从未将架构构建得易于变更,那么演进能力便不太可能自然地出现。架构师无法轻松地将大泥团转变成现代微服务架构,无论他多么有天赋。然而通过为项目增添一些灵活性,便能在不改变其整体架构的情况下改善项目。

1.去除不必要的可变性

通过用不可变的基础设施取代雪花服务器,现代 DevOps 在其领域内解决了动态平衡的问题。

虽然不可变性听起来与演进性背道而驰,但恰恰相反。软件系统由成千上万个动态部分组成,它们相互依赖、紧密联系在一起。然而当某个部分发生变化时,开发人员仍然努力应对各种意外。通过锁定意外变更的可能性,能更有效减少使系统变得脆弱的因素。

不可变的基础设施遵循了我们所提倡的去除不必要的可变性这一思路。构建可演进的软件系统意味着尽可能地控制未知因素。

架构师能通过各种途径将可变的事务变成常量。

构建不可变的开发环境还能让我们在项目中使用有用的工具。

复用旧的功能开关是鲁莽的行为,功能开关的最佳实践是在其目的达成后尽快主动地将其删除。在现代 DevOps 环境中,手动将关键软件部署到服务器也同样被视为鲁莽的行为。

2.让决策可逆

当失败发生时,开发人员需要构建新的适应度函数来防止再次失败。

很多 DevOps 实践可以使那些需要被撤销的决策变得可逆,例如蓝绿部署。功能开关是开发人员使决策可逆的另一种常见方式。

3.演进优于预测

未知的未知问题是软件系统的大敌。很多项目始于一系列已知的未知问题,例如开发人员知道他们需要学习领域知识和新技术。然而,项目也会受到未知的未知问题的影响。

由于未知的未知问题,所有架构都将是迭代式的,敏捷实践只是较早地意识到了这一点。 ——Mark Richards

我们知道动态平衡导致了软件开发领域的不可预见性。架构并不是孤立的前期设计活动,项目在其整个生命周期里持续变化着,一些变化是明确的,另一些则不是。使用防腐层是开发人员隔离变化的常用技术。

4.构建防腐层

抽象干扰反模式描述了这样的场景,项目与某个外部依赖库(商业的或开源的)建立了太多连接。一旦开发人员要升级或更换该库,他们会发现调用该库的很多代码会带有基于该库的抽象假设。领域驱动设计中包含了针对这一现象的保护措施,叫作防腐层。

头脑灵活的架构师在做决定时会遵循最后责任时刻(The Last Responsible Moment)原则,架构师以该原则避免项目中的常见隐患——过早引入复杂度。

所谓技术债就是项目中本不应该存在的部分,它会导致项目缺失理应存在的部分。很多开发人员将复杂的遗留代码视为唯一的技术债,但项目还会因为早期的复杂度而在无意中引入技术债。

构建即时防腐层来隔离库的更新。

控制应用中的耦合点,特别是外部资源,是架构师的关键职责之一。在需要的时候添加依赖。作为架构师,需要记住依赖在提供好处的同时,还会施加约束。确保从中获得的好处多过更新和管理依赖所带来的成本。

开发人员熟悉工具的好处,却忽视所要做出的权衡! ——Rich Hickey,Clojure 之父

使用防腐层有助于系统的演进性。虽然架构师无法预测未来,但至少可以降低变更的成本,以免受到太多负面影响。

使用服务模板仅将合适的架构部分耦合在一起,例如基础设施组件,团队可以从耦合中获益。

服务模板体现了适应性。不把技术架构作为系统的主要结构,使得我们能更容易地将变更和架构维度准确对应起来。当开发人员构建分层架构时,每一层的变更很容易,但跨层的变更会高度耦合。

5.构建可牺牲架构

因此,在管理上,不应该询问是否该构建一个试验性的系统然后将其抛弃。因为你一定会那样做。因此,做好抛弃它的计划,因为你终将如此。 ——Fred Brooks

在架构层面,开发人员努力预测迅速变化的需求和特征。进行概念验证是在选择架构时获取足够信息的一种方式。

为了证明市场的存在,很多企业构建可牺牲架构来实现最小可行性产品。虽然这个策略很好,但最终,团队仍会投入时间和资源来构建更强大的架构。

在 Fred Brooks 提到第二系统综合症第二系统效应,Second system effect)时,他指出技术债会影响很多在初期很成功的项目。由于期望膨胀,小的、优雅的、成功的系统往往会演进成为塞满各种功能的庞然大物。业务人员不愿抛弃还在运行的代码,因此架构走向了一直做加法,但从不做减法的不归路。

作为某种隐喻,技术债发挥着有效的作用,因为它与项目经历共鸣,代表着设计中的缺陷,无论其背后的驱动力如何。技术债加重了项目中不当的耦合——糟糕的设计经常表现为病态耦合和其他反模式,使重建代码变得困难。在开发人员重建架构时,第一步应该清除那些以往的设计妥协,即技术债。

6.应对外部变化

外部依赖是所有开发平台的一个共同特征,其中包括工具、框架、库和其他来自互联网并(更重要的)通过互联网进行更新的资产。软件开发处在高耸的层层抽象之上,每一层抽象都建立在下层抽象之上。

经由构建工具,大多数项目会依赖于繁多的第三方组件。开发人员喜欢外部依赖,因为它们能带来好处,但是很多开发人员忽略了随之而来的代价。当我们依赖第三方代码时,开发人员必须采取防御措施来预防可能的意外,例如破坏性的变更、未经通知的删除等。管理项目的这些外部组件是构建演进式架构的关键。

1
2
3
4
5
6
7
8
9
10
11
12
破坏了整个互联网的 11 行代码

2016 年年初,JavaScript 的开发人员在某个不起眼的依赖上栽了大跟头。
某个创建了很多实用小工具的开发人员,他的模块与某个商业软件重名了,而后者要求他更名,因此他有些恼火。
但他并没有照做,而是删除了 250 多个模块,包括一个名为 leftpad.io 的库,它通过 11 行代码来为字符串左边添加零或空格(如果 11 行代码也称得上“库”)。
不幸的是,很多主要的 JavaScript 项目(包括 Node.js)都依赖该库。
在它消失后,所有人的 JavaScript 部署都无法进行。
JavaScript 的仓库管理员通过恢复代码恢复了整个体系,这是前所未有的,但它引发了社区对于如何更好地管理依赖的更明智的深层次讨论。

这个故事教给架构师两点教训。
第一,铭记外部依赖在带来好处的同时还需要付出成本。我们需要确保收益大于成本。
第二,不要让外部力量影响构建的稳定性。如果某个上游需要的依赖突然消失,那么应该拒绝该变更。

传递依赖管理被视为有害的。 —— Chris Ford(和 Neal 没有关系)

Chris 认为,我们只有意识到问题的严重性才能找到解决方案。有时我们无法找出问题的解决方案,但我们需要格外留意它,因为它会严重影响演进式架构。稳定性是持续交付和演进式架构的共同基础。开发人员无法基于不确定因素构建可重复的工程实践。而允许第三方修改核心依赖背离了这一原则。

拉取的方式获取外部依赖是开启依赖管理的良好开端。例如拉取,设置一个内部版本控制仓库作为第三方的组件商店,然后将外部的变更视为对仓库的拉取请求。如果变更是有益的,那么将其纳入体系中。如果某个核心依赖突然消失,那么就应该将该拉取请求视为破坏稳定的因素并将其拒绝。

秉持持续交付思维,第三方组件库使用自己的部署流水线。当组件发生变更时,部署流水线合并修改,接着执行构建并对受影响的应用进行冒烟测试。如果成功,则保留变更。因此,第三方依赖使用与内部应用相同的工程实践和内部开发机制,有效地模糊了自研代码和第三方依赖之间通常不重要的区别,因为它们终将成为项目中的代码。

7.更新库与更新框架

架构师在库和框架之间做出了一般区分,简单地将其描述为“开发人员的代码会调用库,而框架会调用开发人员的代码”。通常,开发人员从框架中派生出子类(框架反过来调用这些派生出的类),这便是框架调用代码的原因。相反,库代码通常是一系列相关的类或函数,开发人员按需调用它们。由于框架调用开发人员的代码,导致了框架的高度耦合。相反,库通常更为实用,耦合度也更低,例如 XML 解析器、网络库等。

我们青睐库,因为它们引入应用的耦合更少,使得在技术架构演进时易于置换。

工程实践是我们区别对待库和框架的另一个原因。由于应用基于框架搭建,所以应用的所有代码都会受到框架变更的影响。很多人都真切地感受过这样的痛苦——当基础框架因两个主要版本而过时,那么升级框架需要极大的付出。

由于框架属于应用的基础部分,团队必须积极地将其更新。库所形成的脆弱集成点比框架更少,团队更新库时会更自由。一种非正式的管理模式将框架的更新视为推动式更新,将库的更新视为拉动式更新。
当基础框架更新时(其输入 / 输出耦合数量高于某个特定阈值),只要新版本稳定,并且团队能分配出时间,那么就应该应用该更新。尽管这会花费时间和精力,但如果团队无限期地拖延该更新,最终所花费的时间将远不止这些。

积极地更新框架依赖,“消极”地更新库。

8.持续交付优于快照

很多依赖管理工具通过快照机制支持持续开发。构建快照最初是为了标明那些差不多准备好发布但仍在开发中的组件,它暗示着代码还可能定期更新。一旦带上了版本号,那么“快照”(-SNAPSHOT)的标记将被移除。

开发人员使用快照是因为过去大家认为测试困难且耗时,这导致开发人员尝试区分变化的内容和没变化的内容。

在演进式架构中,所有事务都在不断变化,需要通过构建工程实践和适应度函数来适应变化。例如,当项目有着出色的测试覆盖率和部署流水线时,开发人员可以通过自动的部署流水线测试每个组件的每次变更。开发人员没有理由为项目的每个部分保留某个特殊的仓库。

快照是某个开发时期的产物,当时全面测试还不普遍,存储成本很高,验证也很困难。

持续交付建议以更细致的方式思考依赖,开发人员应该为外部依赖引入两个新定义:流动的依赖被守护的依赖。通过部署流水线机制,流动的依赖尝试自动地将自己更新到新版本。

目前流行的构建工具都未支持该级别的功能,开发人员必须基于现有的工具构建这种智能功能。然而在演进式架构中,这种依赖模型表现得相当不错,其中生产周期作为关键的基础值,和其他很多关键指标成正比。

9.服务内部版本化

版本化端点有两种常用的方式:版本号内部版本化。对于版本号,当破坏性的变更发生时,开发人员会创建新的、通常包含版本号的端点名。这使得旧的集成点继续调用旧的服务,而新的集成点则调用新的版本。另一种替代方案是内部版本化,调用方无须修改端点,相反,开发人员在服务端构建逻辑来确定调用方的上下文,从而返回正确的版本。相比调用应用时指定版本号,使用固定名称的好处是耦合更少。

无论是哪种情况,都应该严格限制所支持的版本数量。更多的版本会增加测试和其他工程的负担。建议一次只支持两个版本,并且只是暂时支持。

在版本化服务时,我们倾向于内部版本化,一次只支持两个版本,而不是用版本号。

七、演进式架构的陷阱和反模式

项目中有两种错误的工程实践——陷阱反模式

软件的反模式包含两层含义。首先,反模式是一种实践,开始看起来不错,但结果证明是错的。其次,大多数反模式都有更好的替代方案。很多反模式只有在事后才被架构师注意到,因此很难避免。陷阱表面上像是个好主意,但很快便显露出缺点。

技术架构

反模式:供应商为王

一种完全围绕供应商产品构建的架构,将组织和工具病态地耦合。购买了供应商软件的公司计划通过插件扩充软件包,以丰富供应商软件的核心功能来匹配其业务。然而,很多时候无法将 ERP 定制到满足所有需求,开发人员发现他们受到了 ERP 的制约,这一方面来自于工具的限制,另一方面来自于 ERP 是事实上的架构核心。换句话说,架构师让供应商成为了架构的王者,左右了未来的决策。

想要规避这种反模式,需要将所有软件都视为集成点,即便起初它具有广泛的职责。如果在一开始便假设集成,开发人员可以更容易地替换掉那些对其他集成点无用的行为,废除“王者”。

无论从技术还是从业务流程的角度来看,将外部工具或框架置于架构的核心会严重限制架构的演进能力。开发人员在技术上受到了供应商选择的制约,例如持久层、基础设施以及其他限制。

从业务流程的角度来看,这种工具无法支持最佳的工作流,这也是其副作用。大多数公司最终屈服于这种框架,不再尝试定制这类工具,而是修改自己的流程。越多公司这样做,公司间的差异变得越小,当然,如果差异化不是竞争优势,这或许是可以接受的。

与其沦为供应商为王反模式的受害者,我们不如将供应商产品视为集成点。开发人员可以在集成点间构建防腐层,从而避免架构受到供应商工具变更的影响。

陷阱:抽象泄漏

所有重大的抽象在某种程度上都会泄漏。 —— Joel Spolsky

现代软件构建于层层抽象之上:操作系统、框架、依赖等。开发人员构建抽象来摆脱在最底层无尽的思考。如果开发人员需要将来自硬件驱动的二进制数字转换为文本来进行编程,他们将无法完成任何工作。现代软件成功的原因之一在于我们能建立有效的抽象。

但是抽象也是有代价的,因为没有抽象是完美的,如果有,那么它将不再是抽象,而是实际存在。

底层抽象破坏会导致意外的灾难,即原始抽象泄漏,它是技术栈日渐复杂带来的副作用之一。

始终保持对当前抽象层以下至少一个抽象层的完全理解。 ——许多软件专家

技术栈复杂度的增长印证了动态平衡问题。不只是体系在变化,其组成部分随着时间推移也会变得更加复杂并交织在一起。适应度函数(保护演进式变更的机制)能够保护架构中脆弱的连接点。架构师将关键集成点上的不变量定义为适应度函数,并在部署流水线运行它们,确保抽象不会意外泄漏。

了解复杂技术栈的脆弱部分,并通过适应度函数自动保护它们。

反模式:最后 10%的陷阱

即所有项目都存在缺憾。
在抽象范围的另一端存在着另一种复用陷阱,它隐藏在套装软件、平台和框架中。

整洁的解决方案无法解决现实世界中一些混乱的事物,例如业务流程。
无论开发人员多么努力,他们都无法将事物提炼得足够精细,这便是无限回归问题的一部分:一些命题依赖于其他命题而成立,没有止境。在软件领域中,无限回归表现为人们想要用终级的细节详细描述任何事物,但在任何现有细节之下总是存在另一层更细粒度的细节。

反模式:代码复用和滥用

在软件行业中,我们从他人构建的可复用框架和库中受益匪浅,它们通常是开源软件,可以免费使用。复用代码显然很好,然而,任何美好事物都不能被滥用,很多公司因滥用代码给自己造成了麻烦。每个企业都希望复用代码,因为软件看起来模块分明,像电子元件一样。然而,尽管在真正模块化的软件中的确如此,但它却难以实现。

复用软件更像是器官移植而不是拼装乐高积木。 ——John D. Cook

复用软件很难并且不会自动出现。很多管理者乐观地认为开发者编写的任何代码都可复用,但事实并非总是如此。很多公司尝试并成功编写出真正可复用的代码,但这是有意为之并且困难重重。开发人员通常花费大量时间尝试构建可复用的模块,结果却几乎无法复用。

架构师努力实现 SOA 中的终极规范——所有概念都只有一个(共享的)归属。

讽刺的是,开发人员为了代码复用所付出的努力往往适得其反。为了复用代码,需要引入额外的选项和决策点以适应不同的用途。开发人员为实现可复用所添加的钩子越多,对代码的基本可用性损害越大。

代码复用性越高,其可用性越低。代码的易用性和复用性往往成反比。当开发人员构建可复用的代码时,他们必然会为了将来开发人员以各种方式使用该代码添加特性。所有针对未来的特性都使得开发人员更难将代码用于单一目的。

微服务避免代码复用,遵循重复优于耦合的理念。该理念认为复用意味着耦合,因此微服务架构是极度解耦的。然而,微服务的目标并不是追求重复,而是隔离领域内的实体。那些共享通用类的服务不再独立。

复用所带来的好处是虚幻的,除了其自身缺陷,它还会引入耦合。因此,虽然架构师了解重复的缺点,但他们利用重复抵消了耦合过多对架构的局部损害。

复用代码可以是资产,也可能是潜在的责任。我们要确保代码中引入的耦合点不会和其他架构目标产生冲突。

当耦合点妨碍了演进或其他重要的架构特征时,通过分叉或重复来打破耦合点。

架构师必须持续评估架构特征的适应度,保证它们仍在提供价值,避免沦为反模式。

架构师在当时做出的正确决定,随着时间推移,由于动态平衡等因素的变化,往往会变得不再正确。例如,架构师将系统设计为桌面应用,但随着用户习惯的改变,业界将其引向了网页应用。最初的决定并没有错,但环境意外地改变了。

陷阱:简历驱动开发

架构师迷恋软件开发领域的新发展,并迫不及待地想要尝试。然而,要选择出高效的架构,他们必须仔细了解对应的问题域并选择最合适的架构,这样才能提供最理想的能力并且破坏性约束最小。当然,除非架构师陷入了简历驱动开发的陷阱——为了用这些知识丰富自己的简历而选择框架和库。

不要为了架构而构建架构,构建架构是为了解决问题。在选择架构前,要始终理解问题域,不要本末倒置。

增量变更

几十年来,编写软件的目标没有考虑敏捷性,而是围绕着降低成本、共享资源和其他一些外部约束。因此,很多组织缺乏能够支持演进式架构的基础。

反模式:管理不当

软件架构并非处于真空之中,它通常反映了设计时所处的环境。

从开发的角度来看,由于无意的耦合,在同一台主机上打包多个资源并不可取。无论共享资源间隔离得多么好,资源竞争终将出现。

如今,开发人员可以构建组件高度隔离的架构(例如微服务),来消除因共享环境加剧的意外耦合。但是很多公司依然坚持着陈旧的管理手段。这类管理模式重视共享资源和同质化的环境。近来由于 DevOps 等运动的改进,使得这种管理模式不再适用。

软件能力是每个前沿公司的必备能力,对于想要保持竞争力的公司更是如此。其中便包括如何管理研发资产,例如软件运行环境。

当开发人员能够不费成本(金钱或时间)地创建虚拟机和容器资源时,看重单一解决方案的管理模式就变得不适用了。微服务领域出现了一种更好的方式。微服务架构的一个常见特征就是支持异构的环境,各个服务团队可以选择适合的技术栈来实现他们的服务,而不用按照企业标准进行统一。这与传统方式截然相反,因此当传统企业的架构师听到这个建议时会退缩。然而,大多数微服务项目的目标并不是武断地选择不同的技术,而是根据具体问题选择适当的技术。

在现代环境中,将不同技术栈统一成单一技术栈是不当的管理。这会无意将问题过度复杂化,管理决策使得实现解决方案所需的工作毫无意义地成倍增加。

在微服务架构中,由于服务间不存在技术架构或数据架构的耦合,不同的团队可以选择正确的复杂度来实现其服务。其终极目标是化繁为简,保持技术栈复杂度和技术需求的一致。当团队全权负责其服务(包括运维)时,这样的划分往往效果最佳。

微服务架构的目标之一是技术架构的极限解耦,使得更换服务不会产生任何副作用。

从大型组织的实用性管理的角度来看,我们发现金发姑娘管理模式效果不错:选择简单中等复杂三种技术栈作为标准,然后允许单个服务需求驱动技术栈的需求。这样就赋予了团队选择合适技术栈的灵活性,还能继续为企业保留标准化带来的好处。

陷阱:发布过慢

持续交付中的工程实践排除了拖慢软件发布速度的因素,这些实践应该作为演进式架构成功的前提。虽然持续交付的终极目标,持续部署,对于演进式架构来说不是必需的,但软件的演进能力与其发布能力息息相关。

如果企业围绕持续部署打造工程文化,期望所有变更在通过了部署流水线设置的挑战后便进入生产环境,那么开发人员就会习惯于持续变更。另一方面,如果发布是一个需要很多专业化工作的正式流程,那么利用演进式架构的机会就会减少。

持续交付追求数据驱动的结果,从指标数据中学习如何优化项目。开发人员必须衡量事物从而了解如何优化。生产周期是持续交付的一个关键指标,和交付周期相关。交付周期是指从一个想法开始到它在软件中实现所耗费的时间。然而,交付周期中包含很多主观活动,例如估算、排列优先级等,使其成为了一个糟糕的工程指标。因此持续交付跟踪生产周期,即启动和完成单位工作所用的时间,这里指软件开发。生产周期从开发人员着手开发某个新功能起开始计时,当该功能在生产环境中运行时停止计时。其目标是衡量工程效率,持续交付的关键目标之一便是缩短生产周期

生产周期对于演进式架构也至关重要。在生物学中,果蝇常用于验证遗传特征,因为他们的生命周期短,新一代出现的速度足够让生物学家观察到明确的结果。这在演进式架构中也同样成立——更快的生产周期意味着架构可以更快地演进。因此,一个项目的生产周期决定了架构的演进速度。换句话说,演进速度和生产周期成正比。表达式如下:

1
v ∝ c

v 代表变更速度,c 代表生产周期。开发人员无法在生产周期内完成系统的演进。换句话说,团队发布软件的速度越快,那么他们便能够越快地演进系统的各个部分。

因此,在演进式架构项目中,生产周期是重要指标,更快的发布速度意味着更快的演进能力。事实上,生产周期是基于过程的原子适应度函数的理想选择。例如,开发人员构建了具备自动化部署流水线的项目,其生产周期为 3 个小时。随着时间推移,由于开发人员向部署流水线添加了更多校验和集成点,生产周期逐渐延长。由于时间是该项目的重要指标,他们设置了适应度函数,当周期时间超过 4 个小时便发出警告。一旦达到阈值,开发人员可以决定调整部署流水线的工作方式,或者决定是否可以接受 4 小时的生产周期。适应度函数适用于开发人员想监控项目的任何行为,包括项目指标。将项目关注点统一成适应度函数使得开发人员可以设置未来决策点,即最后责任时刻,以重新评估决策。在前面的例子中,开发人员必须在当时决定哪一个更重要,是 3 小时的生产者周期,还是他们建立的测试。在大多数项目中,开发人员并不会注意到生产周期逐渐延长,因此也不会权衡冲突的目标,结果含糊地做出这些决定。借助适应度函数,他们可以围绕预期的未来决策点设置阈值。

演进的速度和生产周期成正比,生产周期越短,演进越快。

良好的工程、部署和发布实践是使演进式架构获得成功的关键,反过来又通过假设驱动开发为业务提供新能力。

业务问题

大多数时候,业务人员并非有意给开发人员制造麻烦,但他们的优先权会产生不当的架构决策,在无意间限制了将来的选择。

陷阱:产品定制

销售人员需要卖点。

  • 为每个客户定制: 在这个场景中,销售人员在紧迫的时间内承诺实现特定版本的功能,迫使开发人员使用版本控制分支或标签技术来跟踪版本。
  • 永久的功能开关: 有时将它战略性地用于构建永久的定制功能。开发人员可以使用功能开关来为不同客户创建不同的版本,或创建“免费”版产品,让用户付费解锁高级功能。
  • 产品驱动定制化: 有些产品甚至可以通过 UI 来完成定制。在这种情况下,定制功能是应用的永久部分,需要和其他所有产品功能受到同样的维护。

功能开关和定制化的存在导致产品具有很多可能的路径排列,显著加重了测试负担。除了测试场景,为了保护可能的排列,开发人员可能需要构建更多的适应度函数。

定制也会妨碍演进能力,但我们并不是劝阻企业构建可定制的软件,企业应该实事求是地评估相关成本。

反模式:报表

大多数应用根据不同的业务功能有着不同的用途。
报表是单体架构中意外耦合的好例子。

在分层架构中,开发人员和报表设计人员会合谋创建一种常见的陷阱,它体现了不同业务问题之间的紧张关系。架构师构建分层架构来减少意外耦合,创建隔离层来分离关注点。然而,报表并不需要单独的层支持其功能,它只需要数据。另外,在不同层间路由请求还会使延时增加。因此,很多有着良好分层架构的公司允许报表设计人员将报表和数据库模式直接耦合起来,使得在不影响报表的情况下无法变更数据库模式。这个例子很好地展示了冲突的业务目标是如何破坏架构师的工作,并使演进变得极其困难的。虽然没有人开始就打算让系统难以演进,但这是决策的累积效应。

很多微服务架构通过分离行为来解决报表问题,而微服务的隔离有利于分离但不利于整合。通常构建这类架构时,架构师使用事件流或消息队列来向领域“记录系统”数据库填充数据,每个记录系统嵌在服务架构量子中,使用最终一致性而不是事务行为。一些报表服务也会监听事件流,向针对报表优化过的非规范化数据库中填充数据。从架构的角度来看,协调也是一种耦合,使用最终一致性能让架构师免于协调,为应用程序的不同用途做出不同的抽象。

消除因混合领域和报表引起的不当耦合,使得每个团队可以专注于更加具体且简单的任务。

陷阱:规划视野

预算和规划流程通常决定了对假设和早期决策(假设的基础)的需求。在开发人员为最终用户编写任何代码或发布软件之前,他们所学到的“最佳实践”或“同类最佳”构成了基础假设的一部分。投入到假设中的努力越多,即便在六个月内证明它们是错误的,仍会导致对其强烈的依赖。沉没成本的误区描述了受情绪投入影响的决策。简而言之,人们对某件事情投入的时间和精力越多,就越难放弃它。在软件中,它表现为不合理的工件附件。例如,人们在规划和文档上投入的时间和精力越多,就越可能保护其中的内容,即便有证据表明它们不准确或过时了。

谨防长期规划,因为它会迫使架构师做出的决策不可逆转,同时找到方法来保证多个可选方案。将大型项目分解成更小的早期可交付物,以测试架构选型和开发基础设施的可行性。在通过最终用户反馈验证所用技术确实适用他们试图解决的问题之前,架构师应该避免在实际构建软件之前采用需要大量前期投入的技术,例如大型许可和支持合同。

八、实践演进式架构

组织因素

软件架构广泛地影响着看似与软件无关的各种因素,包括团队影响、预算等许多方面。
在构建演进式架构时,围绕领域而不是技术能力组建团队具有许多优势和一些共同特征。

全功能团队

以领域为中心的团队应该是全功能的,这意味着每个项目角色都由该项目组成员承担。以领域为中心的团队,其目标是消除运营摩擦。换句话说,团队拥有负责设计、实现和部署其服务的所有角色,其中还包括传统上单独的角色,例如运维。但是这些角色必须改变以适应新的结构,这些角色如下所示。

  • 业务分析师: 业务分析师必须与其他服务协调该服务的目标,包括其他服务团队。
  • 架构师: 架构师设计架构来消除不当耦合,以简化增量变更。请注意,这里不需要像微服务那样独特的架构。一个精心设计的模块化单体应用也能以相同的能力适应增量变更(虽然架构师必须明确设计应用程序来支持这种程度的变更)。
  • 测试人员: 测试人员必须习惯于跨领域集成测试带来的挑战,例如构建集成环境、创建和维护契约等。
  • 运维人员: 对于 IT 结构相对传统的组织而言,划分服务并分别进行部署(通常与现有服务一同持续部署)是一项艰巨的挑战。保守派架构师天真地认为组件和运维模块化是一回事,但事实通常并非如此。自动化的 DevOps 任务是成功的关键,例如自动化的服务器配置和部署。
  • 数据人员: DBA 必须应对新的数据粒度、事务和记录系统问题。

全功能团队的目标之一便是消除协调摩擦。传统的团队彼此独立,开发人员通常需要等 DBA 做出变更或等运维人员提供资源。同一个团队包含各种角色能消除不同团队协调所产生的偶然摩擦。

项目间可以尝试共享受限的资源。

通过围绕领域组建架构和团队,现在可以由同一个团队处理常见的变更单元,从而减少了团队间的摩擦。以领域为中心的架构仍然使用分层架构来发挥其优势,例如关注点分离。举个例子,某个微服务的实现或许依赖于分层架构的框架,使得团队可以轻松替换某个技术层。微服务将技术架构封装在领域范围内,颠覆了传统的关系。

 围绕业务能力组织团队

在大多数组织中,通过采用开源软件,架构师渐渐摆脱了商业软件的束缚。共享资源架构存在固有的问题,它会引起系统各部分间无意的干扰。现在开发人员可以创建定制的环境和功能,更容易将重点从技术架构上转移到以领域为中心的架构,进而更好地匹配大多数软件项目中的常见变更单元。

产品高于项目

在大多数组织中,软件项目的工作流程是通用的。确定一个问题,组建开发团队,然后着手解决问题,直至“完成”,紧接着将软件移交给运维团队进行后续的管理、升级和维护工作。随后项目团队转向下一个问题。

这导致了许多常见问题。首先,由于团队转向了其他问题,通常很难进行漏洞修复和其他维护工作。其次,由于开发人员不参与代码运维的相关工作,因此他们不太关心质量等问题。通常,开发人员和他们运行的代码的间接层越多,他们与代码之间的联系就越少。有时这会导致在不同团队间产生对立心态,这并不奇怪,因为很多组织结构催生了员工间的冲突。通过将软件视为产品,公司能在三个方面实现转变。第一,与项目的生命周期不同,产品的生命更长久。全功能团队(通常基于康威逆定律)与产品保持联系。第二,每个产品都有一个负责人,他会主张在体系中使用该产品,并管理其需求。第三,由于是全功能团队,团队拥有产品所需的各种角色,例如业务分析师、开发人员、质量保障人员、DBA、运维人员等。

从项目心态转变为产品心态的真正目标是得到公司的长期支持。

亚马逊以其产品团队的组织方式而闻名,他们称之为“两个比萨团队”。其理论是,两个大的比萨就够任何一个团队吃了。这种划分方式背后的动机更多是为了沟通方便而不是控制团队大小,因为在更大的团队中,成员必须和更多的人沟通。每个团队都是全功能团队,并且都奉行着“谁构建,谁运行”的理念,这意味着每个团队全权负责其服务,包括运维工作。

小的全功能团队还充分利用了人性。

根据人们与生俱来的社会行为构建高度负责的团队,能使团队成员更加负责。

构建全功能团队可以防止不同团队间的相互指责,并让团队产生主人翁意识,激励团队成员做到最好。

应对外部变化

我们提倡在技术架构、团队结构等各方面都构建高度解耦的组件,从而将演进能力最大化,但在现实世界中,为了能协同解决领域问题,组件必须交互来共享信息。

我们构建适应度函数来保护架构中的维度免于演进副作用的影响。微服务架构中的一个常见实践便是采用消费者驱动的契约,即原子集成架构适应度函数。

演进式架构的增量变更默认开发团队具备一定的工程成熟度。例如,如果团队正在采用消费者驱动的契约,但他们的构建偶尔会损坏几天,那么他们无法得知集成点是否仍然有效。采用工程实践通过适应度函数来监督实践可以为开发人员减轻做大量手动工作的痛苦,但这需要一定的成熟度才能成功。

团队成员间的连接数

很多公司意识到,大型开发团队的效果不佳,J. Richard Hackman(知名团队动力专家)解释了该现象的原因:

1
人与人之间的连接数 = n(n-1) / 2

因此,构建小型团队是为了减少沟通连接。并且,为了消除不同团队间协作所产生的人为摩擦,这些小型团队需要是全功能团队。每个团队无须了解其他团队所做的事情,除非团队之间存在集成点。即便如此,也应该使用适应度函数保证集成点的完整性。

团队的耦合特征

文化

架构师应该关注工程师构建系统的方式并注意组织所奖励的行为。架构师选择工具和进行设计的活动和决策过程会影响软件的演进能力。好的架构师会担任领导角色,为开发人员构建系统确立技术文化和设计方法。他们教授和支持工程师构建演进式架构所需的技能。
架构师能通过提下列问题了解团队的工程文化:

  • • 是否团队所有人都知道什么是适应度函数,并考虑了新工具或产品选型对演进新适应度函数的影响?
  • • 团队是否衡量了系统与所定义的适应度函数的匹配程度?
  • • 工程师是否理解内聚和耦合?
  • • 是否讨论了什么领域和技术概念该整合到一起?
  • • 团队是基于变更能力还是基于他们想学习的技术来选择解决方案的?
  • • 团队对业务变更如何做出反应?他们是否难于完成小的变更,或在小的业务变更上花费了太多时间?

告诉我你的衡量标准,我就告诉你我会如何行动。 ——Eliyahu M. Goldratt 博士,The Haystack Syndrome

如果团队不习惯改变,那么架构师可以引入实践来优先应对这一点。例如,当团队考虑采用某个新的库或框架时,架构师可以让团队通过快速试验来进行明确的评估,看看新的库或框架会引入多少额外的耦合。工程师是否可以轻松地在该库或框架之外编写和测试代码,又或者新的库或框架是否需要配备额外的运行环境,而拖慢开发周期。

除了选择新的库和框架以外,审查代码需要考虑代码变更对未来变更的支持。如果系统某处突然需要一个额外的集成点,并且该集成点将会变化,那么需要涉及多少处更新呢?当然,开发人员必须留意过度设计,避免因为变更而永久地增加额外的复杂度或抽象。《重构》一书提供了相关建议。

交付新功能是驱动和奖励团队最常见的原因,但对于代码质量和演进性方面,只有在团队重视时才会予以考虑。负责演进式架构的架构师需要注意团队的行为,优先考虑那些有助于或支持演进能力的设计决策。

 试验文化

成功的演进离不开试验,但有些公司因为忙于交付而无暇进行试验。成功的试验是经常进行一些小型活动来尝试新的想法(从技术和产品角度)并将成功的试验集成到现有系统中。

衡量成功的真正标准是在 24 小时内能完成的试验数量。 ——Thomas Alva Edison

组织可以通过如下几种方式鼓励试验。

  • 从外部吸收想法: 很多公司派员工参加展会,并鼓励他们寻求新的技术、工具和能更好地解决问题的方法。还有公司将外部建议或顾问作为新想法的来源。
  • 鼓励明确的改进: 丰田公司因其持续改善(kaizen)文化而闻名。期望每个人能不断地寻求持续改善,特别是那些最了解问题并负责解决问题的人。
  • 进行探针试验并稳定下来: 探针试验是极限编程实践,让团队构建一个临时方案以快速了解某个棘手的技术问题、探索某个不熟悉的领域,或者提升估算的信心。使用探针试验会牺牲软件质量来提升学习速度。没人会把探针试验得到的方案直接用于生产环境,因为它缺少必要的考量和时间来使其切实可行。它是为学习而生的,不是精心设计的方案。
  • 创造创新时间: 谷歌因其“20% 的时间”闻名于世,其员工可以将其 20% 的工作时间用于任意项目。
    其他公司组织黑客马拉松并允许团队探索新产品或改进现有产品。Atlassian 定期召开 24 小时会议,并称其为 ShipIt。
  • 采用基于集合的开发方式: 基于集合的开发专注于探索多种方法。乍一看,由于额外的工作,多个可选项很费功夫,但在探索多个选项的同时,团队最终能更好地理解问题,并通过工具和方法找到真正的约束。令该方法有效的关键是在短时间(几天)内构建多个原型,从而获取更具体的数据和体验。在综合考虑多个竞选方案后,往往才能得出更好的方案。
  • 连接工程师和最终用户: 只有当团队清楚试验的影响,试验才会成功。在许多具有试验思维的企业里,团队和产品人员能直接看到决策对最终用户的影响,并被鼓励通过试验来探索这种影响。A/B 测试是企业应用这种试验思维的实践。企业的另一种实践是派团队和工程师观察用户是如何与软件交互来完成某项任务的。这种实践源于可用性社区的文章,让工程师了解最终用户的感受,以更好地理解用户需求,并通过新的想法来更好地满足他们。

首席财务官和预算

在演进式架构中,企业架构的一些传统功能必须反映不断变化的优先级,例如预算。过去,在软件开发领域,预测长期趋势的能力是预算的基础。

事实上,在架构量子和架构成本之间存在着有趣的联系。随着架构量子数量的增加,每个架构量子的成本降低,直到达到最佳点

p-20.png

首先,由于架构由较小的部分组成,问题需要分离得更加离散和明确。其次,物理量子数量的增加需要运维方面的自动化,因为当量子数量超过某个点后,人们将无法再手动处理这类事务。

然而,过小的架构量子有可能使绝对数量的成本高昂。例如,在微服务架构中,构建服务的粒度可以细到表单中的每个字段。在这样的粒度下,各个细小部分间的协调成本开始主导架构中的其他因素。因此,在图中的极端情况下,架构量子的绝对数量导致每个架构量子能获得的好处减少。

在演进式架构中,架构师追求合适的量子大小和对应成本之间的最佳点。每个公司情况各异。例如,市场迅猛发展,公司可能需要更快的变更速度,因此需要更小的架构量子。记住,新一代架构的出现速度与生产周期成正比,架构量子越小,生产周期越短。

构建企业适应度函数

在演进式架构中,企业架构师的工作主要围绕着架构指导企业级适应度函数展开。微服务架构反映了这一模式转变。由于在运维上各个服务彼此分离,所以不再需要考虑资源共享。相反,架构师指导架构中有明确目标的耦合点(例如服务模板)和平台选择。企业架构师通常负责共享基础设施功能,以及为了企业内部的一致性,将平台选择限制在一定范围内。

演进式架构赋予企业架构师的另一个新职责是定义企业级适应度函数。企业架构师通常负责企业级非功能需求,例如伸缩性和安全性。很多组织缺乏自动评估能力来评估在单个项目和总体上这些架构特征的表现。一旦项目采用了适应度函数来保护架构的各个部分,企业架构师可以利用相同的机制验证这些企业级的特征保持不变。
如果每个项目都使用部署流水线来将适应度函数应用于其构建中,企业架构师也可以在其中插入一些自己的适应度函数。这使得每个项目可以持续校验横切关注点,例如伸缩性、安全性及其他企业级问题,尽早发现缺陷。正如微服务项目共享服务模板可以统一技术架构,企业架构师可以使用部署流水线来推动跨项目的一致性测试。

从何开始

容易实现的目标

如果组织需要早期成功来证明这种方法,架构师可以选择最简单的问题来凸显演进式架构方法。通常,这是系统中在很大程度上已经解耦的一部分,而且最好不在任何依赖的关键路径上。团队可以通过增强模块性降低耦合来展示演进式架构的其他方面,如适应度函数和增量变更。构建更好的隔离可以使测试和适应度函数更具针对性。更好地隔离可部署单元使得构建部署流水线更容易,并为构建更强大的测试提供了平台。
在采取增量变更的环境中,衡量指标常附属于部署流水线。如果团队通过指标数据进行概念验证,开发人员应该在验证前后收集适当的指标数据。收集具体数据是开发人员审查其方法的最佳方式,记住实证胜于雄辩。
这种“最简单者优先”的方法将风险降到了最低,但可能牺牲价值,除非团队有幸找到既容易解决、价值也高的问题。对于那些持怀疑态度并想试水演进式架构的公司来说,这是很好的策略。

最高价值优先

除了“最简单者优先”外,另一种方法“最高价值优先”找到系统中最关键的部分,围绕它构建演进行为。公司可能出于以下几个原因采取该方法。第一,如果架构师确信要实现演进式架构,那么选择价值最高的部分就表明了决心。第二,对于那些仍在评估想法的公司,他们的架构师可能对这些技术在体系中的适用性感兴趣。因此优先选择价值最高的部分,便能明确演进式架构的长远价值。第三,如果架构师怀疑这些方法的适用性,那么用系统中最有价值的部分来审查这些概念,能够为是否继续提供了可行的数据。

测试

很多公司苦于系统缺乏测试。如果开发人员发现他们的代码库缺乏测试,在向演进式架构做出更具大胆的行动前,他们会添加一些关键测试。

通常,管理层不赞成开发人员进行只为代码库添加测试的项目。他们对这类活动持怀疑态度,特别是新功能无法如期实现时。于是,架构师应该将模块化增强和高级功能测试结合起来。用单元测试封装功能来为测试驱动开发(TDD)等工程实践提供更好的基础。但更新代码库需要时间,因而在重建代码之前,开发人员应该对一些行为添加粗粒度的功能测试,以验证系统的总体行为不会因为重建而改变。
对演进式架构的增量变更而言,测试是关键的组件,并且适应度函数也在积极地利用测试。因此,至少在某种程度上,测试使这些技术成为可能,而且实现演进式架构的难易程度和测试的综合性密切相关。

基础设施

对一些公司而言,构建新能力需要时间,而运维团队常受困于缺乏创新。对那些基础设施功能失调的公司来说,构建演进式架构之前可能需要先解决这些问题。基础设施问题通常有很多形式。例如,有些公司将所有的运维工作外包给别的公司,因此无法控制其体系的关键部分。当需要承担跨公司协调的额外开销时,DevOps 的难度呈级数式上升。
另一个常见的基础设施功能失调是开发和运维之间无法穿透的防火墙,因为开发人员根本不了解代码最终将如何运行。这种结构在部门间充斥着权力博弈的公司里很常见,因为每个团队都各行其是。
最后,在某些组织中,架构师和开发人员都忽视好的实践,而不断引入大量技术债,这些技术债体现在基础设施中。一些公司甚至连运行环境和运行主体都不清楚,也不了解架构和基础设施之间交互的基本知识。

演进式架构的未来

基于 AI 的适应度函数

生成式测试

在很多函数式编程社区中,生成测试是广受欢迎的常见实践。传统单元测试包含对每个测试用例结果正确与否的判断。然而,通过生成测试,开发人员运行大量测试并抓取结果,然后对结果进行统计分析来查找反常的行为。例如最常用的边界值检查,传统单元测试检查已知的数字临界点(负数、不断增加的数字等),但无法覆盖意外的少数情况。生成测试检查每一个可能的数值并报告失败的少数情况。

为什么(不)呢

架构中没有灵丹妙药。如果无法从演进性中获益,我们不建议在项目中为其付出额外的成本和精力。

公司为何决定构建演进式架构

    1. 可预测性与可演进性
    1. 规模
    1. 高级业务能力
    1. 以生产周期为业务指标
    1. 在量子级别隔离架构特征

企业为何选择不构建演进式架构

    1. 大泥团无法演进
    1. 其他架构特征占主导地位
    1. 牺牲架构
    1. 计划即将停止业务

说服他人

架构师和开发人员希望非技术人员和管理层理解演进式架构的好处。当组织的某些部分被必要的变更扰乱时尤为如此。例如,当开发人员指出运维部门工作不当时,通常会遇到阻力。

与其尝试说服组织中的保守人群,不如展示这些想法对其实践的改进。