上下文映射图为什么重要
在开始采用DDD时,首先你应该为你当前的项目绘制一个上下文映射图,其中应该包含你项目中当前的限界上下文和它们之间的集成关系。图3.1表示一个抽象的上下文映射图,我们将不断地向里面添加细节内容。
图3.1 一个抽象领域的上下文映射图。图中显示了3个限界上下文以及它们之间的集成关系。U表示上游(Upstream),D表示下游(Downstream)
以上这个简单的框图便可以作为你团队的上下文映射图。其他团队在实施DDD时应该创建他们自己的上下文映射图。上下文映射图主要帮助我们从解决方案空间的角度看待问题。
又是新术语?
这里我们引入了大泥球、客户方-供应方(Customer-Supplier)和遵奉者(Conformist)等概念。耐心一点,我们将在本章后面对这些概念做详细的解释。
比如,当你为一个大型企业进行限界上下文之间的集成时,你可能需要与大泥球进行交互。大泥球的维护团队才不关心你的项目呢,因为你依赖于他们的API。因此,他们并不会深入到你的上下文映射图中,也不会关心你是如何使用他们的API的。然而,你的映射图依然需要反映出和他们的集成关系,因为这样可以使你了解到映射图的内部,并且可以指明在哪些地方需要与其他团队进行交流。这样的理解对你团队的成功是有帮助的。
交流渠道
前面,我们已经讲到了与一个库存系统的交互。除了解决集成问题之外,上下文映射图还可以促进团队间的交流。
假定你期望大泥球维护团队提供一套新的API。然而,他们却并不打算这么做,或者他们根本就不知道你的想法。此时你的团队和大泥球维护团队的关系便成了客户方-供应方的关系。由于大泥球团队决定维持现状,你的团队不得不陷入一种遵奉者关系中。这样的关系可能导致你的项目延期交付或者彻底失败。尽早绘制上下文映射图,这样可以迫使你仔细思考你的项目和你所依赖项目之间的关系。
识别出项目中的每一个模型并确定它的限界上下文……为每个限界上下文命名,该名字应该是通用语言的一部分。描述模型之间的连接点,将模型间的翻译转换显式地勾勒出来。[Evans,p.345]
CollabOvation团队应该在建模之初就使用上下文映射图。虽然他们几乎是从零开始的,但以映射图的形式对项目做出假设可以帮助他们更好地分离限界上下文。同时,他们应该将一些显著的建模元素列在白板上,然后从语言层面对这些元素进行分组。这样可以迫使他们识别出语言边界,然后创建一个简单的上下文映射图。然而当时,他们对于战略建模一无所知。他们需要在战略建模上有所突破,后来,他们的确做到了。在之后的ProjectOvation中,他们尝到了战略建模的甜头。
接下来,让我们看看如何快速地生成一个可用的上下文映射图。
绘制上下文映射图
上下文映射图表现的是项目当前的状态,如果项目会在将来发生变化,你可以到那时才对上下文映射图做相应的更新。关注于当前的项目状态可以帮助你了解你正处的位置,并帮助你决定如何走出下一步。
绘制一个上下文映射图并不复杂。通常,首选在白板上手绘映射图,此时你可以采用[Brandolini]的风格。如果你打算使用一个绘图工具来绘制上下文映射图,请注意不要把图画得太正式了。
回到图3.1中,图中限界上下文的名字和彼此之间的集成关系只是占位符而已,在真实的上下文映射图中,我们将代之以实际的名字。图3.1中同时还展示了上游(upstream)和下游(downstream)两种集成关系,在本章后面我们将对此做详细讲解。
白板时间
为你的的项目绘制一个简单的框图,其中包含不同的限界上下文、它们之间的关系、各个上下文的团队、上下文之间的集成和必要的翻译等。
请注意,你的软件所实现的只是框图之内的内容。如果你想获得更多的信息,可以参考那些与你的限界上下文有集成关系的系统。
有时,我们希望对上下文映射图的某些特定部分进行放大,以向里面加入更多的细节。这只是从另外一个角度来看待同一个限界上下文。除了边界、关系和翻译,我们可能希望加入其他的一些内容,比如模块(9)、聚合(10),或者团队的分布信息等。我们将在本章后面讲到这些。
所绘制的所有映射图,包括文字,都可以装订在同一份参考文档中,只要这对团队是有价值的。在这个过程中,我们应该避免那些繁文缛节性的仪式,保持简单和敏捷。向框图中加入过多的细节对团队并无多大帮助,交流才是关键,我们应该将交流对话也加入到上下文映射图中。
这不是企业架构
上下文映射图并不是一种企业架构,也不是系统拓扑图。
上下文映射图并不是一种企业架构,也不是系统拓扑图。但是,它可以用于高层次的架构分析,指出诸如集成瓶颈之类的架构不足。上下文映射图展现了一种组织动态能力(organizational dynamic),它可以帮助我们识别出有碍项目进展的一些管理问题。
牛仔的逻辑
AJ:“我的夫人对我说,‘我刚才在外面的草场上和奶牛呆了一会儿,你看见我了吗?’我说,‘没有。’接下来这一周,她都没有跟我说话。”
这些框图可以贴在墙上一个显著的位置,这样团队成员都可以看到。如果团队经常使用wiki,这些框图也可以在wiki中进行维护,否则,大可不必这么做。有这么一种说法,wiki是葬送信息的地方。不管你将这些框图放在什么地方,上下文映射图都只是默默地呆在那里,除非团队成员经常去关注这些框图并围绕着展开讨论。
产品和组织关系
这里,我们简单重复一下SaaSOvation公司正在开发的3个产品:
1. CollabOvation——一款社交协作产品。该产品允许注册用户发布对业务有价值的内容,发布方式是一些流行的基于Web的工具,比如论坛、共享日历、博客和wiki等。这是SaaSOvation公司的旗舰产品,也是该公司的第一个核心域(2)(即使当时他们还并不知道“核心域”这个DDD术语)。开发团队后来从CollabOvation中提取出了IdOvation模型(第2点)。对于CollabOvation来说,IdOvation是一个通用子域(2),而CollabOvation本身又作为ProjectOvation(第3点)的支撑子域(2)。
2. IdOvation——一款可重用的身份和访问管理产品。IdOvation为注册用户提供安全的、基于角色的访问管理。这些功能一开始和CollabOvation(第1点)混合在一起,这样导致的问题是:实现受到了限制,功能不可重用。SaaSOvation对CollabOvation进行了重构,引入了一个新的、清晰的限界上下文。SaaSOvation公司决定支持多个租户,这种功能对于SaaS产品来说是至关重要的。对于消费方来说,IdOvation扮演着通用子域的角色。
3. ProjectOvation——一个款敏捷项目管理产品。这是SaaSOvation公司的新核心域。ProjectOvation采用基于Scrum的项目运行框架,用户可以创建项目管理资产,同时对项目资产进行分析和设计,还能跟踪项目进度。和CollabOvation一样,ProjectOvation将IdOvation作为一个通用子域来使用。该产品的一大创新是将CollabOvation(第1点)引入了敏捷项目管理,这样用户可以围绕Scrum的产品、发布、冲刺和待定项展开讨论。
终于轮到定义了!
我们马上将对前面提到的组织模式和集成模式进行定义……
这些限界上下文之间的关系如何,不同开发团队之间的关系又如何?在DDD中,存在多种组织模式和集成模式,其中,有一种模式存在于任意两个限界上下文之间。以下的定义在很大程度上来自于[Evans,Ref]。
• 合作关系(Partnership):如果两个限界上下文的团队要么一起成功,要么一起失败,此时他们需要建立起一种合作关系。他们需要一起协调开发计划和集成管理。两个团队应该在接口的演化上进行合作以同时满足两个系统的需求。应该为相互关联的软件功能制定好计划表,这样可以确保这些功能在同一个发布中完成。
• 共享内核(Shared Kernel):对模型和代码的共享将产生一种紧密的依赖性,对于设计来说,这种依赖性可好可坏。我们需要为共享的部分模型指定一个显式的边界,并保持共享内核的小型化。共享内核具有特殊的状态,在没有与另一个团队协商的情况下,这种状态是不能改变的。我们应该引入一种持续集成过程来保证共享内核与通用语言(1)的一致性。
• 客户方-供应方开发(Customer-Supplier Development):当两个团队处于一种上游-下游关系时,上游团队可能独立于下游团队完成开发,此时下游团队的开发可能会受到很大的影响。因此,在上游团队的计划中,我们应该顾及到下游团队的需求。
• 遵奉者(Conformist):在存在上游-下游关系的两个团队中,如果上游团队已经没有动力提供下游团队之所需,下游团队便孤军无助了。出于利他主义,上游团队可能向下游团队做出种种承诺,但是有很大的可能是:这些承诺是无法实现的。下游团队只能盲目地使用上游团队的模型。
• 防腐层(Anticorruption Layer):在集成两个设计良好的限界上下文时,翻译层可能很简单,甚至可以很优雅地实现。但是,当共享内核、合作关系或客户方-供应方关系无法顺利实现时,此时的翻译将变得复杂。对于下游客户来说,你需要根据自己的领域模型创建一个单独的层,该层作为上游系统的委派向你的系统提供功能。防腐层通过已有的接口与其他系统交互,而其他系统只需要做很小的修改,甚至无须修改。在防腐层内部,它在你自己的模型和他方模型之间进行翻译转换。
• 开放主机服务(Open Host Service):定义一种协议,让你的子系统通过该协议来访问你的服务。你需要将该协议公开,这样任何想与你集成的人都可以使用该协议。在有新的集成需求时,你应该对协议进行改进或者扩展。对于一些特殊的需求,你可以采用一次性的翻译予以处理,这样可以保持协议的简单性和连贯性。
• 发布语言(Published Language):在两个限界上下文之间翻译模型需要一种公用的语言。此时你应该使用一种发布出来的共享语言来完成集成交流。发布语言通常与开放主机服务一起使用。
• 另谋他路(SeparateWay):在确定需求时,我们应该做到坚决彻底。如果两套功能没有显著的关系,那么它们是可以被完全解耦的。集成总是昂贵的,有时带给你的好处也不大。声明两个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的、专门的方法来解决问题。
• 大泥球(Big Ball of Mud):当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,它们之间的边界是非常模糊的。此时你应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列。在这个边界之内,不要试图使用复杂的建模手段来化解问题。同时,这样的系统有可能会向其他系统蔓延,你应该对此保持警觉。
在与身份与访问上下文集成时,协作上下文和敏捷项目管理上下文均没有采用另谋他路的手法。诚然,在上下文范围之内,另谋他路的手法可以应用在特殊的系统上,同时它也可以用于一些个例。比如,一个团队可能拒绝使用集中式的安全管理系统,但他们依然会与另外类型的安全管理系统集成。
SaaSOvation的不同团队之间存在着客户方-供应方关系。SaaSOvation公司的管理层绝对不会允许一个团队迫使另一个团队成为遵奉者。并不是说遵奉者关系所带来的影响总是负面的,而是客户方-供应方关系要求供应方向客户方提供支持,这种团队关系有助于SaaSOvation获得整体性的成功。当然,客户方也不总是正确的,这时团队之间需要的是礼尚往来。总的来说,团队应该好好地保持这种积极的组织关系。
在集成时,他们将会用到开放主机服务和发布语言,有可能还会用到防腐层。虽然这两者都会在限界上下文之间创建开放的标准,但是它们并不矛盾。通过使用下游上下文中的基本原则,他们依然可以得到由独立翻译[1]所带来的好处,并且不会像与大泥球集成时那样复杂。翻译层将变得简单、优雅。
在上下文映射图中,我们使用以下缩写来表示各种关系:
• ACL表示防腐层
• OHS表示开放主机服务
• PL表示发布语言
在阅读下一节时,你可以回头参考第2章,“领域、子域和限界上下文”。第2章中关于这3个示例限界上下文的框图在这里也是有用的。由于描述的是高层次概念,这些框图同样可以作为上下文映射图的一部分。
映射3个示例限界上下文
现在,让我们看看SaaSOvation公司的开发团队都在做些什么……
当CollabOvation团队意识到自己已经陷入僵局时,他们开始向[Evans]求助。在众多的战略设计模式中,他们发现了一种名为“上下文映射图”的工具非常实用。同时,他们还在网上发现了一篇文章[Brandolini],该文中对上下文映射图做了扩展性的讲解。由于上下文映射图建议只对已有的系统进行映射,他们立即开始把映射图用于协作上下文,如图3.2所示。
从团队设计的第一张映射图可以看出,他们已经知道应该创建一个名为“协作上下文”的限界上下文。从图中怪异的边界又可以看出,他们可能希望创建第二个限界上下文,但是却不知道如何将这个上下文从核心域中分离出来。
图3.2 协作上下文中的三角形表示一些不受欢迎的概念,其中的感叹号 表示一些不纯净的区域。
在图3.2中,位于图上方的狭窄通道(警告标志处)可能成为一些外部概念肆意进出之所。这里并不是说上下文边界一定得密不透风,而是:对于任何上下文边界,开发团队都希望协作上下文能够完全地控制所进所出,还包括进出的原因。否则,该上下文中便会出现一些不受欢迎的“拜访者”。对于模型而言,这些“拜访者”通常会导致混淆和bug。建模人员应该是友好的,但是友好的前提是秩序与和谐。任何进入边界的外部概念都应该持有充分的理由,甚至需要和边界内的模型保持良好的兼容性。
有了这样的分析,团队成员不仅对当前的模型有了更好的理解,同时还知道了如何迈出项目的下一步。一旦团队成员意识到安全、用户和权限并不属于协作上下文,他们便会采取相应的措施。他们需要将这些概念从核心域中分离出来,使这些概念只有在得到同意的时候才能进入核心域。
这是对于DDD项目很重要的贡献。限界上下文的通用语言维持了所有模型的纯洁性,这一点,通用语言实至名归。语言上的分离有助于各个团队将精力集中在各自的限界上下文上。
在对子域进行了分析,或对问题空间进行了评估之后,团队所绘制的上下文映射图如图3.3所示。从一个限界上下文中分离出了两个子域。由于子域和限界上下文最好是保持一对一的关系,我们应该将原先的协作上下文分离成两个限界上下文。
图3.3 团队对子域的分析导致了两个子域的产生:协作核心域和安全通用子域。
对子域和边界的分析要求我们做出决定。当人们需要使用CollabOvation的功能时,他们扮演的是参与者、作者和主持者等角色。有了这样的角色分离,我们便可以绘制一个更高层次的上下文映射图,如图3.4所示。团队使用了分离内核[Evans]对系统进行了重构。
图3.4 原有核心域的边界和集成点以粗线标记。这里的IdOvation作为下游CollabOvation的通用子域。
在理解充分的情况下,要绘制上下文映射图并不难,但是通常来说,我们并不会将映射图中的所有内容都显示出来。在迭代过程中,思考和讨论可以帮助我们改进上下文映射图,比如对集成点进行改进,这将有助于描述限界上下文之间的关系。
在上面3个上下文映射图中,前2个表明了团队的确从战略设计中有所收获。团队成员需要在原先的CollabOvation项目中抽出与身份和访问相关的内容,进而形成如图3.4所示的上下文映射图。团队成员只绘制了核心域、协作上下文、新的通用子域,还有身份与访问上下文。他们并没有把未来的敏捷项目管理上下文也包含在内,而加入该上下文对团队并无益处。此时,他们只需要对已有系统进行更正和改进。今朝有酒今朝醉,明日愁来明日忧。
白板时间
• 对于你自己的限界上下文,你能识别出那些不该属于其中的概念吗?如果是,绘制一个新的上下文映射图,将这些概念放在另一个上下文中,再在图中标明两个限界上下文之间的关系。
• 对于上文中的9种DDD组织和集成关系,你会选择哪一种,为什么?
当下一个ProjectOvation项目启动时,团队将使用这个新的核心域——敏捷项目管理上下文来增强已有的上下文映射图,如图3.5所示。此时,我们已经能看出哪些方面正在计划当中了,即便它们还没有编码实现。团队成员还不大理解新上下文中的细节,但是通过讨论,他们是可以做到这点的。在项目早期采用战略设计可以帮助所有的团队成员了解他们的职责。由于第3个映射图是对前两个的改进,我们将把关注点放在该映射图上。SaaSOvation公司将领队开发人员分配到了新的PorjectOvation项目。作为最“富有”的上下文,这个新的核心域就应该是最优秀的开发者工作的地方。
SaaSovation的开发团队已经对核心的模型分离有了很好的理解。与CollabOvation相似,当ProjectOvation的用户创建产品、计划发布、冲刺,或者工作在待定项上时,他们扮演的是产品负责人或者团队成员的角色。身份与访问上下文已经从核心域中分离出去了。对于协作上下文也是这种情况,此时的协作上下文对于ProjectOvation来说只是一个支撑子域。任何时候,这个新的模型都受到了上下文边界的保护,外部概念需要通过翻译才能进入核心域中。
看看图3.5中框图的细节,他们并不是系统架构图。如果是,我们应该将新核心域——敏捷项目管理上下文放在最上部或者中间位置才对。但是此处,它却位于底部,表示这个核心模型位于其他系统的下游。
图3.5 当前核心域的边界和集成点以粗线标记。CollabOvation支撑子域和IdOvation通用子域为上游。
这种细微的差别还表明了另一点:上游模型会对下游模型产生影响,不管是正面的还是负面的,就像河流一样。考虑一个城市向河流中倾倒的污染物,这些污染物可能对本城市的影响很小,但是下游城市便深受其害了。映射图中框图的上下关系表明了上游模型对下游模型的影响,同时字母U和D显式地指出了它们之间的关系。有了这些字母标签,上下文的位置关系就不那么重要了,但是,这些位置关系依然能给我们营造一种好的视觉效果。
牛仔的逻辑
LB:“当你口渴时,记得在牛群的上游喝水。”
身份与访问上下文位于最上游,它对协作上下文和敏捷项目管理上下文均会产生影响。同时,协作上下文又是敏捷项目管理上下文的上游,因为后者的模型依赖于前者的模型和服务。在限界上下文(2)中我们提到,ProjectOvation将自治地运行,而不会依赖于周边系统。这并不是说自治服务就可以完全独立于上游模型,而是我们的设计应该尽可能地限制实时依赖性。虽然ProjectOvation是自治的,但是它依然属于其他系统的下游。
拥有自治服务的应用程序并不表示需要将上游系统的数据库复制到下游系统中。数据库复制将迫使本地系统承担过多的职责,它需要创建一个共享内核,而这并不是真正意义上的自治。
在第三个映射图中,请注意上游系统的连接框,它们都标以OHS/PL,分别表示开放主机服务和发布语言。所有下游系统的连接框都标以ACL,即防腐层。对于它们的技术实现将在集成限界上下文(13)中讲到。简单地讲,这些集成模式将采用以下技术:
• 开放主机服务:该模式可以通过REST实现。通常来讲,我们可以将开放主机服务看成是远程过程调用(Remote Procedure Call,RPC)的API。同时,它也可以通过消息机制实现。
• 发布语言:发布语言可以通过多种方式实现,但最常见的是使用XML Schema。在使用REST服务时,发布语言用来表示领域概念,此时可以使用XML和JSON。发布语言也可以使用Google的协议缓冲(Protocol Buffer)来表示。如果你打算发布Web用户界面,你也可以使用HTML。使用REST的好处在于每个客户端都可以指明使用哪种发布语言,同时还可以指明资源的展现方法。REST的另一个优点是超媒体(Hypermedia)展现,即HATEOAS(Hypermedia As The Engine Of Application State)。超媒体赋予发布语言以动态性和可交互性,使得客户端可以访问一系列彼此链接的资源。发布语言既可以使用标准的媒体类型进行发布,也可以使用自定义类型。同时,发布语言还可以用于事件驱动架构(Event-Driven Architecture,4),其中领域事件(Domain Event,8)以消息的形式发送到订阅方。
• 防腐层:在下游上下文中,我们可以为每个防腐层定义相应的领域服务(Domain Service,7)。同时,你也可以将防腐层用于资源库(12)接口。在使用REST时,客户端的领域服务将访问远程的开放主机服务,远程服务器以发布语言的形式返回,下游的防腐层将返回内容翻译成本地上下文的领域对象。比如,协作上下文向身份与访问上下文请求“具有Moderator角色的用户”。所返回的数据可能是XML格式或JSON格式,然后防腐层将这些数据翻译成协作上下文中的Moderator对象,该对象是一个值对象。这个Moderator实例反映的是下游模型中的概念,而不是上游模型。
以上3个模式是非常常见的。这里我并不打算对每种集成模式都做讲解,我们将看到,即便我们只是选择性地讲解了少数几种模式,我们依然能够看出这些模式在应用上的不同。
剩下的问题是:以上便是创建上下文映射图的全部吗?可能吧。高层视角已经为我们提供了有关项目整体的大量信息。然而,我们可能会对这些集成关系和上下文间连接的内部感到好奇。当我们对上下文映射图进行放大时,以上3种集成模式的内部结构将会变得清晰起来。
让我们往后退一步。由于协作上下文是第一个核心域,让我们把它放大来看看。首先我们将接触到一些简单的集成方式,然后再是更高级的。
协作上下文
现在,让我们看看CollabOvation项目的进展情况……
协作上下文作为SaaSOvation公司的第一个核心域,开发团队已经对其有很好的理解了。这里,他们使用的集成方式比较简单,但是在可靠性和自治性上还稍逊一筹。要将此上下文映射图进行放大是相对容易的。
身份与访问上下文通过REST的方式向外发布服务。作为该上下文的客户,协作上下文通过传统的类似于RPC的方式获取外部资源。协作上下文并不会永久性地记录下从身份与访问上下文中获取来的数据,而是在每次需要数据时重新向远程系统发出请求。显然,协作上下文高度依赖于远程服务,它不具有自治性。但就目前而言,SaaSOvation公司是愿意采取这种方式进行集成的。与一个通用子域进行集成属于SaaSOvation意料之外的事情,而为了满足交付计划的需求,他们不敢将时间浪费在实现软件的自治性上,而是采用了一种相对简单的方案。由于之后的ProjectOvation项目将使用自治性服务,CollabOvation到时可以借鉴ProjectOvation的做法。
在图3.6所示的放大后的上下文映射图中,下游系统中的边界对象(boundary object)采用同步的方式向上游系统获取资源。当获取到远程模型数据之后,边界对象取出所需数据,再将其翻译成适当的值对象实例。在图3.7中,翻译图(Translation Map)将所获数据转化成一个值对象。这里,身份与访问上下文中一个具有Moderator角色的User被翻译成了协作上下文中的Moderator值对象。
白板时间
对于你自己项目的限界上下文,为你所感兴趣的一个集成点创建一个翻译图。
如果翻译过于复杂,并且需要大量的数据复制和同步,从而使得翻译前后的模型存在很大的相似度,此时你该怎么办?你可能过多地使用了外部上下文中的数据,从而导致自己的模型混淆不清。
不幸的是,如果由于远程系统不可用而导致同步请求失败,那么本地系统也将跟着失败。此时本地系统将通知用户所发生的问题,并告诉用户稍后重试。
系统集成通常依赖于RPC。从高层面上看,RPC与编程语言中的过程调用非常相似。一些程序库和工具使得RPC极具吸引力,使用起来也非常简单。然而,和在相同进程空间中进行过程调用不同的是,远程调用更容易产生有损性能的时间延迟,并且有可能导致调用彻底失败。网络和远程系统的加载过程都是RPC产生延迟的原因。当RPC的目标系统不可用时,用户对你系统的请求也将失败。
虽然REST并不是真正意义上的RPC,但它却具有与RPC相似的特征。彻底的系统失败并不多见,但它却是一个潜在的问题。CollabOvation团队急切地希望解决这个问题。
图3.6 协作上下文和身份与访问上下文集成时的防腐层和开放主机服务。
图3.7 如何将展现状态(XML)映射到本地模型中的值对象。
敏捷项目管理上下文
由于敏捷项目管理上下文是新的核心域,让我们重点关注一下。让我们将该上下文以及它与其他模型的连接关系放大来看看。
为了达到比RPC更高的自治性,敏捷项目管理上下文的团队将尽量限制对RPC的使用,此时他们可以选择异步请求,或者事件处理等方式。
如果系统所依赖的状态已经存在于本地,那么我们将获得更大的自治性。有人可能认为这只是对所有的依赖对象进行缓存,但这不是DDD的做法。DDD的做法是:在本地创建一些由外部模型翻译而成的领域对象,这些对象保留着本地模型所需的最小状态集。为了初始化这些对象,我们只需要有限的RPC调用或REST请求。然而,要与远程模型保持同步,最好的方式是在远程系统中采用面向消息的通知(notification)机制。消息通知可以通过服务总线进行发布,也可以采用消息队列或者REST。
做一个极简主义者
被同步的状态应该是本地模型所需远程模型的最小属性集。这里并不只是限制我们的对数据的需求,还应该对概念进行恰当的建模。
限制对远程模型状态的使用是值得的,即使考虑到对本地模型本身的设计时也是如此。例如,我们并不希望将ProductOwner和TeamMember分别映射到UserOwner和UserMember,因为这样它们承担了远程User对象的过多特征属性,从而在不经意间导致了一种“杂交”状态。
和身份与访问上下文集成
在图3.8中我们看到,对于身份与访问上下文中的领域事件,系统将以URI的方式向外发布事件通知。这种功能是通过NotificationResource提供的,它向外发布REST资源。这里的通知资源是一组被发布的领域事件。对于消费方来说,每个事件总是可用的。消费方应该避免对事件的重复消费。
一个自定义的媒体类型表明客户可以请求两种资源:
通过第一个资源URI,客户可以(使用HTTPGET请求)获取到当前的通知日志(一个固定大小的通知集合)。对于自定义的媒体类型:
application/vnd.saasovation.idovation+json
图3.8 敏捷项目管理上下文和身份与访问上下文集成时的防腐层和开放主机服务。
可以看出,这里的URI是全新的,并且是稳定的,因为它不会改变。无论当前的通知日志中包含了什么样的内容,该URI都会将其发布。当前日志是在身份与访问上下文模型中最近产生的事件的一个集合。通过第二个URI,客户可以获得先前存档的所有事件通知。为什么我们同时需要当前日志和存档日志呢?请参考领域事件(8)和集成限界上下文(13)中有关基于订阅源(feed)通知的工作机制。
事实上,ProjectOvation团队并不打算全盘使用REST。比如,他们目前正与CollabOvation团队协商是否可以使用消息机制,例如RabbitMQ。但就目前来说,它们和身份与访问上下文的集成依然是基于REST的。
现在,让我们先忽略技术细节,看看映射图中各个交互对象所扮演的角色。图3.9表示集成过程的序列图,对其解释如下:
• MemberService是一个领域服务,它向本地模型提供ProductOwner和TeamOwner对象,同时作为基本防腐层的接口。maintainService()方法用于周期性地检查身份与访问上下文所发出的通知。该方法不由模型的正常客户调用,而是由通知组件周期性地调用,在图3.9中,MemberSynchronizer表示这样的通知组件,它会把请求委派给MemberService。
• MemberService进一步把请求委派给IdentityAccessNotificationAdapter,该类在领域服务和远程的开放主机服务之间扮演者适配器的角色。该适配器作为远程系统的客户端而存在。与远程NotificationResource的交互并没有显示在图3.9中。
图3.9 敏捷项目管理上下文和身份与访问防腐层的内部工作机制。
• 一旦适配器从远程的开放主机服务获取到了数据,它将调用MemberTranslator的toMember()方法将发布语言中的媒体数据翻译成本地系统的领域对象Member。如果该对象在本地系统已经存在,则更新该对象。MemberService的updateMember()方法用于更新一个Member对象,此时它把更新操作委派给了自己。Member的子类有ProductOwner和TeamMember,它们反映了本地系统中的上下文概念。
我们不应该将重点放在技术实现或集成产品上,而应该放在限界上下文之间的分离上,这样我们可以保持每个上下文的纯洁性,同时将一个上下文中的数据用在另一个上下文的概念中。
以上框图和辅助性文本向我们展示了如何创建一个上下文映射图文档。该文档不必非常详尽,但需要提供足够的背景信息和概念解释,以使得团队的新成员能够迅速上手。然而,请记住,只有在对团队有用的情况下,我们才创建上下文映射图文档。
与协作上下文集成接下来,让我们看看敏捷项目管理上下文是如何与协作上下文集成的。同样,我们关注的是系统的自治性,但这给集成带来了更多的困难与挑战。
ProjectOvation将使用CollabOvation所提供的附加功能,比如论坛讨论和共享日历等。ProjectOvation用户并不直接与CollabOvation交互。对于某个租户来说,ProjectOvation必须决定出哪些CollabOvation的功能是对该租户可用的。然后,ProjectOvation将协调对CollabOvation资源的创建。
考虑如下“创建产品”的用例:
1.用户提供该产品的描述信息
2.用户希望展开团队讨论
3.用户向ProjectOvation发出产品创建请求
4.ProjectOvation创建该产品,同时为其创建论坛(Forum)和讨论(Discussion)
Forum和Discussion必须在协作上下文中进行创建,这与身份与访问上下文不同。在后者中,一个租户在是已经存在的,而用户、用户群和角色等信息也已经被定义好了,事件通知对于外界来说也是可用的。也就是说,在身份与访问上下文中,对象是先前存在的。而对于敏捷项目管理上下文来说,对象是不会预先存在的,直到被请求时为止。这对于实现系统自治性来说是一个潜在的障碍,因为我们依赖于协作上下文来远程地创建资源。
Discussion为什么会在两个上下文中使用
这是一个有趣的现象,因为在两个上下文中,Discussion只是名字相同而已,其类型和实例对象都是不同的,因此Discussion在两个上下文中的状态和行为也是不同的。
在协作上下文中,Discussion是一个聚合,它管理一系列的Post。而在敏捷项目管理上下文中,Discussion只是一个值对象,它维护了对协作上下文中某个Discussion的引用。请注意,在第13章中,当团队在实现集成时,他们发现应该为敏捷项目管理上下文中不同种类的Discussion采用强类型。
在使用领域事件(8)和事件驱动架构(Event-Driven Architecture,4)时,我们应该仔细思考最终一致性(Eventual Consistency)。本地系统产生的事件通知并不是只能由远程系统消费。在ProjectOvation中,当ProductInitiated事件产生时,该事件将由本地系统进行处理。本地系统要求Forum和Discussion在远程完成创建,这可以通过RPC或消息机制完成,当然这取决于CollabOvation支持哪种类型的通信方式。在使用RPC时,如果远程的CollabOvation系统不可用,PorjectOvation将定期重试直到成功为止。如果采用消息机制,ProjectOvation将向CollabOvation发出消息,在资源创建成功之后,CollabOvation同样会以消息的形式返回。当ProjectOvation接收到返回的消息时,它将使用新建Discussion的标识引用来更新本地的Product对象。
如果项目负责人或团队成员试图使用一个不存在的Discussion时会发生什么情况呢?我们可以认为这是一个bug吗?这将导致系统处于不稳定状态吗?有时我们可能由于没有预先付款而导致协作功能不可用,但这并不是一个技术原因。对最终一致性的处理绝非什么东拼西凑之事,我们应该将其考虑在建模范围之内。
处理资源不可用的一个好办法便是将其显现出来。考虑以下由标准类型实现的状态(State)模式[Gamma et al.]。此时的状态是一个值对象(6):
在该设计中,由DiscussionAvailability定义的状态对象将对值对象Discussion起保护作用。当有人试图参加关于一个Product的讨论时,该设计将正确地处理Discussion的状态。如果状态不为READY,参与者将得到以下信息之一:
要使用团队协作功能,你需要购买附加功能。
产品负责人还没有请求创建产品讨论。
讨论启动失败,请稍后再试。
如果状态为READY,那么我们允许所有的团队成员都参与讨论。
有趣的是,从以上的第一条信息可以看出,PorjectOvation将协作功能作为了可选项显示在用户界面上,即便这些功能还未被购买也是如此,这对于市场营销是有好处的,因为这种方式可以时常提醒用户购买CollabOvation附加功能。显然,使用资源可用性状态的好处并不只是技术上的,还有商业上的。
此时,ProjectOvation团队还不清楚采用哪种方式与CollabOvation进行集成。在讨论了客户方-供应方关系之后,他们得到了图3.10。敏捷项目管理上下文可以使用第二个防腐层来处理与协作上下文之间的集成。图3.10中显示了主要的边界对象,它们和图3.8中的边界对象相似。事实上并不存在单个CollaborationAdapter,此处它只是一个占位符而已,表示有可能的多个适配器。
敏捷项目管理上下文在本地有DiscussionService和SchedulingService,它们是领域服务,用于管理协作系统中的讨论和日历条目。具体的实现机制由双方团队协商而定,这将在集成限界上下文(13)中讲到。
图3.10 敏捷项目管理上下文和协作上下文集成时的防腐层和开放主机服务。
现在,ProjectOvation团队部分地理解了他们当前的模型。那么,当CollabOvation创建好了一个讨论并将结果返回到ProjectOvation后,ProjectOvation又应该如何处理呢?此时,ProjectOvation将使用异步组件——不管是RPC客户端还是消息处理器——来调用Product的attachDiscussion()方法,传入的参数为一个新创建的Discussion值对象。所有依赖于远程资源的本地聚合都会通过这种方式处理。
以上的例子已经向我们展示了上下文映射图的某些细节。然而,我们的讲解应该稍稍收敛一下了,因为一不注意就有可能处于收效递减的地步。也许我们还应该引入模块(9),但是我们会在另外的章节中对模块做单独讲解。我们应该将有助于团队交流的高层次元素加入上下文映射图中,而不是冗繁的细节。
你创建的上下文映射图应该可以贴在墙上,这样所有团队成员都可以看到;或者你可以将映射图上传到项目的wiki中。团队应该经常性地围绕上下文映射图展开讨论,并对其进行改进。