企业集成使用 REST
大多数内部 REST API 都是为单个集成点专门构建的一次性 API。在本文中,我将讨论非公共 API 的约束和灵活性,以及从跨多个团队进行大规模 RESTful 集成中吸取的经验教训。
2013 年 11 月 18 日
为什么企业集成要使用 REST?
遗留系统替换很困难。事实上,我敢打赌,大规模遗留系统替换是整个 IT 行业中最难的工作。我们大多数人永远不会编写公司几十年来一直依赖的软件,但这种软件在大型遗留系统替换中很常见,其作者应该受到赞扬。尽管如此,这种软件早于我们对设计、测试和良好操作实践的了解,因此可能难以理解和更改。
我们经常进入这种替换,想象着新系统将拥有的原始架构,并且大大低估了这项工作的难度。在面对一个高度纠缠、定制构建、难以更改的遗留系统的情况下,某些模式出现了,这是可以理解的。首先,我们购买供应商软件包,目的是将内部开发工作量减少到集成,并发誓永远不再依赖于没有外部支持的定制系统。其次,我们找到了面向服务的架构进行集成,目的是将可替换性烘焙到新系统的各个部分,并减少未来不可避免的遗留系统替换项目的痛苦。
Thoughtworks 参与了几个大型遗留系统替换项目,尽管我们不能总是公开谈论它们。REST over HTTP 是许多此类项目的诱人选择,也是我们通常倡导的选择。它易于使用和理解,并且不需要重量级的框架或工具链即可开始使用。它非常适合测试,并将许多操作问题减少到与管理网站相同的实践。从架构上讲,REST 已经证明了可扩展性,并且与领域建模很好地融合在一起。
许多关于 REST 的在线讨论深入探讨了内容类型和超媒体作为应用程序状态引擎 (HATEOAS) 的细节,但没有提供任何关于使 REST 适用于大型集成项目的工程和管理实践的建议。我的假设是,此类项目中的成功与理解 HATEOAS 的细微差别关系不大,而与理解部署和测试策略等方面关系更大。以下是我从进行大规模 RESTful 集成中吸取的经验教训。
定义逻辑环境 - 每个需求一个
许多大型 IT 机构从大型机或大型供应商安装中继承了昂贵的环境,并试图将服务硬塞到一组预定义的僵化环境中。不幸的是,管理所有开发人员都必须使用的企业级环境集放弃了 RESTful 服务的主要优势之一:轻量级环境。虽然服务可能是需要大量马力的应用程序前面的一个外观,但服务本身往往易于部署和托管,并且可以通过浏览器和命令行进行测试。此外,使用类似 ATOM 的事件馈送等技术避免了在启动新环境时需要广泛的中介基础设施。关键的见解是理解逻辑环境的概念。
逻辑环境是一组适当隔离的相互关联的应用程序、服务和基础设施组件,这些组件是满足业务或开发需求所必需的。
满足开发需求所需的组件可能与满足业务需求所需的组件对于不同的团队和角色来说大不相同。大型组织中的很少有开发人员期望运行一个隔离的完整堆栈环境,并且隔离应该只进行到使开发人员高效的程度。例如,在零售项目中,订单输入团队中的开发人员可能需要产品目录和客户管理的服务,但可能不需要仓库管理的服务。在生产中,每个服务可能都有一个支持它们的负载均衡集群,但开发人员和 QA 更重视隔离而不是性能和可用性。在极端情况下,不同的开发人员可能在同一台 VM 上拥有不同的逻辑环境。在这种情况下,可以通过将端口和数据库名称作为环境配置的一部分来实现隔离。
图 1:环境隔离独立于托管环境的硬件
共享环境的另一个问题是每个人都在同一时间升级,这在混乱的开发世界中往往不合适。更好的做法是将发布计划掌握在受其影响的个人手中 - 这对于生产发布和开发人员升级其沙盒环境中依赖的服务来说同样适用。这对于 QA 来说尤其重要,QA 的角色需要在他们的逻辑环境中管理发布节奏。测试需要一组已知且稳定的服务版本,开发人员在版本已知的情况下更容易修复错误。
在一个大型项目中,我们使用 Yaml 定义了环境的声明性描述。顶级键将定义一个环境名称,子键定义了该环境所需的服務,下一级键定义了每个服务的特定于环境的配置。
order-entry-dev: product: webservers: [localhost] port: 8080 logPath: /var/log/product dbserver: localhost dbname: product customer: webservers: [localhost] port: 9080 logPath: /var/log/customer dbserver: localhost dbname: customer
对部署自动化的无情关注以及对基础设施的适当投资意味着某些服务存在于 50 多个逻辑环境中,对于习惯于大型机的公司来说,这是一个令人难以置信的数字。像 Ansible 这样的工具有助于声明性地描述环境,而无需大量的前期投资。为了允许开发人员使用的轻量级临时环境,通常将 localhost
作为服务器名称定义一个环境,可以使用类似 Vagrant 的工具在本地虚拟机上启动它。这允许使用相同的环境配置但不同的 VM 来实现环境弹性。
包怎么办?
供应商软件包会使环境创建变得复杂,因为它们很少构建为支持轻松部署和环境弹性。它们中的许多都附带一个安装文档,该文档随每次升级而更改,并且没有可靠的机制来在多个环境中重放更改。许可证也增加了障碍,尽管大多数供应商都会提供低成本的开发许可证。
如果您发现自己负担着难以部署的供应商软件包,则有两种补救策略。如果软件包在安装期间不需要复杂的许可证,您可能能够完成供应商的工作,即自动执行安装和升级。或者,您可以设置一个可克隆的 VM,这为您提供了弹性,但使升级变得复杂。本质上,这是配置管理讨论中 烘焙与油炸 的区别。
当两种选择都不可用时,还有其他方法可以实现一定程度的隔离,尽管没有一种方法可以与实际的环境隔离相媲美。可能有一种方法可以使用应用程序中的自然数据边界来允许一定程度的开发人员隔离。不同的用户帐户往往是一个容易的数据边界,尽管用户往往共享全局状态。更好的做法是为各个开发人员提供不同的租户,因为多租户应用程序旨在防止跨租户流量。这种方法显然是一种变通方法,存在扩展挑战,并且不提供发布计划独立性。
部署和环境管理的简便性应该是选择软件包的标准之一。
当然,最好的解决方案是在供应商选择过程中审查这些操作注意事项。部署和环境管理的简便性应该是选择软件包的标准之一。在选择过程中,我们不仅要考虑功能集和适用性,还要考虑集成简便性和集成开发人员的生产力。
仅在万不得已时使用版本控制
逻辑环境定义的一个重要推论是凝聚力的概念 - 每个环境应该只有一个给定服务的实例。不幸的是,在每个团队都以不同速度前进的大型项目中,很容易遇到通常与编译时依赖项相关的经典钻石依赖问题。
图 2:不兼容的版本要求
根据我的经验,RESTful 架构师首先想到的解决方案之一是版本控制。我持一种更有争议的观点。借用 Jamie Zawinski 对正则表达式的 著名挖苦
有些人,当遇到问题时,会想“我知道,我会使用版本控制。”现在他们有了 2.1.0 个问题。
问题是版本控制会显着地使理解、测试和排查系统的故障变得复杂。一旦您拥有多个不兼容的相同服务的版本,这些版本被多个消费者使用,开发人员就必须在所有支持的版本中维护错误修复。如果它们在单个代码库中维护,开发人员可能会通过向新版本添加新功能来破坏旧版本,仅仅因为共享代码路径。如果版本是独立部署的,则操作足迹将变得更加复杂,难以监控和支持。这种额外的复杂性要么被忽视,要么被简化相互依赖服务的发布过程所证明。但是,通过有纪律地使用 基于消费者的测试(将在下一节中讨论),可以显着缓解发布复杂性,这是一个企业 API 可以使用而公共 API 无法使用的有趣选项。
对于许多类型的更改,可以通过其他技术来避免版本控制。 Postel 定律 指出,您应该在接受方面宽容,在发送方面保守。这是服务开发的明智建议。不幸的是,某些反序列化器的默认行为违反了这一建议,当消费者提供意外字段时会抛出异常。这是不幸的,因为它可能只是消费者在不期望消费的情况下通过网络传递额外的诊断信息,或者可能是消费者正在为生产者的未来更新做准备,在生产者准备好处理之前传递新字段。它可能是生产者向响应主体添加了一个新字段,消费者可以自由忽略。所有这些情况都不值得抛出异常。
自动反序列化通常会陷入将消费者和生产者耦合的陷阱。
通常可以配置反序列化器以更容忍。虽然这不是主流建议,但我更喜欢完全避免自动反序列化。自动反序列化通常会陷入 WSDL 的陷阱,即通过在两者中复制静态类结构来将消费者和生产者耦合在一起。手工编码的反序列化也允许对传入数据做出更少的假设。正如 Martin Fowler 在 Tolerant Reader 中所描述的,使用像 //order
这样的 XPath 表达式允许在 order
元素之上进行嵌套更改,而不会破坏反序列化。
在服务契约设计方面,前期投入一些设计工作可以带来巨大的回报。在一个项目中,契约的属性大小写不一致,例如 firstName
和 LastName
。当消费者团队的开发人员根据该契约进行开发时,他们肯定会暗自抱怨,但当契约随后在未经通知的情况下被“修复”时,他们会大声抱怨。
在大型 SOA 项目中,我更喜欢在服务边界编写许多故事。这确实带来了一个明显的挑战,即确保端到端功能与业务目标一致(我将在 后面 讨论这个问题),但它也具有许多优势。首先,它们自然倾向于让技术主管或架构师参与分析,让他们有时间思考概念的粒度并模拟契约,以形成对资源的连贯描述。编写验收标准需要考虑各种错误条件和响应代码。在服务边界进行 QA 审查提供了另一个机会来发现明显的错误,例如上面提到的大小写问题。最后,与测试驱动开发通过确保每个类至少有两个使用者(它被编写的使用者和测试)来鼓励松耦合的方式类似,服务边界故事有助于确保服务端点是可重用的,而不是过度特定于它最初被开发的端到端功能。能够独立测试服务端点可以防止它与它支持的端到端工作流程过于紧密地耦合。
生产者还可以使用 语义版本控制 来指示何时需要进行重大更改。这是一种简单的方案,为版本号的 MAJOR.MINOR.PATCH 部分添加了众所周知的含义,包括为重大更改增加 MAJOR 版本号。如果您有一套规范的消费者驱动测试(稍后描述),您可能能够在同一个版本中升级所有消费者。当然,这并不总是可能的,在某些时候,支持同一服务的多个版本的复杂性可能是合理的,因为协调其依赖项的发布更加复杂。一旦您需要使用版本控制,您需要在两种主要技术之间进行选择:URL 版本控制和 HTTP 头部版本控制。在您的选择中,重要的是要了解您选择的版本控制方案首先是一种发布管理策略。
URL 版本控制 涉及在 URL 中包含版本号(例如 /customers/v1/…
- 语义版本控制中的 MAJOR 版本就足够了)。对于这种情况,消费者将不得不等到生产者发布之后才能使用。URL 版本控制的优势在于它非常直观,并且可以通过浏览器进行测试。然而,当一个服务提供指向另一个服务的链接,并期望消费者遵循该链接时(这在尝试使用超媒体来驱动工作流时最为常见),URL 版本控制存在一个重要的缺陷。如果超链接的服务升级到新版本,协调这些依赖项的升级可能会变得很棘手。例如,客户服务链接到产品服务,而 UI 盲目地遵循该链接,因为版本嵌入在提供的链接中,所以 UI 不知道产品版本。当我们对产品服务进行非向后兼容的升级时,我们最终希望升级客户服务以更新链接,但我们必须小心地首先升级 UI。在这种情况下,最简单的解决方案通常是同时升级所有三个组件,这实际上与根本不进行版本控制的发布管理策略相同。
Duncan Beaumont Cragg 建议扩展 URL 空间而不是进行版本控制。当您需要进行不兼容的更改时,只需创建一个新的资源,而不是对现有资源进行版本控制。表面上,/customers/v2/profile
和 /customers/extendedProfile
之间存在细微的差别。它们甚至可能以相同的方式实现。但是,从通信的角度来看,这两个选项之间存在天壤之别。版本控制是一个更广泛的话题,在大型组织中,版本控制通常需要与多个外部团队进行协调,包括架构和发布管理,而团队往往拥有添加新资源的自主权。
HTTP 头部版本控制 将信息放入 HTTP 头部,指示消费者将接受哪个版本。这最常与 Content-Type
相关联,例如 application/vnd.acme.customer-v1+json
,它允许内容协商来管理版本。客户端可以在 Accept
头部中发送一个支持版本的列表,服务器可以在 Content-Type
头部中响应使用的版本,或者对于不支持的版本请求发送 415 HTTP 状态代码。这吸引了纯粹的 RESTafarians,并且不受上面提到的 URL 版本控制的缺陷的影响,因为最终的消费者可以决定请求哪个版本。当然,它变得更难通过浏览器进行测试,并且在开发时更容易被忽视。在请求和响应主体中添加版本号(如果存在)可以增强头部版本控制,以提供额外的可见性。头部版本控制还会带来缓存方面的挑战。Vary
头部旨在使同一个 URL 以不同的方式进行缓存,但它会增加网络配置的复杂性,并且您可能会遇到忽略 Vary
头部的配置错误的网络缓存。
使用基于消费者的测试来捕获集成问题
基于消费者的测试是我所见过的最有价值的实践之一,它使 REST 在企业中得以扩展,但在深入研究之前,我们需要了解部署管道的概念。
部署管道
在他们关于 持续交付 的开创性著作中,Jez Humble 和 Dave Farley 将部署管道描述为代码从签入到生产的路径。如果我们在大型组织中跟踪一个签入到生产发布,我们可能会发现以下步骤
- 开发人员签入新代码。
- 持续集成工具编译、打包并针对源代码运行单元测试(通常称为提交阶段)。
- 持续集成工具部署到沙箱环境,以针对独立部署的服务运行一组自动化测试。
- 应用程序团队部署到展示环境,在该环境中,业务利益相关者进行内部用户验收。
- 中央 QA 团队部署到系统集成测试 (SIT) 环境,在该环境中,他们与其他应用程序和服务一起进行测试。
- 发布管理部署到预生产环境,在该环境中,应用程序团队、安全团队和运维团队对发布质量进行一些手动验证。
- 发布管理部署到生产环境。
将该工作流建模为一系列阶段,就形成了我们的部署管道,它让我们可以可视化服务在每个管道阶段的版本
图 3:简单的部署管道
上面描述的管道描述了单个服务的独立流程。当我们添加集成时,大型 SOA 项目的实际情况要复杂得多
图 4:集成的部署管道
请注意,在集成管道中,我在早期阶段省略了很多细节。不同的团队通常对面向团队的管道部分有不同的阶段,这些阶段可能与真正的外部依赖项隔离。例如,一些团队可能会添加手动 QA 或性能测试阶段。在组织阶段(SIT、预生产和生产)中,所有代码通常以相同的方式进行,并且这些阶段测试一起集成的服务。您越深入管道,集成程度就越高,环境就越接近生产环境。
在管道早期阶段,对存根的投资可以带来巨大的回报。当您的构建变为红色时,知道这是因为您的团队内部的代码出现问题,而不是因为您正在测试的环境发生了变化,这令人欣慰。使用测试替身有助于消除测试中 非确定性 的主要原因。像 VCR 这样的库允许您记录真实的 service 调用,并在以后的自动化测试运行期间重放响应。使用测试替身确实会让您暴露于集成问题,但企业中的大多数复杂性都涉及集成。幸运的是,Ian Robinson 描述 了一个解决集成复杂性的解决方案,它与我们的部署管道非常契合。
基于消费者的测试
基于消费者的测试是违反直觉的,因为它依赖于消费者为生产者编写测试。在编写契约测试时,消费者会针对它使用的服务编写测试,以确认服务契约满足消费者的需求。例如,订单输入团队可能依赖于产品服务的代码和描述,以及月费是数字,因此他们会编写这样的测试
[Test] public void ValidateProductAttributes() { var url = UrlForTestProduct(); var response = new HttpResource(url) .ThatAccepts("application/xml") .Get(); Assert.That(response.StatusCode, Is.EqualTo(200)); AssertHasXPath(response.Body, "//productCode"); AssertHasXPath(response.Body, "//description"); AssertHasXPath(response.Body, "//monthlyCharge"); AssertNumeric(ValueFor(response.Body, "//monthlyCharge")); }
这在部署管道中实现了一个巧妙的技巧。在单个服务通过其内部管道之后,所有服务和消费者都会经历一个统一的契约测试阶段。它可以由消费者更改测试或生产者提交服务更改来触发。每个消费者都会针对更改后的服务的最新版本运行其测试,任何失败都会阻止新版本在管道中进行。
在我们的示例中,假设订单输入服务的最新构建进展到契约测试阶段
图 5:契约测试阶段
它依赖于产品和计费服务,并且由于新代码可能更改了其契约测试,因此它会针对产品和计费服务的最新版本运行其测试,以进入契约测试阶段。UI 依赖于订单输入服务,因此契约测试阶段中 UI 代码的最新版本会针对订单输入运行其测试。这意味着服务及其消费者测试都需要在契约测试阶段中。由于产品服务没有依赖项,因此它没有消费者测试。让我们再看看我们的菱形;这次请注意,每个服务只有一个依赖版本。
图 6:契约测试运行示例
仅触发与特定更改相关的测试可能很棘手,但您可以通过在每次将新服务部署到管道的契约测试阶段时运行所有契约测试来实现很大程度上的自动化。这将包括 图 6 中的灰色线条,这些线条与引入的更改无关。这是测试运行速度和您愿意使依赖项管理变得多么复杂的权衡。
假设所有测试都通过,我们现在有一组已证明可以协同工作的服务。我们可以将它们以及它们的关联版本一起记录下来。
图 7:契约测试运行成功
此时,所有参与的服务的版本都将捕获在一个可部署的工件集中,或称为 DAS。此 DAS 可以成为部署管道更高阶段的单个可部署工件。或者,如果需要支持独立发布,它可以提供兼容性参考。无论哪种方式,它都代表着一组已证明可以相互通信的组件。
如果新的订单输入代码破坏了 UI 消费者测试,则组合工件将不会进行。这并不一定表明服务存在问题;这可能是消费者对契约的误解。或者,这可能是生产者有意进行的重大更改,尽管根据语义版本控制礼仪,如果这是故意的,他们应该增加其 MAJOR 号码。无论如何,契约测试失败都会在早期触发两个团队之间的对话,而不是在几天或几周后消费者更新其环境时才进行对话。
图 8:破坏契约更改
数据怎么办?
在进行全面的基于消费者的测试时,其中一个较难的挑战是生成有价值的测试数据。上面的契约测试假设存在已知的测试产品。我使用 UrlForTestProduct
方法隐藏了这个假设,该方法可能需要一个特定产品的 URL。在测试时对数据可用性进行硬编码假设可能是一种脆弱的方法,因为无法保证产品在未来会继续存在。此外,某些端点可能需要跨多个服务的数据一致性才能正常工作。例如,订单输入可能会将订单提交给账单,并附带一组相关产品。账单端点需要有一致的产品数据集。
一种更稳健的策略是在测试运行期间创建测试数据,因为这样可以保证数据在使用之前存在。这预先假设每个服务都允许创建新的资源,但这并不总是可能的,尽管在我们的一位客户那里,我们添加了仅供测试的端点来方便测试数据的创建。这些端点在生产环境中没有公开。这种策略仍然可能需要在上面的账单示例中进行复杂的测试设置。创建测试产品后,您需要强制与订单输入和账单服务同步,这通常是异步操作。另一种策略是让每个服务发布一套连贯的黄金测试数据,并保证其稳定性。这通常最好封装在一些更高层次的数据边界中。例如,您可以创建一个测试营销品牌或业务线,跨越所有服务,其唯一目的是提供不会对生产产生影响的假数据。无论哪种方式,整理测试数据都应该是实现稳健的服务部署管道的首要任务。
不要让系统独占资源
对数据边界定义不当是架构师可能犯的最昂贵的错误之一。一种常见的反模式是尝试将有关实体的所有信息存储在一个数据存储中,并在需要时将其导出到依赖系统,这种策略是由对主数据管理 (MDM) 的肤浅理解所鼓励的。这种策略的问题在于它违反了 康威定律,该定律指出软件架构必然会反映构建它们的组织的结构 [1]。
让我们看一个产品目录的例子。在遗留系统中,一个团队输入新的产品代码及其相关的费率。一个配置团队使用另一个工具输入适当的配置,例如下游电话配置系统所需的代码,以及在另一个应用程序中打开电视频道的服务代码。财务团队在他们的财务工具中输入了有关产品的总账信息,而开票团队在另一个应用程序中添加了特殊的开票规则。这些团队之间的沟通由业务管理,而不是由技术管理。
迁移到一个用于所有产品数据的单一应用程序可能是一场灾难性的尝试,主要是因为这些不同的业务团队对产品的定义都不同。客户服务代表认为的单一产品可能需要拆分为两个产品才能支持适当的会计。开票团队非常关注通过简化发票来降低呼叫费率,因此他们经常需要将两个产品代码合并到发票上的单行中。当然,还有营销团队,他们肆意地重新定义产品。试图将整个企业对产品的看法合理化为一个单一的目录只会使该目录变得脆弱且不灵活。实际上,整个企业现在必须进入产品目录才能进行更改。更改的表面积显著增加,更改的连锁反应变得难以推理。康威定律被违反,因为系统的通信路径不再代表组织的通信路径。
图 9:数据建模灾难
我对试图标准化像产品这样重要的事物在整个企业及其集成合作伙伴中的规范表示的通用数据模型持怀疑态度。电信行业创建了这样一个数据模型,称为 TM Forum 共享信息/数据模型 (TMF SID)。该模型声称,通过对 SID 进行标准化,不同的公司或公司内部的不同部门可以使用相同的术语来描述相同的逻辑实体。我怀疑如果没有一些成功,这样一项大规模的尝试将无法持续下去,但我还没有看到这些成功。
我推荐的解决方案借鉴了 Eric Evans 的 领域驱动设计,是在一个 有界上下文 中包装每个团队对产品的定义。有界上下文是一个语言边界,在这个边界内,术语在任何使用的地方都保证具有相同的含义。在有界上下文之外,不存在这样的保证,必须通过集成和业务流程的组合来管理转换。现在可以以符合康威定律的方式对财务产品与可配置产品之间的差异进行建模。
围绕软件包提供定义明确的有界上下文是使用外观服务的绝佳用途。供应商不断发展其软件包以支持多个业务的一个自然结果是,软件包的功能集可能超出了您企业的需求。将与软件包的所有通信包装在外观服务后面,可以让您将软件包数据限制并塑造成您的业务流程定义的有界上下文,并提供一个干净的 API,将软件包特定的术语转换为您的企业定义的语言。
对集中式实体使用一小部分数据
我们在技术上实现这一点的方法是缩减驻留在产品目录中的主数据集,并将该数据复制到其他系统,并在这些系统中对其进行增强。这种技术有时与“旋转椅集成”相关联,但它并不相同。在真正的旋转椅集成中,相同的信息输入到多个不直接集成的应用程序中。在有界上下文中,主数据被复制,并在依赖应用程序中被上下文增强和转换。
定义像产品这样的中心资源实体的关键是了解创建新产品的业务团队认为产品是什么。在我们的假设示例中,他们决定产品定义了一些基本费率和所有部门使用的通用描述信息。这给了我们产品服务中产品的定义:产品是我们可以出售给客户的具有单一费率的东西。在遗留世界中,创建产品后,产品团队会向其他部门发送电子邮件。此事件可能值得自动化,我们可以通过在我们的服务上公开的队列或更改提要上发布来实现。但是,我们不应该假装可以自动化此事件触发的业务流程,也不应该将该业务流程移到中心目录中。
当财务部门收到它时,他们必须决定如何分解产品。假设它是一个捆绑的电视体育频道包,以折扣价提供,这个概念很好地符合产品团队对产品的定义。但是,财务部门需要确保捆绑包中的每个体育台都收到他们的版税,因此他们需要将单一费率发送到不同的总账桶。现在我们有了财务定义:产品是收入与总账账户的关联。在刚刚描述的用例中,我们可以想象财务应用程序接收 NewProduct
事件,并提供一个界面供用户将收入的一部分分配到总账账户。
每个业务部门都有一个不同的通用实体模型,并在它们的有界上下文之间进行明确的转换
当账单部门收到该事件时,他们需要决定是否对该软件包进行按比例分配。也许,出于对太多客户订购按比例分配的体育频道,并在观看完超级碗比赛后第二天就取消订阅的担忧,他们决定这个特定的体育频道包需要预先支付一个月的费用。我们从产品的定义开始:产品是向客户收取的定期费用,并用一组可能很复杂的配置进行增强,这些配置超出了简单的月费率。
开票部门将产品定义为发票上的单行。他们可能会决定,也许是受营销策略驱使,购买了两个独立体育捆绑包的客户应该在发票上只看到一行,名为“超级体育套餐”,金额为总和。同样,我们可以想象一个应用程序来促进接收 NewProduct
事件并允许这种合并,或者我们可以想象开发人员在引入此类产品时编写新的规则。
图 10:使用有界上下文进行集成示例
此示例显示了四个不同的有界上下文,以及使用通知提要传播的 NewProduct
事件,这是一种常见的 RESTful 方法。当创建新产品时,产品服务会将新产品记录为事件,并将其公开在一个端点上。消费者轮询该端点以接收自上次轮询以来的所有事件,该端点可能类似于 /notifications?since=2013-10-01
。
使用史诗来协调业务功能
之前,我建议将服务端点视为故事边界,但需要注意的是,我们可能会失去传统敏捷故事的好处——即它们与业务功能保持一致。由于团队的工作优先级和速度不同,这个问题在大型 SOA 中会加剧,我们可能会失去全局观。想象一下,一个对产品的第一个月进行计费的业务功能。真正的业务流程可能需要在这一点之前进行一系列服务调用,包括客户创建、产品查找、订单创建和现场技术人员批准。在规模上,这些服务端点将由不同的团队实现。
敏捷工具箱一直包括史诗,用于协调沿单个高级功能的一组故事。我建议将它们视为大型 SOA 项目程序管理的一级公民。事实上,我相信我们附加给用户故事的大部分仪式都应该在史诗级别进行,因为史诗通常代表我们对需求的业务友好描述。
例如,创建客户史诗可能涉及订单输入和账单团队,以及客户管理团队,每个团队都有各自的服务导向或应用程序特定的故事来跟踪他们的工作。在我们的假设示例中,服务由一个独立团队开发的用户界面使用,我们可能无法在完成整个系统流程之前向业务展示我们的劳动成果。
史诗的一些关注和管理可以从较小规模的参与中的故事实践中提升。对史诗进行架构审查以定义跨职能需求,以及业务分析师定义验收需求,可以帮助我们牢记全局。跨团队展示应该在史诗级别进行管理,这些展示可能是第一个展示实际业务用户流程的展示。
程序级指标将史诗作为跟踪速度的主要指标,因为团队用户故事速度可能会给出错误的进度感。
重要的考虑因素是,程序级指标将史诗作为跟踪速度的主要指标,因为团队用户故事速度可能会给出错误的进度感。要观察的症状是,当速度燃尽图显示程序按时交付,但似乎什么都不起作用时。我曾经参与过一个项目,该项目就是这种情况,项目基于各个团队跟踪的单个故事速度。有些团队按时完成,有些团队略微落后于进度,但我们无法在数月的开发之后向业务展示任何成果。简单地将程序级燃尽图更改为显示已完成的史诗数量,而不是已完成的故事数量,就很有启发性。尽管各个团队显示出显著的进展,但我们只完成了一个史诗。更糟糕的是,至少有三分之二的史诗中至少有一个故事需要发布,并且同时在进行中。传统的软件看板方法试图在故事级别限制正在进行的工作。当我们意识到问题的范围时,我们能够通过重新排序故事开发来纠正方向,以限制同时进行的史诗数量。
总结
无论采用何种技术或架构,扩展软件开发都是一项棘手的任务。我们经常自欺欺人,假装它只是“集成”。Eric Evans 在 曾经说过,在任何大型系统中,总会有一些部分设计不佳。我的经验——即使是与高技能团队合作——也让我相信他是对的。因此,我们集成的主要目标是确保我们能够从另一个子系统的设计中隔离出来。
我主张采用 RESTful 服务集成策略。我认为 REST 使开发更简单,并且由于 RESTful 消息通常是自描述的,因此测试和故障排除也更简单。然而,它远非有些人想象的灵丹妙药,并且在规模化 RESTful 集成时需要关注上述教训。我自己的经验让我相信
- 环境隔离很重要。关注部署自动化至关重要。选择不遵循良好部署实践的软件包会减慢所有需要与该软件包集成的开发人员的速度。
- 过早地进行版本控制会给系统增加不必要的复杂性。容忍反序列化和基于端点的用户故事分析等实践可以帮助您延迟版本控制,即使您随后添加了语义版本控制支持,这些实践也是有用的。
- 使用消费者测试可以极大地降低升级一组相互依赖的服务的发布管理复杂性,并进一步帮助延迟版本控制。
- 试图让一项服务控制有关实体的所有数据是灾难性的。不要忽视业务流程以及业务对实体的不同定义。
- 在规模化的情况下,协调业务功能不太可能在用户故事级别进行。需要史诗来排序业务发布。
在 RESTful 讨论中,超媒体、内容协商和统一接口获得了所有关注,它们是有价值的技术,但要使集成解决方案能够扩展,我们需要从 REST 的机制中抽离出来,关注社会和组织问题。成功地解决这些问题并不意味着每个单独的服务或组件都将设计良好。它 *确实* 意味着您将拥有可靠的实践来逐步交付业务价值并确保集成层具有适当的健壮性,而这些实践可能会决定成功交付和失败交付之间的区别。
致谢
特别感谢 Martin Fowler、Damien Del Russo、Danilo Sato、Duncan Beaumont Cragg 和 Jennifer Smith 对本文的早期反馈。许多想法来自我将 Thoughtworks 同事作为不同项目的试金石。虽然这里无法一一列举,但我希望特别感谢 Ryan Murray、Mike Mason 和 Manoj Mahalingam 对本文中提出的一些想法的影响。
脚注
1: 康威定律
关于康威定律有几种不同的观点,有些人认为它只是一个纯粹的描述性同义反复。我使用它更符合 维基百科 与 James Coplien 和 Neil Harrison 联系在一起的变体。它可能确实是组织内软件的一个描述性定律,但我认为这仅仅是因为试图违背康威定律的软件注定会失败。
重大修订
2013 年 11 月 18 日:添加了扩展 URL 空间以避免版本控制的选项,以及一个脚注以澄清康威定律的使用
2013 年 11 月 12 日:添加了关于使用史诗的章节,从而完成了首次出版
2013 年 11 月 8 日:添加了关于阻止系统垄断资源的章节
2013 年 10 月 31 日:添加了关于基于消费者的测试的章节
2013 年 10 月 24 日:添加了关于版本控制的章节
2013 年 10 月 21 日:发布第一版,包含逻辑环境部分