前面提到了为了降低复杂度,会将业务领域拆分为子领域,限界上下文,每个限界上下文是能够自治的完成一个业务价值的。
一般来说,一个业务场景是需要多个限界上下文合作才能完成的,比如酒店客人登记的场景,可能涉及到限界上下文有:会员,客情,客账,支付等。此时各限界上下文之间必须会产生依赖关系。整个业务系统又是由很多业务场景互相交织而成,此时的各限界上下文之间的依赖关系就会很复杂,我们必须要理顺这些依赖关系。
为了理顺各限界上下文的依赖关系,先需要识别上下文之间依赖关系产生的原因。

上下文依赖关系产生原因

领域行为产生的依赖

这种依赖是指当前上下文需要使用到别的上下文中的行为或接口而产生的依赖。
比如前面提到的酒店客人登记的场景中,当客人报出会员卡号或者手机号时,是需要由会员上下文来验证其会员身份是否有效,会调用会员上下文的验证行为,由此产生的对会员上下文的依赖属于领域行为产生的依赖。
也此类似的,收取客人押金时,会依赖于支付上下文来完成不同支付方式的具体收款动作,支付上下文又可能需要依赖于第三方的微信、支付宝等来完成收款动作。完成后,依赖于客账上下文,由客账上下文正常的记录押金收取结果。
这种依赖是属于正常的一种依赖关系,在整理依赖关系时,需要注意避免循环依赖即可,具体的依赖关系类型后面会详细介绍。

领域模型产生的依赖

这种依赖是指当前上下文需要使用到别的上下文中的领域模型而产生的依赖。
比如会员上下文中,给会员发放项目次卡时,需要知道客账上下文中的消费项目,此时他不是依赖于客账上下文的某个行为,而是依赖于消费项目这个领域模型。
image.png
这种依赖是需要进行解决的,后面会讲到解决方案。

数据产生的依赖

这种依赖是指绕过上下文代码,直接访问对方的数据库产生的依赖。
比如还是酒店登记场景中,不再调用会员上下文的验证行为,而是直接去会员数据库中查询某个会员卡号或者手机号是否有效。由此带来的问题如下:

  1. 必须熟悉会员上下文的数据库表结构和实体状态
  2. 会导致会员上下文团队不能完全自治的调整其数据库结构,因为调整后可能会影响到除自己以外的其他上下文。

这种依赖是不正确的,原则上是不允许出现的。

上下文关系术语

  • 上游(Upstream):代表被依赖方,上游的变更会影响到下游
  • 下游(Downstream):代表依赖方,依赖于上游,上游变更后,下游会受到影响
  • 发布者(Publisher):事件发布者,当其上下文内部某事件产生后,将此事件发布到消息队列中
  • 订阅者(Subscriber):事件订阅消费者,订阅感兴趣的其他上下文的事件,当接收到事件后,进行自己的进一步的业务处理。

注意:上下游之间的关系,是一种强的耦合关系,彼此是需要知道对方的。
发布订阅之间的关系,是一种弱的耦合关系,彼此之间是可以不知道对方的。
示例:
image.png

上下文及团队合作方式

由于拆分后的一个上下文是一个自治的整体,实现一个完整的业务价值,所以可以理解为一个上下文就是一个服务,由专门的业务团队进行开发。
当然根据上下文业务价值的重要程度和复杂度,团队的人员配置也会有所不同,可能好几个上下文都由同一个团队负责。所以此时上下文之间的关系也决定了团队之间的合作方式。

共享内核

还记得前面讲依赖关系时的,由于领域模型产生的依赖吗?上面只是讲了问题,没讲解决方案,现在讲第一种解决方案,就是共享内核。
实现方式如下图:
image.png
这种方式主要是将多个上下文都需要使用到的上下文提取出来,形成单独的上下文,给其他所有上下文提供支持。

客户方-供应方开发(Customer-Supplier Development)

这种方式是最常见的方式,体现了上下游的合作关系。
这种模式下,需要两个团队共同协商:

  • 下游团队对上游团队提出的领域需求
  • 上游团队提供的服务采用什么样的协议与调用方式
  • 下游团队针对上游服务的测试策略
  • 上游团队给下游团队承诺的交付日期
  • 当上游服务的协议或调用方式发生变更时,该如何控制变更

