消费者驱动契约:一种服务演进模式
本文讨论了在服务提供者和消费者社区演进中遇到的一些挑战。它描述了服务提供者更改其契约部分(尤其是文档模式)时出现的一些耦合问题,并确定了两种广为人知的策略——添加模式扩展点和对接收到的消息进行“足够”验证——来缓解此类问题。这两种策略都有助于保护消费者免受提供者契约更改的影响,但它们都没有为提供者提供任何关于其使用方式以及在演进过程中必须维护的义务的洞察力。本文借鉴了其中一种缓解策略(“足够”验证策略)的基于断言的语言,描述了“消费者驱动契约”模式,该模式为提供者提供了对其消费者义务的洞察力,并将服务演进集中在交付消费者要求的关键业务功能上。
2006年6月12日
自最初发布以来,本文有一些更新。在 Thoughtworks Anthology 和 偶尔的博客 中有一个更新版本。
服务演进:一个示例
为了说明我们在演进服务时遇到的某些问题,请考虑一个简单的 ProductSearch 服务,该服务允许消费者应用程序搜索我们的产品目录。搜索结果具有以下结构
图 1:搜索结果模式
一个示例搜索结果文档如下所示
<?xml version="1.0" encoding="utf-8"?> <Products xmlns="urn:example.com:productsearch:products"> <Product> <CatalogueID>101</CatalogueID> <Name>Widget</Name> <Price>10.99</Price> <Manufacturer>Company A</Manufacturer> <InStock>Yes</InStock> </Product> <Product> <CatalogueID>300</CatalogueID> <Name>Fooble</Name> <Price>2.00</Price> <Manufacturer>Company B</Manufacturer> <InStock>No</InStock> </Product> </Products>
ProductSearch 服务目前由两个应用程序使用:一个内部营销应用程序和一个外部经销商的 Web 应用程序。这两个消费者都使用 XSD 验证来验证接收到的文档,然后再处理它们。内部应用程序使用CatalogueID、Name、Price 和Manufacturer 字段;外部应用程序使用CatalogueID、Name 和Price 字段。两者都不使用InStock 字段:虽然在营销应用程序中考虑过,但在开发生命周期的早期就被放弃了。
我们演进服务的最常见方法之一是代表一个或多个消费者向文档添加一个额外的字段。根据提供者和消费者的实现方式,即使是像这样的简单更改也会对企业及其合作伙伴产生高昂的成本。
在我们的示例中,ProductSearch 服务投入生产一段时间后,第二个经销商考虑使用它,但要求在每个产品中添加一个Description 字段。由于消费者的构建方式,这种更改对提供者和现有消费者都具有重大而昂贵的意义,每个消费者的成本根据我们如何实施更改而有所不同。我们至少有两种方法可以将更改成本分配到服务社区的成员之间。首先,我们可以修改原始模式,并要求每个消费者更新其模式副本,以便正确验证搜索结果;系统更改的成本在这里分配到提供者——面对这样的更改请求,提供者总是必须进行某种更改——以及消费者,他们对更新的功能不感兴趣。或者,我们可以选择代表新消费者向服务提供者添加第二个操作和模式,并代表现有消费者维护原始操作和模式。更改成本现在仅限于提供者,但代价是使服务更加复杂,维护成本更高。
插曲:服务负担
服务启用企业应用程序环境的主要优势包括提高组织敏捷性和降低实施更改的总体成本。SOA 通过将高价值业务功能置于离散的可重用服务中,然后连接和编排这些服务来满足核心业务流程,从而提高组织敏捷性。它通过减少服务之间的依赖关系来降低更改成本,从而允许它们快速重新组合和调整以应对更改或计划外事件。
但是,企业只有在 SOA 能够使服务彼此独立演进的情况下才能充分实现这些优势。为了提高服务独立性,我们构建共享契约而不是类型的服务。即使这样,我们也经常不得不以与服务提供者相同的速率演进消费者,主要是因为我们让消费者依赖于提供者契约的特定版本。最终,服务提供者发现自己对更改其向消费者提供的契约的任何元素都采取谨慎的态度;部分原因是他们无法预测或洞察消费者实现此契约的方式。最糟糕的是,服务消费者会实现提供者契约,并通过在其内部逻辑中天真地表达整个文档模式来将自己与提供者耦合。
契约使服务独立;矛盾的是,它们也可能以不可取的方式耦合服务提供者和消费者。如果我们不深入了解我们在 SOA 中实施的契约的功能和作用,我们就会使我们的服务受到一种我们很少有能力以任何系统的方式解决的“隐藏”耦合形式的影响。缺乏对服务社区如何采用契约的任何编程洞察力,以及对服务提供者和消费者所做实施选择缺乏约束,共同破坏了 SOA 使企业受益的预期优势。简而言之,企业将被服务所累。
模式版本控制
我们可以通过查看模式版本控制问题来开始对困扰我们 ProductSearch 服务的契约和耦合问题进行调查。 WC3 技术架构组 (TAG) 描述了许多版本控制策略,这些策略可能有助于我们以减轻耦合问题的方式演进服务的邮件模式。这些策略范围从过于宽松的none(它要求服务不能区分模式的不同版本,因此必须容忍所有更改)到极其保守的big bang(它要求服务在收到意外版本的邮件时中止)。
两种极端都带来了阻碍业务价值交付并加剧系统总拥有成本的问题。显式和隐式“无版本控制”策略导致系统在交互方面不可预测、脆弱且成本高昂。另一方面,大爆炸策略会导致服务环境紧密耦合,其中模式更改会波及提供者和消费者,从而中断正常运行时间,延缓演进并减少创收机会。
我们的示例服务社区有效地实施了大爆炸策略。鉴于与提高系统业务价值相关的成本,很明显,提供者和消费者将受益于更灵活的版本控制策略——TAG 发现称为兼容策略——它提供向后兼容和向前兼容的模式。在演进服务的背景下,向后兼容的模式使较新模式的消费者能够接受较旧模式的实例:构建为处理较新版本的向后兼容请求的服务提供者,仍然可以接受根据旧模式格式化的请求。另一方面,向前兼容的模式使较旧模式的消费者能够处理较新模式的实例。这是现有 ProductSearch 消费者的症结所在:如果搜索结果模式在首次投入生产时被设为向前兼容,那么消费者将能够处理新版本搜索结果的实例,而不会中断或需要修改。
扩展点
使模式既向后兼容又向前兼容是一项众所周知的设计任务,最好由可扩展性的Must Ignore 模式来表达(参见 David Orchard 和 Dare Obasanjo 的论文)。Must Ignore 模式建议模式包含扩展点,这些扩展点允许将扩展元素添加到类型中,并将附加属性添加到每个元素中。该模式还建议 XML 语言定义一个处理模型,该模型指定消费者如何处理扩展。最简单的模型要求消费者忽略他们不认识的元素——因此得名。该模型也可能要求消费者处理带有“必须理解”标志的元素,或者在无法理解它们时中止。
这是我们最初基于其创建搜索结果文档的模式
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns="urn:example.com:productsearch:products" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:example.com:productsearch:products" id="Products"> <xs:element name="Products" type="Products" /> <xs:complexType name="Products"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" /> </xs:sequence> </xs:complexType> <xs:complexType name="Product"> <xs:sequence> <xs:element name="CatalogueID" type="xs:int" /> <xs:element name="Name" type="xs:string" /> <xs:element name="Price" type="xs:double" /> <xs:element name="Manufacturer" type="xs:string" /> <xs:element name="InStock" type="xs:string" /> </xs:sequence> </xs:complexType> </xs:schema>
现在让我们倒流时间,从服务的生命周期开始,指定一个向前兼容的可扩展模式
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns="urn:example.com:productsearch:products" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:example.com:productsearch:products" id="Products"> <xs:element name="Products" type="Products" /> <xs:complexType name="Products"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" /> </xs:sequence> </xs:complexType> <xs:complexType name="Product"> <xs:sequence> <xs:element name="CatalogueID" type="xs:int" /> <xs:element name="Name" type="xs:string" /> <xs:element name="Price" type="xs:double" /> <xs:element name="Manufacturer" type="xs:string" /> <xs:element name="InStock" type="xs:string" /> <xs:element minOccurs="0" maxOccurs="1" name="Extension" type="Extension" /> </xs:sequence> </xs:complexType> <xs:complexType name="Extension"> <xs:sequence> <xs:any minOccurs="1" maxOccurs="unbounded" namespace="##targetNamespace" processContents="lax" /> </xs:sequence> </xs:complexType> </xs:schema>
此模式在每个产品的底部包含一个可选的 Extension 元素。扩展元素本身可以包含来自目标命名空间的一个或多个元素
图 2:可扩展搜索结果模式
现在,当我们收到一个更改请求,要求在每个产品中添加描述时,我们可以发布一个带有附加Description 元素的新模式,提供者将该元素插入扩展容器中。这允许 ProductSearch 服务返回包含产品描述的结果,以及使用新模式验证整个文档的消费者。使用旧模式的消费者不会中断,尽管他们不会处理描述。新的结果文档如下所示
<?xml version="1.0" encoding="utf-8"?> <Products xmlns="urn:example.com:productsearch:products"> <Product> <CatalogueID>101</CatalogueID> <Name>Widget</Name> <Price>10.99</Price> <Manufacturer>Company A</Manufacturer> <InStock>Yes</InStock> <Extension> <Description>Our top of the range widget</Description> </Extension> </Product> <Product> <CatalogueID>300</CatalogueID> <Name>Fooble</Name> <Price>2.00</Price> <Manufacturer>Company B</Manufacturer> <InStock>No</InStock> <Extension> <Description>Our bargain fooble</Description> </Extension> </Product> </Products>
修改后的模式如下所示
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns="urn:example.com:productsearch:products" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:example.com:productsearch:products" id="Products"> <xs:element name="Products" type="Products" /> <xs:complexType name="Products"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" /> </xs:sequence> </xs:complexType> <xs:complexType name="Product"> <xs:sequence> <xs:element name="CatalogueID" type="xs:int" /> <xs:element name="Name" type="xs:string" /> <xs:element name="Price" type="xs:double" /> <xs:element name="Manufacturer" type="xs:string" /> <xs:element name="InStock" type="xs:string" /> <xs:element minOccurs="0" maxOccurs="1" name="Extension" type="Extension" /> </xs:sequence> </xs:complexType> <xs:complexType name="Extension"> <xs:sequence> <xs:any minOccurs="1" maxOccurs="unbounded" namespace="##targetNamespace" processContents="lax" /> </xs:sequence> </xs:complexType> <xs:element name="Description" type="xs:string" /> </xs:schema>
请注意,第一个版本的可扩展模式与第二个版本向前兼容,第二个版本与第一个版本向后兼容。但是,这种灵活性是以增加复杂性为代价的。可扩展模式允许我们对 XML 语言进行不可预见的更改,但同样,它们也提供了可能永远不会出现的需求;这样做会掩盖简单设计带来的表现力,并通过在域语言中引入元信息容器元素来阻碍对业务信息的有效表示。
我们在这里不再讨论模式可扩展性。需要说明的是,扩展点允许我们对模式和文档进行向后兼容和向前兼容的更改,而不会破坏服务提供者和消费者。但是,模式扩展不能帮助我们在需要对契约进行表面上是重大更改时管理系统的演进。
重大变更
作为增值服务,我们的 ProductSearch 服务在搜索结果中包含一个字段,指示产品当前是否有货。该服务使用对遗留库存系统的昂贵调用来填充此字段——这种依赖关系维护成本很高。服务提供者希望消除这种依赖关系,清理设计,并提高系统的整体性能——最好不要将任何更改成本强加给消费者。在与消费者的所有者交谈时,提供者团队发现没有一个消费者应用程序实际上对该值执行任何操作;虽然昂贵,但它是多余的。
不幸的是,在我们现有的设置中,如果我们从可扩展模式中删除一个必需的组件——在本例中是InStock字段——我们将破坏现有的消费者。为了修复提供者,我们必须修复整个系统:当我们从提供者中删除功能并发布新契约时,每个消费者应用程序都必须使用新模式重新部署,并且必须彻底测试服务之间的交互。在这方面,ProductSearch 服务不能独立于其消费者而发展:提供者和消费者必须同时跳跃。
我们的服务社区在其发展中感到沮丧,因为每个消费者都实现了某种形式的“隐藏”耦合,这种耦合天真地反映了消费者内部逻辑中的整个提供者契约。消费者通过使用 XSD 验证,以及在较小程度上,从文档模式派生的静态语言绑定,隐式地接受了整个提供者契约,无论他们对处理组件部分的兴趣如何。
David Orchard 在谈到互联网协议的鲁棒性原则时,提供了一些关于我们如何避免这个问题的线索:“一般来说,实现必须在其发送行为中保持保守,在其接收行为中保持宽松”。我们可以在服务演化的背景下增强这一原则,即消息接收者应该实现“足够”的验证:也就是说,他们应该只处理有助于他们实现的业务功能的数据,并且应该只对他们接收的数据执行明确的边界或目标验证——而不是 XSD 处理中固有的隐式无边界、“全有或全无”的验证。
Schematron
我们可以通过在接收消息的文档树轴上断言模式表达式来实现消费者端验证的目标或边界,也许可以使用像Schematron这样的结构化树模式验证语言。使用 Schematron,ProductSearch 服务的每个消费者都可以以编程方式断言它期望在搜索结果中找到什么。
<?xml version="1.0" encoding="utf-8" ?> <schema xmlns="http://www.ascc.net/xml/schematron"> <title>ProductSearch</title> <ns uri="urn:example.com:productsearch:products" prefix="p"/> <pattern name="Validate search results"> <rule context="*//p:Product"> <assert test="p:CatalogueID">Must contain CatalogueID node</assert> <assert test="p:Name">Must contain Name node</assert> <assert test="p:Price">Must contain Price node</assert> </rule> </pattern> </schema>
Schematron 实现通常将这样的 Schematron 模式转换为消息接收者可以应用于文档以确定其有效性的 XSLT 转换。
请注意,此示例 Schematron 模式没有对底层文档中消费应用程序没有兴趣的元素进行断言。通过这种方式,验证语言明确地针对一组有界限的必需元素。对底层文档模式的更改不会被验证过程捕获,除非它们扰乱了 Schematron 模式中描述的明确期望,即使这些更改扩展到弃用或删除以前强制性的元素。
因此,这是一个针对我们的契约和耦合问题的相对轻量级的解决方案,并且不需要我们在文档中添加模糊的元信息元素。所以让我们再次回溯时间,恢复文章开头描述的简单模式。但这一次,我们还将坚持消费者在接收行为中保持宽松,并且只验证和处理支持他们实现的业务功能的信息(使用 Schematron 模式而不是 XSD 来验证接收的消息)。现在,当要求提供者为每个产品添加描述时,服务可以在不影响现有消费者的前提下发布修订后的模式。同样,在发现InStock字段没有被任何消费者验证或处理后,服务可以修改搜索结果模式——同样不会影响每个消费者的演化速度。
在这个过程结束时,ProductSearch 结果模式如下所示
<?xml version="1.0" encoding="utf-8"?> <xs:schema xmlns="urn:example.com:productsearch:products" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:example.com:productsearch:products" id="Products"> <xs:element name="Products" type="Products" /> <xs:complexType name="Products"> <xs:sequence> <xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" /> </xs:sequence> </xs:complexType> <xs:complexType name="Product"> <xs:sequence> <xs:element name="CatalogueID" type="xs:int" /> <xs:element name="Name" type="xs:string" /> <xs:element name="Price" type="xs:double" /> <xs:element name="Manufacturer" type="xs:string" /> <xs:element name="Description" type="xs:string" /> </xs:sequence> </xs:complexType> </xs:schema>
消费者驱动契约
在上述示例中使用 Schematron 导致了一些关于提供者和消费者之间契约的有趣观察,其影响超出了文档验证。在本节中,我们将这些见解提取出来并进行概括,并用我们称之为消费者驱动契约的模式来表达它们。
首先要注意的是,文档模式只是服务提供者必须提供给消费者以使他们能够利用其功能的一部分。我们将这些外部化的利用点的总和称为提供者契约。
提供者契约
提供者契约用支持该功能所需的导出元素集来表达服务提供者的业务功能能力。从服务演化的角度来看,契约是导出业务功能元素集的容器。这些元素的非规范列表包括
- 文档模式 我们已经详细讨论了文档模式。除了接口之外,文档模式是提供者契约中最有可能随着服务演化而改变的部分;但也许正是因为这一点,它们也是我们最有经验地赋予服务演化策略(如扩展点和文档树路径断言)的部分。
- 接口 在最简单的形式中,服务提供者接口包含消费者可以利用以驱动提供者行为的一组可导出操作签名。面向消息的系统通常导出相对简单的操作签名,并将业务智能推送到它们交换的消息中。在面向消息的系统中,接收的消息根据消息头或有效负载中编码的语义驱动端点行为。另一方面,类似 RPC 的服务在其操作签名中编码了更多业务语义。无论哪种方式,消费者都依赖于提供者接口的某些部分来实现业务价值,因此,在演化我们的服务环境时,我们必须考虑接口消费。
- 对话 服务提供者和消费者在构成一个或多个消息交换模式(如请求-响应和发布-订阅)的对话中交换消息。在对话过程中,消费者可能会期望提供者在它发送和接收的消息中外部化一些特定于交互的状态。例如,酒店预订服务可能会为消费者提供在对话开始时预订房间的能力,并在随后的消息交换中确认预订并支付押金。在这种情况下,消费者可能会合理地期望服务在进行这些后续交换时“记住”预订的详细信息,而不是要求各方在流程中的每个步骤都重复整个对话。随着服务的演化,提供者和消费者可用的对话策略集可能会发生变化。因此,对话是作为提供者契约的一部分进行考虑的候选者。
- 策略 除了导出文档模式、接口和对话之外,服务提供者还可以声明和执行特定使用要求,这些要求管理如何实现契约的其他元素。最常见的是,这些要求与消费者可以利用提供者功能的安全和事务上下文相关。Web 服务堆栈通常使用WS-Policy通用模型加上其他特定于域的策略语言(如WS-SecurityPolicy)来表达此策略框架,但在将策略视为包含在提供者契约中的候选者的背景下,我们对策略的定义与规范和实现无关。
- 服务质量特征 服务提供者和消费者利用的业务价值潜力通常是在特定服务质量特征(如可用性、延迟和吞吐量)的背景下进行评估的。我们应该将这些特征视为提供者契约的可能组成部分,并在我们的服务演化策略中考虑它们。
这里对契约的定义比我们在谈论服务时通常提供的定义要宽泛一些,但从服务演化的角度来看,它有效地抽象了影响我们问题域的重要力量。也就是说,该定义并非旨在穷举提供者契约可能包含的元素类型:它只是指一组逻辑的导出业务功能元素,这些元素是包含在服务演化策略中的候选者。从逻辑的角度来看,这组候选元素是开放的,但在实践中,内部或外部因素(如互操作性要求或平台限制)可能会限制契约可以包含的元素类型。例如,属于符合 WS-Basic 配置文件的服务的契约可能不会包含策略元素。
尽管存在任何此类约束,但契约的范围仅由其成员元素的凝聚力决定。契约可以包含许多元素,范围很广,也可以只关注少数几个元素,只要它表达了某种业务功能能力即可。
我们如何决定是否将候选契约元素包含在我们的提供者契约中?我们通过问自己,我们的任何消费者是否可能会合理地表达一个或多个期望,即元素封装的业务功能能力在服务的整个生命周期中都能够得到满足。我们已经看到,我们示例服务的消费者如何表达对服务导出的文档模式部分的兴趣,以及他们如何断言他们对该契约元素的期望能够得到满足。因此,我们的文档模式是我们提供者契约的一部分。
提供者契约具有以下特征
- 封闭且完整 提供者契约用可供消费者使用的完整导出元素集来表达服务的业务功能能力,因此,它们对于系统可用的功能是封闭且完整的。
- 单一且权威 提供者契约在表达系统可用的业务功能方面是单一且权威的。
- 有限的稳定性和不变性 提供者契约在有限的时期和/或位置内是稳定且不变的(参见Pat Helland的论文外部数据与内部数据中的“有限空间和时间内数据的有效性”部分)。提供者契约通常使用某种形式的版本控制来区分不同有界限的契约实例。
消费者契约
如果我们决定在演化我们的服务时考虑消费者对我们公开的模式的期望——并认为让我们的提供者了解这些期望是值得的——那么我们需要将这些消费者期望导入提供者。我们示例中的 Schematron 断言非常类似于,如果由提供者实现,可能有助于确保提供者继续履行其对客户的承诺的测试。通过实现这些测试,提供者可以更好地了解如何在不破坏服务社区中现有功能的情况下演化它生成的消息的结构。如果提议的更改确实会破坏一个或多个消费者,提供者将立即了解该问题,并且能够更好地与相关方解决该问题,满足他们的要求或为他们提供根据业务因素进行更改的激励。
在我们的示例中,我们可以说,所有消费者生成的断言集表达了在断言对其父应用程序保持有效期间交换的消息的强制结构。如果提供者拥有这组断言,它将能够确保它发送的每条消息对于每个消费者都是有效的,只要断言集是有效且完整的。
概括这种结构,我们可以将我们已经称为提供者契约的内容与在提供者-消费者关系实例中获得的单个契约义务区分开来,我们现在将其称为消费者契约。当提供者接受并采纳消费者表达的合理期望时,它就进入了消费者契约。
图 3:消费者契约
消费者契约具有以下特征
- 开放且不完整 消费者契约对于系统可用的业务功能是开放且不完整的。它们用消费者对提供者契约的期望来表达系统业务功能能力的一个子集。
- 多重且非权威的 消费者契约的数量与服务的消费者数量成正比,并且每个契约对于提供者所承担的全部合同义务而言都是非权威的。消费者与提供者之间关系的非权威性是区分面向服务的架构和分布式应用程序架构的关键特征之一。服务消费者必须认识到,服务社区中的同行可能会以与他们自身完全不同的方式使用提供者。同行可能会以不同的速度发展,并要求提供者进行更改,这些更改可能会扰乱系统其他部分中存在的依赖关系和预期。消费者无法预测同行何时或如何扰乱提供者契约;分布式应用程序中的客户端没有这样的担忧。
- 有限的稳定性和不变性 与提供者契约一样,消费者契约在特定时间段和/或位置有效。
消费者驱动契约
消费者契约使我们能够反思提供者生命周期中任何时间点所利用的商业价值。通过表达和断言对提供者契约的期望,消费者契约有效地定义了提供者契约的哪些部分目前支持系统实现的商业价值,以及哪些部分不支持。这使我们建议服务社区可能首先从消费者契约的角度进行规范。在这种观点中,提供者契约应运而生,以满足消费者的期望和需求。为了反映这种新的合同安排的派生性质,我们将此类提供者契约称为消费者驱动契约或派生契约。
消费者驱动的提供者契约的派生性质为服务提供者和消费者之间的关系增添了异质性。也就是说,提供者受制于来自其边界之外的义务。这绝不影响其实现的根本自主性;它只是明确地表明服务依赖于被消费才能取得成功。
消费者驱动契约具有以下特点
- 封闭且完整 消费者驱动契约对于其现有消费者要求的全部功能而言是封闭且完整的。该契约代表了在这些期望对父应用程序保持有效的时间段内,支持消费者期望所需的强制性可导出元素集。
- 单一且非权威的 提供者契约在其对系统可用业务功能的表达中是单一的,但由于源自现有消费者期望的联合,因此是非权威的。
- 有限的稳定性和不变性 消费者驱动契约对于特定的一组消费者契约而言是稳定且不变的。也就是说,我们可以根据指定的一组消费者契约确定消费者驱动契约的有效性,有效地将契约在时间和空间上的向前和向后兼容性限制在一定范围内。契约的兼容性对于特定的一组消费者契约和期望保持稳定和不变,但会随着期望的出现和消失而发生变化。
契约特征摘要
下表总结了本文中描述的三种契约类型的特征
契约 | 开放 | 完整 | 数量 | 权威 | 有限 |
---|---|---|---|---|---|
提供者 | 封闭 | 完整 | 单一 | 权威 | 空间/时间 |
消费者 | 开放 | 不完整 | 多重 | 非权威 | 空间/时间 |
消费者驱动 | 封闭 | 完整 | 单一 | 非权威 | 消费者 |
实现
消费者驱动契约模式建议使用消费者契约和消费者驱动契约构建服务社区。但是,该模式并没有指定消费者契约和消费者驱动契约应该采用的形式或结构,也没有确定如何将消费者的期望传达给提供者并在提供者的生命周期中得到断言。
契约可以以多种方式表达和构建。在最简单的形式中,消费者的期望可以记录在电子表格或类似文档中,并在提供者应用程序的设计、开发和测试阶段实施。通过更进一步并引入断言每个期望的单元测试,我们可以确保契约在每次构建中以可重复的自动化方式进行描述和强制执行。在更复杂的实现中,期望可以表达为在服务端点的输入和输出管道中运行时评估的 Schematron 或 WS-Policy 样式的断言。
与契约的结构一样,在提供者和消费者之间传达期望时,我们也有多种选择。由于消费者驱动契约模式与实现无关,因此,在适当的组织设置下,我们可以简单地与其他团队交谈或使用电子邮件来传递期望。当期望和/或消费者的数量变得太大而无法以这种方式管理时,我们可以考虑在连接的系统基础设施中引入契约服务接口和实现。无论机制如何,沟通都可能在系统业务功能的任何对话之前进行,并且在系统业务功能的任何对话之前进行。
优势
在服务演进方面,消费者驱动契约提供了两个重要的好处。首先,它们将服务功能的规范和交付集中在关键的商业价值驱动因素周围。服务对业务的价值仅在于其被消费的程度。消费者驱动契约通过断言可导出服务社区元素的价值(即消费者需要提供者完成其工作所需的事物)将服务演进与商业价值联系起来。因此,提供者公开了一个精简的契约,该契约与支持其消费者的业务目标明确一致。更改(服务演进)仅在消费者表达明确需求时才会出现。
当然,我们能够从最小的精简需求集开始,并在消费者需要时演进我们的服务,这预示着我们能够以受控且高效的方式演进、部署和操作服务。这就是消费者驱动契约模式提供的第二个关键好处。消费者驱动的提供者契约为我们提供了进行更改计划和评估其对当前生产中的应用程序的影响所需的细粒度洞察力和快速反馈。在实践中,这使我们能够针对单个消费者,并为他们提供放弃阻止我们进行当前不向后和/或向前兼容的更改的期望的激励。通过从消费者契约中派生我们的服务提供者,我们为他们赋予了知识库和反馈机制,我们可以在系统生命周期的操作部分中利用这些知识库和反馈机制。
责任
在本文中,我们确定了在服务环境中引入消费者驱动契约的动机,然后描述了消费者驱动契约模式如何解决决定服务演进的因素。最后,我们将讨论该模式的适用范围,以及在实施消费者契约和消费者驱动契约时可能出现的一些问题。
消费者驱动契约模式适用于单个企业或封闭的知名服务社区的背景:更具体地说,提供者可以对消费者如何与他们建立契约施加一定影响的环境。无论用于传达和表示期望和义务的机制多么轻量级,提供者和消费者都必须了解、接受和采用一组商定的渠道和约定。这不可避免地为已经复杂的服务基础设施增加了一层复杂性和协议依赖性。
我们建议围绕消费者驱动契约构建的系统能够更好地管理对契约的重大更改。但我们并不意味着该模式是解决重大更改问题的灵丹妙药:归根结底,重大更改仍然是重大更改。但是,我们确实相信,该模式提供了许多关于什么真正构成重大更改的见解,因此可以作为服务版本控制策略的基础。此外,正如我们已经讨论的那样,实施该模式的服务社区能够更好地预测服务演进的影响。特别是提供者开发和运营团队可以更有效地规划其演进策略 - 例如,通过在特定时间段内弃用契约元素,并同时针对顽固的消费者提供激励措施,以升级到新版本的契约。
消费者驱动契约并不一定减少服务之间的耦合。松散耦合的服务彼此相对独立,但仍然存在耦合。但是,该模式所做的是挖掘并展示一些剩余的“隐藏”耦合,以便提供者和消费者能够更好地协商和管理它们。
我们已经讨论了消费者契约和消费者驱动契约如何表达商业价值的方式。但我们应该明确表示,我们不认为此类契约是商业价值的索引或衡量标准 - 它们不是商业指标 - 尽管与WS-Agreement和WSLA等规范有一些表面上的相似之处,但它们并非旨在表达服务级别协议。这里的基本假设是,服务本身对业务没有价值;它们的价值在于被消费。通过在更接近使用服务的地方(即消费者)规范服务,我们的目标是以精益的、即时的方式利用商业价值。
最后,我们应该指出,允许消费者契约驱动服务提供者的规范可能会破坏该提供者的概念完整性。服务封装了离散的、可识别的、可重用的业务功能,其完整性不应因超出其授权范围的不合理需求而受到损害。
进一步聆听
您可以在 2006 年 3 月的微软架构洞察大会上收听微软的 Ron Jacobs 采访我和 Martin Fowler。我们在改变架构的背景下讨论了消费者驱动契约。
致谢
Ian Cartwright, Duncan Cragg, Martin Fowler, Robin Shorrock, Joe Walnes
重大修订
2006 年 6 月 12 日:首次发布