不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

DDD领域驱动战略篇(3) 上下文映射与协作

DDD领域驱动战略篇(3)

上下文映射

理解上下文映射

上下文映射是为了用来描述限界上下文之间的协作问题,上下文映射是一种设计手段包含:共享内核、防腐层、开放式主机等多种协作模式
上下文映射是用于将限界上下文边界变得更清晰的重要工具。所以当我们正在为一些限界上下文的边界划分而左右为难时,不妨先放一放,在定下初步的限界上下文后,通过绘制上下文映射来检验,或许会有意外收获。

限界上下文之间关系是有方向的:

U 代表上游,D 代表下游,上游限界上下文作为下游限界上下文的功能支撑

用例图中的包含用例或扩展用例或许是一个不错的判断上下文协作关系的切入点,选择从包含或扩展关系切入,既可能确定了职责分离的逻辑边界,又可以确定协作关系的方向,这就是用例对领域驱动设计的价值所在了
上下文映射可以分为两大类:团队协作模式和通信集成模式

上下文映射的团队协作

领域驱动设计根据团队协作的方式与紧密程度,定义了五种团队协作模式

合作关系

合作关系:两个限界上下文同生共死的关系,合作越多依赖越多,耦合越严重,甚至出现双向依赖
不好的示例:图中出现了双向依赖,循环依赖,可谓是貌合神离,并没有真正的分开

解决之道:1 干脆合在一起;2 尽量减少一个方向上多余依赖;3 抽取双向依赖循环依赖原因,建立单独限界上下文(共享内核)

新引入的 Metadata 成为了其余限界上下文的上游,解除了 DataEngine 对 ReportEngine 的依赖,同样解除了 EntityEngine 以及 ReportDesigner 对 ReportEngine 的依赖

共享内核

共享内核是解除不必要依赖实现重用的重要手段,共享内核仍然属于领域的一部分,分离出来的共享内核属于上游团队的职责,因而需要处理好它与下游团队的协作
共享内核往往被用来解决合作关系引入的问题
要特别注意:共享内核可能是多个限界上下文共同的上游,每次修改都可能牵一发而动全身

客户方与供应方

正常情况下,这是团队合作中最为常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系
面对多个下游时,如何排定不同领域需求的优先级,如何针对不同的领域需求建立统一的抽象,都是上游团队需要考虑的问题
采用持续集成为上下游建立集成测试和API测试等自动化测试的构建与发布,可以更好的规避风险

遵奉者

由上游团队来决定是响应还是拒绝下游团队提出的请求时,所谓的“遵奉者”模式就产生了
领域驱动设计提出的“限界上下文”实践,影响的不仅仅是设计决策与技术实现,还与企业文化、组织结构直接有关
遵奉者还有一层意思是下游限界上下文对上游限界上下文模型的追随
做出遵奉模型决策的前提是需要明确这两个上下文的统一语言是否存在一致性

分离方式

分离方式的合作模式就是指两个限界上下文之间没有哪怕一丁点儿的丝毫关系,就是无关系
这种“无关系”仍然是一种关系,而且是一种最好的关系
这种没有关系的关系似乎无足轻重,其实不然,它对改进设计质量以及团队组织都有较大帮助。两个毫无交流与协作关系的团队看似冷漠无情,然而,正是这种“无情”才能促进它们独立发展,彼此不受影响

上下文映射的通讯集成

防腐层ACL

防腐层其实是设计思想“间接”的一种体现,引入一个中间层,有效隔离限界上下文之间耦合
防腐层经常扮演:适配器、调停者、外观等角色(设计模式中常见几种结构型模式)
防腐层往往属于下游限界上下文,用以隔绝上游限界上下文可能发生的变化

对付遗留系统时,防腐层可谓首选利刃

开放主机服务OHS

