向 SOA 过渡给软件开发生命周期带来了许多新的挑战:机构只有形成一种明确面向服务的开发能力,才能战胜这些挑战。
本文为提高机构的服务开发能力提供了一些实践性建议。本文将先概述 SOA 给软件开发生命周期带来的一些挑战,然后讲述以“服务故事(stories for services)”和服务开发线(service development streams)间交换的单元测试为形式的消费者驱动的契约(consumer-driven contracts)何以能够增强面向服务开发的生命周期。
SOA 给开发带来的挑战
面向服务(service orientation)不仅仅是采纳一种新的架构这么简单。若机构仅使其架构变得更加面向服务、而不对其开发技术作相应改变的话,那么 SOA 行动肯定要失败。
在启动、构建及运营服务方面的一些挑战包括:
- 在启动阶段,服务功能的描述必须能在多种场合下被重用:粒度既不能粗到仅在一种特定场合下能被重用,也不能细到要做大量补充工作方可在不同场合下被重用。
- 在构建服务时,我们必须确保提供者与消费者可彼此独立地进行演化。如果一项服务功能的消费者们必须随提供者改变而改变,那么该服务就不是真正松耦合的。
- 在运营服务时,我们需要理解各个服务之间的关系,以便我们可以诊断问题、评估服务可用性(availability)发生变化(常常是去除)时将产生的影响、并为各服务针对新的或变化的业务需求而演化设计计划。
从整体资产的角度来看,各服务的具体开发生命周期必须彼此协调一致,且没有不恰当地将各方拴在一个一体的、极其缓慢的、最终无法实行的活动时间表之上。
SOA 给软件开发生命周期带来的挑战源于服务资产的联邦特性。有价值的业务成果不再是由那些“具有单向时间活动表,以及所有权、预算和运营边界都分散”彼此相隔的应用实现了;相反,它们是通过那些“拥有各自独特的服务开发生命周期的”服务之间的交互实现的。然而,协调好这些生命周期并不是一次搞定就完事了的。相反,正如我们将看到的,我们需要识别出一组代表着“对交付一个共同结果的承诺”的协作活动与制品(collaborative activities and artefacts)。这些活动与制品为时刻(若简单地看)将各条开发线联合起来提供了基础。
用敏捷方法实现 SOA,意味着及早交付高价值业务成果且常常需要频繁地发布版本。若机构试图采用这种方法,那只会进一步加剧开发挑战。尽管其名称如此,但许多人认为敏捷的软件交付方式对与 SOA 相关的机构敏捷效益是不利的。由于在一个服务间存在着很多已知关联的环境中发布新版本的服务会有运营风险,所以敏捷方法频繁发布版本这一做法常被免去。而且,虽然敏捷鼓励各方进行密切及时地协作,但这种活动难以跨越多个不同服务及其生命周期进行协调。
联邦期望
传统的烟囱式应用开发把彼此的交付线(delivery streams)排除在了业务所有权、预算及运营边界之外。面向服务的开发与传统的烟囱式应用开发的不同之处在于,它强调外部的依赖和约束,并把它们整合到了开发生命周期的每一步中去。这属于 SOA 治理领域:管理交付关联性,并围绕着关于“服务资产及该资产所支持的业务活动、流程及结果”的一个公共表示来安排交付线。SOA 治理有赖于洞察与反馈——即对服务目录当前状态的洞察,以及关于演化中的服务提供者与消费者所产生影响的反馈。我们可以将联邦的期望和约束形成制品,这样便可增强洞察与反馈的能力。然后,这些制品就可以在一个特定服务开发线(service development stream)里的各阶段之间转移,以及 / 或者在不同开发线之间交换了。
要掌握并交换期望与约束,一个实际的做法就是使用所谓的消费者驱动的契约(consumer-driven contracts)。消费者驱动的契约是存在紧密联系的服务契约三件套——提供者契约、消费者契约及消费者驱动的契约——中的一员,它从期望与约束的角度描述了服务提供者与服务消费者之间的关系:
- 提供者契约(Provider contracts)——提供者契约是我们最为熟悉的一种的服务契约,参考 WSDL+XML Schema+WS-Policy。顾名思义,提供者契约是以提供者为中心的。提供者规定了它要提供什么;然后,各消费者便将自己绑定到这个一成不变的契约上。不论消费者实际需要多少功能,消费者接受了提供者契约,就将自己与该提供者的全体功能耦合起来了。
- 消费者契约(Consumer contracts)——另一方面,消费者契约是对一个消费者的需求更为精确的描述。消费者契约描述了,在一次具体交互场合下,提供者功能中消费者需要的特定部分。消费者契约可被用来标注一个现有的提供者契约,另外消费者契约也有助于发现一个现今尚未规定的提供者契约。
- 消费者驱动的契约(Consumer-driven contracts)——消费者驱动的契约描述的是服务提供者向其所有当前消费者承诺遵守的约束。一旦各消费者把自己的具体期望告知提供者,消费者驱动的契约就被创建了。在提供者方面创建的约束,确定了一个消费者驱动的契约。若提供者接受了一个消费者驱动的契约,那么它只需保证已有约束仍能得到满足,即可自行改进与修改其服务。
## 面向服务开发的生命周期
如何将消费者驱动的契约对应到面向服务开发的生命周期上去呢?它们如何帮助解决由联邦的服务资产造成的难题?
启动
在服务开发的第一阶段,消费者驱动的契约很像有关服务的用户故事(user stories for services)。服务只有被消费了才有用;所以,要规定服务的效用,最好的方式就是描述消费者对它们有何期望。通过从角色、功能及利益等方面(参见 Dan North 的《What‘s in a story》及Mike Cohn 的《User Stories Applied》)描述高优先级业务结果,用户故事(user stories)有助于我们制订服务关键功能的交付计划。以用户故事(user stories)实现的消费者驱动的契约,描述了一个服务消费者的角色、该消费者所寻求实现的结果或利益、以及它为了实现该结果而对提供者在功能或行为方面的要求。
这种详细说明具有业务含义的服务行为的方法明显是由外向内的,它跨越业务、机构及技术边界,捕获来自其他开发线及开发生命周期的依赖、期待和约束。实际上,它跟行为驱动的开发(Behaviour-Driven Development,BDD)很相像,行为驱动的开发也是注重以由外向内的视角来看待业务目标、与那些目标相应的利益、以及为满足那些目标不同参与者需展现出的活动与行为。通过识别出有助于实现整个业务结果的行为与交互,消费者驱动的契约(就像BDD 范本一样)指出了一个服务对业务有重大贡献“正好”所需的功能。
关于这些外部化的交互与行为,关键之处在于它们表示的是对业务有意义的东西,它们若不发生,业务活动的某部分就无法继续或完成。在各方之间发生的业务事件、文档交换和对话过程中,服务符合之处就是系统内在价值显露的地方。消费者驱动的契约反映了一个业务团体、功能或能力为完成其工作而对另一个伙伴的期望。总之,这些契约提供了一个高层的业务活动视图,它对业务结果有极大的帮助。
消费者驱动的契约解决了一个SOA 常被问到的难题,即:采用过去的烟囱式方法也能解决当前的业务需求(而且说不定还更简便),那为何还要用服务来支撑一个活动或流程呢?在一个经过良好构造的服务资产中,真正重要且有用的结果是通过若干对等服务之间的交互实现的,将一个服务与一组分散的业务目标与利益对应起来并非易事。尽管烟囱式应用常常可直接跟一个具体的利益或结果对应起来,但许多服务具有“流程不可知性”,它们不知道自己在为业务提供什么价值。为了认识到服务所提供的具体利益与结果,我们需要在其协作上下文之中来理解该服务。这就是消费者驱动的契约发挥作用之处:消费者驱动的契约描述了对服务群落的协作期望,它通过其更为直观的成对关系、有效地把全体参与者间接感知的价值串连了起来。
构建
在构建阶段,消费者驱动的故事可被转换为可执行的期望。因为它们描述了可接受的结果,所以可以把故事(stories)转换为接受标准,并最终成为程序断言或测试。要把消费者驱动的契约整合到开发流程中来,破坏性最小的做法是:让提供者团队把消费者驱动的故事转换为程序契约。这无疑提供了一种在服务开发生命周期各阶段间转移制品的方式,不过这不太符合服务资产的联邦特性。更好的做法是:让负责交付消费者应用与服务的团队来编写他们自己的消费者测试,并把这些测试交给服务提供者。各个消费者把自己的一个基于测试的消费者契约交给服务提供者——提供者从各消费者处收到的契约集合便构成了它的消费者驱动的契约。接着,消费者可以参照它们自己的契约进行开发,并相信提供者也将参照同样的期望进行开发。这样做,便可以把契约整合到双方的开发线之中。
为保存服务资产的适当联邦特性,我们要认识到,消费者的期望和契约正是消费者的财产。若我们剥夺消费者对期望的所有权,我们就降低了SOA 在组织和架构方面的协同力。通过把消费者契约的所有权分配给消费者开发线,我们确保了提供者方消费者驱动的契约是由消费者制品直接得到的,而不是提供者自己对消费者期望的理解。以这种方式交换测试,不但把开发与构建过程连结了起来,还降低了出现提供者消费者间理解偏差的风险。
有时,不同消费者要求的契约会彼此冲突。将消费者契约明确化有助于我们及早发现这种冲突。假如冲突相对较小,我们可以跟相关团队商量看是否可以调解——比方说请某个消费者放松一些规则。假如冲突比较严重,可能我们得重新检查我们的设计,以期提取出某些操作与消息,以面向更窄范围的问题。这在一定程度上是个有关判断力的问题——消费者驱动的契约可以指导我们生产出可重用、流程不可知的最小行为集合,但我们仍需确保结果对业务有意义且实现了关注点分离。
最简单的消费者契约,就是对服务提供者与消费者之间交换的消息作断言。对于基于XML 的消息来说,消费者可以用诸如XPath 或 Schematron 这样的语言来表达他们的期望,在异质技术环境下这两种语言都比较好用。消费者也可以使用 XML Schema 和 RELAX NG ,不过由于这些语言的验证结果常常是“要么对要么错”,因此用 XML Schema 或 RELAX NG 编写的消费者契约必须提供扩展点,以说明目前消费者不感兴趣的提供者契约部分——而这某种程度上是有违初衷的。
一个服务暴露什么样的提供者契约,直接受到消费者契约的影响。如果一个服务用 XML Schema 描述它发送与接收的消息结构,那么它发布的模式(schema)必须符合其消费者的期望。对一个新的服务来说,这可能涉及到提供者与消费者双方的开发团队联合设计消费者断言以及符合那些断言的提供者模式(schema)。对已有的服务来说,消费者可以选择用自定义的断言对模式的一个副本进行标注。对提供者而言,消费者驱动的契约提供了一个明确的开发方向。跟其他单元测试一样,消费者测试可用于驱动开发活动:开发者创建服务行为,以满足测试的期望。也可以把测试添加到一个持续集成环境中,然后拿每个版本对照服务功能进行断言。若消费者以原始 XPath 或 Schematron 表达式的形式来提供契约,那么提供者就可以用单元测试框架(如 JUnit、NUnit 或 XMLUnit)对那些它生成的消息执行底层测试。
提供者甚至可以把消费者驱动的契约整合到它自己的运行时活动中,以便当真实世界的消息未能满足消费者期望时及早提供反馈。如果必须完全遵守所有消费者契约,那么提供者可能需要对发送管道里的消费者期望进行断言、以验证它发送的消息。根据要验证的契约大小与数目,这可能会对响应速度和吞吐量有影响。或者,提供者可以实现侦听或对已发送的消息作某种另外的订阅,并对发送管道以外的约束进行断言。
在消费者方面,消费者契约鼓励特别的开发实践。若提供者必须恰好符合消费者驱动的契约,那么消费者必须确保自己也符合对方的期望。消费者不是把提供者契约整体导入,而是只用它告诉提供者它需要的那部分。这样一来,消费者可以保护自己免受它不关注部分的变化的影响。只用必要的部分意味着不在消费者方面进行模式验证——相反,假定提供者会遵守被告知的期望,并会根据那些期望发送有效的消息。根据消费者方面反序列化(deserialisation)机制能够承受多余、丢失或乱序字段的程度,消费者也可以希望避免将收到的消息反序列化为强类型的表示(representations),而是选择用XPath 提取出消息中所感兴趣的部分。这种策略常被称为 WS-DuckTyping 。
基于测试的消费者契约的另一个用处是对厂商套件及 COTS 应用进行评估。采取消费者驱动的方式,我们从“它们将参与的协作”及“系统里其他部分对它们的期望”的角度来描述我们对这些套件的需求。通过将消费者期望编制成表,我们从外部协作点的角度描述了一个套件实现应该是什么样子;通过用消费者驱动的契约(也许是以一套自动化测试的形式)来表达这些期望,我们可以判断该应用是否有希望成为的一个优秀资产。
运营
在服务开发团队之间交换的测试,使得服务资产里的较重要的外部协作点凸现了出来。这些测试及它们所代表的相互关联,为那些负责运营服务的人提供了有用的可执行档案。消费者驱动的契约指出了谁依赖于一个服务、以及该依赖的特性是什么,这增强了运营对资产日常行为的洞察。
除了令服务资产之中的关系更加透明,一套以共享测试实现的消费者驱动的契约还增强了关于服务演化将产生影响的反馈。消费者驱动的契约提供了一个已断言行为的基础,我们可以根据它对一个服务契约的变动产生的可能的影响进行评估。消费者测试构成了一个跨服务开发线的回归测试集的基础,只要提供者能够继续满足测试集的现有约束,提供者就可以演化。
消费者驱动的契约可用于规划服务资产的变更,它们对交付机构的版本化策略有帮助。虽然向后 / 向前兼容性原则依然对版本化服务极为重要,但消费者驱动的契约有助于根据现有的约束与关系来在大环境中考虑兼容性问题。不必总把必备元素的变化看成是破坏性变化;相反,可以根据它们所违反的消费者契约来找出需要版本化的变化。若现有消费者未用到某必备消息元素,那么修改或去除该元素就不用被看成是一种破坏性变化。
总结
实施 SOA 给软件开发生命周期带来了新的挑战,这些挑战源自服务资产的联邦特性。一个成功的服务开发能力有赖于对多开发线的洞察和收集关于多开发线的反馈。以“服务故事(stories for services)”和开发线间交换的测试来使用消费者驱动的契约实现了开发生命周期中各个活动的有机联系和开发线间各个活动的协调,从而让我们部分解决了目标。消费者驱动的契约支持面向服务的系统的开发与测试,而且支持负责服务生命周期的各方之间的协作。
更多关于消费者驱动的契约的论述,请参看《The ThoughtWorks Anthology》及作者的博客。
关于作者
Ian Robinson 是 ThoughtWorks 公司的首席顾问,他专门研究面向服务及分布式系统的设计与实施交付。他为微软公司编写了用微软技术实现集成模式的指南,并发表了一些关于面向业务的开发方法及分布式系统设计的文章——最近的文章收录在《The ThoughtWorks Anthology》(Pragmatic Programmers,2008)里。他目前正在与人合写一本关于面向Web(Web-friendly)的企业集成方面的书籍。
查看英文原文: Service-Oriented Development with Consumer - Driven Contracts ****
志愿参与 InfoQ 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。
评论