领域驱动设计DDD入门4:上下文映射
欢迎来到第四课:战略工具之上下文映射。 本课重点介绍上下文映射以及各种团队关系和集成技术,这些技术可用于管理多个有界上下文如何和谐地工作以形成整个系统解决方案。 首先,我将介绍有限上下文之间的上下文映射。这导致对各种上下文映射、团队关系和集成技术的检查,包括伙伴关系、共享内核、客户供应商、遵从者、反腐层、开放主机服务、发布语言、大泥球等。然后我们看一下如何充分利用上下文映射,我们会将上下文映射与SOAP和RPC一起使用,RESTful HTTP和消息传递。最后,我给出一个使用消息传递和REST的使用上下文映射的示例。
思考一个问题:我们的核心域和其他子域是如何集成的,它们关系如何?比如我们的敏捷项目管理软件和协作上下文集成在一起,这样敏捷软件就有了论坛和日历功能。那么这两个子域之间是啥样的呢?
伙伴关系
当两个团队在两个不同的边界上下文中工作,而他们有共同的目标并且互相支持时构成伙伴关系。 有伙伴关系的两个组织有非常重的关系,因为他们必须相互协调他们的上线,必须同时发布,因为他们相互支持。两者之间没有上游下游关系,他们的模型在相当长的一段时间内保持同步。因此需要大量的协调、大量的规划。这两个团队必须认真合作,以确保一切顺利进行,相互支持。
共享内核
共享内核是两个或多个团队拥有非常相似的软件模型。因此,他们将开发出单个要使用的共享模型部分。这是一个相对不正常的情况,因为要确保两个团队都能从这个小核心模型中受益,需要进行大量规划以及大量的协调和集成测试。共享内核不太可能会经常使用,因为这要求团队共享大量信息,并就模型如何协同工作达成共识。不太可能在两个不同的团队之间形成那种沟通和协调的水平,因为他们往往不会进行协作或有效地进行沟通。
客户供应商关系
客户供应商关系是两个团队之间存在确定的上游下游关系。比如团队1位于上游,团队2位于下游。那上游团队1的模型更改将对下游团队2产生影响。所以更改的时候必须通过协商来协。团队1和团队2需要一起进行协商,团队2会要求团队1正在开发中的某些功能必须满足这种协调,以便团队2可以获取他们需要的功能。如果没有达成,那么团队2将无法在他们需要的时间范围内获得所需的东西,因此对团队2可能产生非常负面的影响。
遵从关系
遵从关系是指存在明确的上游下游关系的情况,其中团队1具有直接影响下游团队2模型的上游模型。团队2基本上只是奴隶般的消耗团队1的模型并遵守该模型。可能因为是一个复杂的模型或一个大型模型,团队2没有时间或资源进行修改。因此,他们像使用自己的模型一样简单地消费它。
防腐层
在防腐层中,有两个具有单独模型的团队,下游团队不想受到上游模型的不必要影响就使用防腐层。可以看到这是遵从关系模式的反面。 上游团队拥有自己的模型,下游团队将使用该模型,但会将上游模型的数据和其他结构转换为下游域模型或有界上下文中的自身结构和数据,所以不会受上游团队的影响。
开放主机服务
开放主机服务是一个文档齐全、定义明确、使用方便且美观的用于消费的模型,是上游团队提供的良好API。可能是restful的,也可能是消息api,也可能是RPC api。无论哪种都有完整的文档,可以方便的集成。下游团队直接使用开放主机服务,可以直接使用其模型或者可以方便的转换。
发布语言
发布语言是上游团队使用的一种用来生成定义明确、文档齐全的数据交换格式的语言,生成的数据格式可以使小组2非常舒适、方便地使用。开放主机服务向下游团队提供发布语言是很常见的。因此,使用不同模型的数据和结构也非常方便,并且团队2可以轻松地将其转换为标准格式。
单独的方式
单独的方式描述了可能发生在团队1和团队2之间集成的类型。出于某种原因,———— 也许是费用,也许是缺乏便利,也许是因为使用团队1的模型的收益较低 ———— 所以团队2决定采用单独的方式,并在自己的模型内创建一个一次性解决方案,以解决他们可能需要从团队1那里使用的解决方案。
大泥球
我们已经讨论过大泥球。正确使用DDD战略工具来不太会开发出一个大泥球。但是,有时候可能必须与一个大泥球整合。现在,关于大泥球的一个新警告是:当你对模型的一部分进行更改时,很可能会对模型的另一部分产生负面影响。发生变化导致最终出现棘手问题的地方是,一个变化会对另一区域的数据或模型产生影响;而你试图遏制这种情况,但当您进行更改时,这种变化就会传播到另一个区域。就DDD而言,大泥球是一个非常不利的情况。如果必须与一个大的泥球整合,请尝试在上下文的下游边界(无疑是干净的核心域)和上游的大泥球之间使用一个反腐层。这样一来,就可以按自己的意愿消耗大量的泥球,并使其数据和结构与下游模型中的数据和结构保持一致。
集成样式
你会如何使用上下文映射在有界上下文之间进行集成?现在我们将讨论三种不同的集成样式以及如何将它们与上下文映射一起使用:RPC、RESTful集成、消息传递。
RPC
首先,让我们讨论使用SOAP的RPC。这里我们有服务有界上下文和客户端有界上下文。客户端必须在服务注册表中查找服务,并且该服务将在服务有界上下文中被使用并由客户端消费。 缺点是RPC可能会由于网络状况而失败:例如在网络分区或其他情况下、网络连接中的某种中断,它们都是RPC可能发生的所有事情。因此,缺点是依赖RPC与服务边界上下文进行集成的客户端可能无法实时完成其集成而导致失败。但是,当RPC工作或RPC大部分工作时,这可能是实现集成的非常理想且方便的方法。服务边界上下文可能会受益于将其设计为具有已发布语言的开放主机服务,并且下游需要使用边界上下文,因此可以使用空的防腐层来仅按自己的条件转换和使用上游模型,从而使客户端具有自己的独特模型,独特的数据结构,并通过从上游开放主机服务和已发布的语言转换为自己独特的模型组成来实现。
RESTful
RESTful HTTP非常类似于RPC,客户端必须通过网络连接与服进行通信。常用的动词是post、get、put和delete,这些由服务上下文提供。同样,当将服务上下文与客户端上下文集成时,会使用某种文档格式交换数据。这些是在两个有界上下文之间交换的RESTful数据格式。 通常,你希望在服务上下文中使用已发布的语言再次创建开放式主机服务。实际上,发布的语言是在两种上下文之间交换的表示形式。如果客户端上下文具有反腐层,该层可以将服务上下文提供的表示转换为自己的本地模型,则它也可能会受益。请注意,通过这种集成,您有时还可能会遇到网络问题,与使用RPC时相同。这就是我们看到的客户端上下文和服务上下文之间存在脱节的地方。在为服务上下文设计资源时,应根据客户想要使用的用例来设计资源。如果有多个客户,则希望他们每个人都有机会按照他们的条件消耗资源。这将防止每个客户端不得不与服务建立遵从关系。如果根据你自己的内部域模型来设计资源,将迫使客户端建立遵从关系。不要那样做!
消息
使用RPC和RESTful集成时,网络可能会导致这种集成问题。现在,我们探讨一种更稳健的集成形式,即使用消息传递。使用DDD进行消息传递通常使用域事件进行集成。聚合和域事件由聚合发布并由其他有界上下文使用。使用消息传递机制有很多不同的选择。主要模式是聚合将发布一个域事件,并且该聚合在发布域事件时使该事件对另一个有界上下文可用,而另一个有界上下文(订阅有界上下文)将获取域事件,并且此域事件可能会影响上下文中的另一个聚合。 有时必须向服务上下文提供一条命令,以使其执行某些操作来导致域事件发生,该域事件可以对使用方(客户端)产生影响。当你将域事件与消息传递机制一起使用时,必须支持“至少一次投递”,并且接收者或订阅上下文必须是幂等的。假设消息传递机制传递的消息是打开某种资源的消息。如果该消息传递了两次、三次或更多,则接收者必须能够对多次接收做出负责任的反应,并使资源实际上只打开一次,而不是多次打开。
让我们来看一个使用消息集成的示例。我们使用保险行业示例,有承保上下文、索赔上下文和检查上下文。我们将研究如何通过消息与域事件来集成这些有界上下文。假设首先在承保上下文中创建了一个保单并使其生效。承保通常是指保险公司与客户达成协议以确保他们的利益,并且此时的保单已存在。现在,用保险术语来说,可能会认为此保单已签发,因此将由承保上下文发布保单签发域事件。发布保单域事件后,索赔上下文和检查上下文可以消费它并各自创建保单。因此,我们现在有三个单独的保单了。一个最初是在承保上下文中创建的,另外两个是对保单发布域事件的响应而创建的。 那么保单发布域事件保存了多少数据呢?它应该是有限的,我们不想发布包含来自承保上下文的所有状态的域事件。它应该只具有足够的状态来传达该保单已发布的事实,并允许在其他有界上下文中作出反应。 但有可能我们订阅者之一需要更多数据,该怎么做呢?我们可以使用在承保上下文中发布的保单ID,允许消费端在承保上下文中进行回查。承保上下文使用已发布的语言来实现开放主机服务,并且订阅上下文可以使用轻松的GET请求从承保上下文中获取更多数据。承保上下文使用保单ID发布其保单域事件,并且该保单的ID可用于回查来自承保上下文的其他信息。