这种方式很好的体现了上下文的自治性,当下游团队遇到某需求时,发现不属于自己这个上下文范围的,是属于上游上下文范围的,只需要将需求提交给上游团队,由他们开发完成后,下游团队再实现自己业务范围内的相应功能即可。
这种方式需要注意,可能会有好多下游团队都给上游团队提交需求,而这些需求有些可能是有冲突的,也可能太多导致上游团队忙不过来,所以上游团队需要合理的和下游团队进行沟通,安排进度。同时要尽量保证接口的稳定性和扩展性,避免由于接口变更导致下游团队大面积的重新对接。
比如我们现在的各个业务系统和支付中间件组件之间的关系就是属于这种。
当各业务系统需要对接新的支付方式时,把新的支付方式的对接文档提交给支付中间件,支付中间件进行对接后,再公布新的接口给到各业务系统,各业务系统只需要简单的接内部接口调用就可以完成支付对接,复杂的签名算法,地址管理,请求封装,结果解析等都由支付中间件实现了。

遵奉者(Conformist)

上面的客户方-供应方开发方式有提到,下游团队提出领域需求后,上游团队由于一些原因可能会拒绝,此时下游团队也只能选择自己来实现这种需求了。
而实现此需求的相关业务知识都是在上游的领域模型中才有,所以下游团队会选择直接使用上游团队的领域模型,然后再增加一些新的模型类来实现新的领域需求。此时就产生了此合作方式。
还有一种情况下也会产生此合作方式,当上下游会共用一些模型,但又有所差异,此时可以选择后面的防腐层将上游的模型转换为下游的相应模型进行使用(也是推荐方式),但有时这种转换的工作量可能也是不小的,可能就会选择直接使用上游的领域模型,也就产生了此合作方式。

  • 好处:
    • 可以直接重用上游上下文模型
    • 减少了两个上下文之间模型的转换成本
  • 坏处

    • 下游上下文强依赖于上游模型
    • 当上游模型变更时,可能会产生对下游模型的不可预期的影响

      分离方式(Separate Ways)

      这种方式应该比较好理解。
      当下游给上游提交的需求被拒绝,下游自己实现时。或者下游有部分模型和上游的相同,但选择不直接使用上游的模型,而是完全自己另外新建一套新的模型,和上游没有任何关系,两者之间自由发展,互不影响。此时就产生了分离方式。
  • 好处

    • 和上游没有任何关系,不会受到上游的影响
  • 坏处
    • 业务重用性降低,重复造轮子

      防腐层(Anticorruption Layer)

      严格来说,这不是一个合作方式,而是一种实现方式。
      前面提到的合作方式中,除了分离方式外(因为分离方式下游和上游实际上不存在关系,可以自由发展),其他的无论是共享内核,客户方-供应方,遵奉者,都存在调用上游接口后,上游返回的一些实体在下游业务中会使用到。
      如果下游直接将上游返回的内容做为领域模型的一部分,则必然会导致,每次上游调整领域模型后,下游领域模型会受到影响,这样影响范围就会很广泛。
      为了限制这种影响范围,提出这种实现方式。
      限界上下文映射 - 图4
      在绘制上下文映射图时,我们往往用 ACL 缩写来代表防腐层.

      开放主机服务(Open Host Service)

      这种合作方式和客户方-供应方有些类似,都是上游定义接口方式,下游来进行对接。
      但在客户方-供应方的方式下,虽然接口方式是由上游定义的,但多半是专为下游提出的领域需求而设计的,具有针对性,可以采用一些内部协议或对接方式。
      而开放主机服务则是由上游针对所有下游(而不是某个具体的下游)所定义的,并且接口协议要采用常见的发布语言(比如RPC(Protocol Buffer)、WebService 或 RESTful)等,并且要保证协议的稳定性。
      在绘制上下文映射图时,我们往往用 OHS 缩写代表开放主机服务。

      发布/订阅事件

      前面讲的合作方式都适用于下游直接调用上游接口的情况。
      现在还有一种开发方式是通过事件推动的,比如酒店登记的场景中,客人登记成功后,需要发布客人入住XX房间的事件。然后客控系统接收到此事件后,会在房间的电视机上等客人打开时,就自动显示欢迎信息。餐饮入口的人脸设备会自动同步客人的人脸信息,以便客人去用早餐时,可以自动识别等。
      在这种情况下,是通过由上游来发布领域事件,下游来订阅,然后执行自己的业务流程。如果有必要时,下游也会发布一个业务执行结果,由上游来订阅后,记录特定领域事件的结果。