领域驱动设计07-辨别限界上下文的协作关系

在思考限界上下文之间的协作关系时,首先我们需要确定是否存在关系,然后再确定是何种关系,最后再基于变化导致的影响来确定是否需要引入防腐层、开放主机服务等模式。倘若发现协作关系有不合理之处,则需要反思之前我们识别出来的限界上下文是否合理。

1. 限界上下文通信边界对协作的影响

限界上下文的通信边界对于界定协作关系至为关键。限界上下文的通信边界分为进程内边界进程间边界,这种通信边界会直接影响到我们对上下文映射模式的选择。

  • 进程间通讯: 考虑跨进程访问的成本/网络开销/序列化等
  • 进程内通信: 数据库是否会耦合?

一个典型的协作方式是同时引入开放主机服务(OHS)与防腐层(ACL)

协作即依赖

如果限界上下文之间存在协作关系,必然是某种原因导致这种协作关系。从依赖的角度看,这种协作关系是因为一方需要“知道”另一方的知识,这种知识包括:

  • 领域行为:需要判断导致行为之间的耦合原因是什么?如果是上下游关系,要确定下游是否就是上游服务的真正调用者
  • 领域模型:需要重用别人的领域模型,还是自己重新定义一个模型。
  • 数据:是否需要限界上下文对应的数据库提供支撑业务行为的操作数据。

领域行为产生的依赖: 领域行为,其实就是每个领域对象的职责。对象履行职责的方式有三种,Rebecca Wirfs-Brock 在《对象设计:角色、职责与协作》一书中总结为:

  • 亲自完成所有的工作。
  • 请求其他对象帮忙完成部分工作(和其他对象协作)。
  • 将整个服务请求委托给另外的帮助对象。

先判断是不是真的产生了依赖。

一个好的设计,职责一定是“分治”的,就是让每个高内聚的对象只承担自己擅长处理的部分,而将自己不擅长的职责转移到别的对象

不同的职责分层会直接影响到我们对限界上下文协作关系的判断。归根结底,还是彼此之间需要了解的“知识”起着决定作用。我们应尽可能遵循“最小知识法则”,在保证职责合理分配的前提下,产生协作的限界上下文越少越好。

领域模型产生的依赖

针对领域行为产生的依赖,我们可以通过抽象接口来解耦。

一般有两种决策:重用和分离

如果是两个架构零共享的限界上下文,就不应该使用重用的的领域模型。因为这种模型的重用又导致了二者不再是“零共享”的。之所以采用零共享架构,是希望这两个限界上下文能够独立演化,包括部署与运行的独立性。倘若一个限界上下文重用了另一个限界上下文的领域模型,就意味着二者的代码模型是耦合的,即产生了包之间的依赖,而非服务的依赖。一旦该重用的模型发生了变化,就会导致依赖了该领域模型的服务也要重新部署。零共享架构带来的福利就荡然无存了。

如果二者的通信是发生在进程内。

  • 重用: 需求变更时,只需要改一处,避免的霰弹式修改。但是如果两个限界上下文对同一个领域模型的需求不相同,那么就会导致该咯领域模型渐渐成为一个低内聚的对象,这就是重用的代价。
  • 分离: 两个限界上下文中分别建立领域对象,这会带来代码的重复,新需求变化,需要修改两个模型。如果两个上下文新需求不相同,就可以独立应对不同的变化了。这就是“独立演进”

Sales Context 与 Support Context 都需要客户与商品信息,但它们对客户与商品的关注点是不相同的。销售可能需要了解客户的性别、年龄与职业,以便于他更好地制定推销策略,售后支持则不必关心这些信息,只需要客户的住址与联系方式。正如前面在讲解限界上下文的边界时,我们已经提到了限界上下文作为保持领域概念一致性的业务边界而存在。上图清晰地表达了为两个不同限界上下文分别建立独自的 Customer 与 Product 领域模型对象,而非领域模型的重用。

推荐分离的领域模型,因为相较于维护相似领域对象的成本,我更担心随着需求变化的不断发生需要殚精竭虑地规避(降低)重用的代价。

数据产生的依赖

所谓“数据产生的依赖”,来自于数据库。倘若严格遵循领域驱动设计,通常不会产生这种数据库层面的依赖,因为我们往往会通过领域模型的资源库去访问数据库,与数据库交互的对象也应该是领域模型对象(实体和值对象)。即使有依赖,也应该是领域行为与领域模型导致的。

我们必须警惕这种数据产生的依赖,没有绝对的理由,我们不要轻易做出这种妥协。SQL 乃至存储过程形成的数据表关联,是最难进行解耦的。一旦我们的系统架构需要从单体架构(或数据库共享架构)演进到微服务架构,最大的障碍不是代码层面而是数据库层面的依赖,这其中就包括大量复杂的 SQL 与存储过程。