开放主机服务就是上游服务用来吸引更多下游调用者的诱饵
设计开放主机服务,就是定义公开服务的协议,包括通信的方式、传递消息的格式(协议)。同时,也可视为是一种承诺,保证开放的服务不会轻易做出变化
因为开放主机服务一般位于上游,对应多个下游,所以不建议为每个下游都做一个防腐层,实践时候防腐层一般在下游

发布订阅事件

采用发布/订阅事件的方式可以在解耦合方面走得更远
当确定了消息中间件后,发布方与订阅方唯一存在的耦合点就是事件,准确地说,是事件持有的数据
一个电商购物流程发布订阅事件流程:


事件的发布和订阅并不一定是异步的或跨进程的

辨别限界上下文协作关系

首先我们需要确定是否存在关系,然后再确定是何种关系,最后再基于变化导致的影响来确定是否需要引入防腐层、开放主机服务等模式

通信边界对协作的影响

限界上下文的通信边界分为进程内边界与进程间边界,这种通信边界会直接影响到我们对上下文映射模式的选择

这里存在一个设计决策,即引入开放主机服务与防腐层是否必要?这就需要设计者权衡变化、代码重用、架构简单性的优先级

协作即依赖

从依赖的角度看,这种协作关系是因为一方需要“知道”另一方的知识(领域行为,领域模型,数据)
不要在领域建模过程中过多纠缠建模的细节,选择一个恰好合理的模型即可
从建模到设计,再从设计到编码开发,其实是一个迭代的过程,倘若在实现时确实发现模型存在瑕疵,再回过头来修改即可

领域行为产生的依赖

针对领域行为产生的依赖,我们可以通过抽象接口来解耦,通过引入防腐层(ACL)解除了对促销上下文的直接依赖
示例:根据促销策略计算订单的总价(上下文协作设计)

  • 订单上下文分别去调用客户上下文和促销上下文,客户侧调用1次,促销侧调用2次


  • 订单只调用促销上下文,由促销上下文再调用客户上下文,促销侧调用了2次

  • 可以对促销上下文的调用进一步进行封装,只调用1次,上下文协作图不变

我们应尽可能遵循“最小知识法则”,在保证职责合理分配的前提下,产生协作的限界上下文越少越好

领域模型产生的依赖

我们可以使用延迟加载来判断领域对象是否合理
不好的设计示例:在客户领域对象中添加了订单属性,正确姿势应该通过调用订单上下文的服务来获取

public class Customer { public List<SaleOrder> saleOrders() { // ... } }

在跨限界上下文消费领域模型的场景有二种解决方式(订单查询商品信息):

  • 第一种:采用遵奉者模式,重用(订单重用商品的领域模型)
  • 第二种:定义符合自己的领域模型,分离(订单定义自己和商品有关的领域模型)

在两个不同的限界上下文中为相同或相似的领域概念分别建立独立的领域模型为常见做法

销售和售后对商品的关注点其实是不同的,销售可能需要了解客户的性别、年龄与职业,以便于他更好地制定推销策略,售后支持则不必关心这些信息,只需要客户的住址与联系方式

推荐采用分离的方式进行领域模型设计,但需要注意以下几点:

  • 数据可以仍然存(持久化)在一处,领域模型仅仅是内存中的对象
  • 数据按照不同的业务边界分散存储,但它们之间用相同的 Identity 来保持关联
  • 数据虽然出现了冗余,但是导致它们产生变化的原因却不相同
数据产生的依赖

倘若严格遵循领域驱动设计,通常不会产生这种数据库层面的依赖
出于性能或其他原因的考虑,CQRS 模式就存在一个限界上下文去访问属于另外一个限界上下文边界的数据
没有绝对的理由,我们不要轻易做出这种数据依赖的妥协
数据分析用的数据库应该和业务数据库分离,通过对业务数据库的采集提供给数据分析使用

未经允许不得转载:菡萏如佳人 » DDD领域驱动战略篇(3)

欢迎加入极客江湖

进入江湖关于作者