限界上下文依赖: 共享内核与防腐层怎么选择?

297 阅读8分钟

在软件开发中依赖的处理是很大的难题。甚至很大程度上影响后续系统的可维护性。依赖不可避免,小到类与类之间的依赖,大到服务与服务之间的依赖。处理的方式也有很多种,处理依赖也是一门艺术,需要权衡利弊。本文将分析上下文依赖常见的2种方式的优缺点和使用场景。

共享内核和防腐层在 DDD 中这两种是最常见的处理限界上下文依赖的方式。概念和 DDD 相关,意味着还融合了 DDD 的其他概念。如果你是这样理解的:共享内核就是模块间进行直接依赖,防腐层就是通过抽象层来对另一个模块进行封装从而进行依赖。那证明对这两种依赖关系的处理还处于非常浅的认识。

限界上下文

我们常常会把相同类型的功能放到一个模块里面,例如对商品的管理,会放到商品模块中。这就是商品上下文(product)。对订单的管理,包括用户下单等会放到订单模块中,也就是订单上下文(order)。订单上下文会依赖商品上下文,就可以选择共享内核或者防腐层。这里并不是说微服务的拆分,微服务拆分和限界上下文的拆分是2个概念,他们可以有关联也可以没关联。每一个上下文可以用 maven 的 module 进行承接。

共享内核

从实现上看,共享内核就是直接依赖另一个上下文。本质上看是一个A上下文依赖B上下文的领域模型。

image.png

假设 dj-shopping 有 2个上下文,分别是 order 和 product, 订单需要获取商品信息。

image.png

 订单上下文直接依赖商品上下文
        <dependency>
            <groupId>com.test</groupId>
            <artifactId>product</artifactId>
            <version>1.0</version>
        </dependency>

例如在订单模块里面可以直接获取商品上下文的仓储,并通过仓储获取到聚合。这种实现方式两个上下文耦合比较大,形成单向依赖。

优点: 使用方便,订单上下文如果需要商品信息,直接根据商品仓储获取到商品聚合即可。商品上下文一般都提供了比较完善的能力,订单上下文直接复用即可。

productRepository.getById(String productId)

缺点:

  • 依赖范围不可控:整个上下文依赖可能会导致依赖的范围会变大,例如若订单只关心商品的名称,价格,并不关心商品的重量,体积。但因为依赖了整个聚合,因此也间接依赖了不需要的领域对象。
  • 难以拓展:会把 B上下文的实现也依赖了,导致难以拓展。例如订单若需要支持对接其他商品系统,则非常难。
  • 拆分成本高:耦合大意味着后续要拆的成本会变大,无法快速往更高的维度-服务级别。例如订单上下文流量比商品流量要大,订单上下文希望做的更加弹性,则需要把订单上下文作为单独一个服务拆分出来,这时候依赖散落在不同地方,导致难以拆分。
  • 影响范围大:A上下文依赖B上下文也会导致 B上下文的改动会直接影响到 A 上下文,例如 B上下文商品的属性发生变化,例如从商品的价格从只支持人民币的整数型变成支持多币种的值对象,则无论如何也会对订单上下文产生影响。

防腐层

防腐层本质上是一个上下文通过依赖倒置的方式让别的上下文依赖自己,从而使自己变的更加稳定。防止其他上下文的修改影响到自己。从实现上看,防腐层是调用方提取了接口,由提供方实现。

image.png

这时如果订单上下文需要获取商品信息,则重新定义接口和对象。前面提到了依赖倒置是希望商品依赖订单上下文,因此订单上下文需要提取接口,商品上下文再去依赖实现。

image.png

但这样其实是爽了订单上下文,却苦了商品上下文。商品上下文若还为其他上下文服务,则需要实现大量的逻辑,而且被迫依赖了很多上下文,让自己变得很不稳定。因此既然大家都不想依赖对方,那就增加一层中间层。也就是增加 order-product 模块来解耦订单和商品上下文。至于 order-product 一般是给订单上下文的团队维护,不能因为自己想解耦就把解耦的层丢给下游上下文,所以就把实现归到给 order 上下文。

image.png

虽然我们常常把order-product 当成 order 的一个包,但要清楚知道 order 上下文不能依赖 order-product ,他们本质上属于不同的上下文。为了让边界更加清晰,我们还是单独成一个模块。

image.png

order-product 可以依赖 order 的 ProductAcl,依赖 product 的领域模型来实现。

image.png

特点:

  • 依赖范围可控,order 可以定义自己需要的 productInfo,而非完整的 Product,例如订单只关心商品名和价格,则只定义商品名和价格即可。订单只关心商品的人民币价格,则仍然可以用整数型来存储价格而非值对象。
  • 拓展方便,如果要对接其他的商品系统,则只需要修改防腐层实现,也就是 order-product 即可。
  • 后续拆分服务成本小。只需要把 order-product 里面对 product 的调用改成服务间调用即可,order 的上层业务完全不受影响。
  • 影响范围可控,例如 order 和 product 并不会直接相互依赖,而是多了一层 order-product 层,因此及时 product 修改了模型,只要 order 的接口不改,order-product 进行兼容即可。对 order 是无感的。

防腐层的重点在接口的设计,而不在实现。从 order上下文 的 productInfo 和 product 上下文 里面的 product 可以看出。order 上下文其实是自己定义了一个 "product"。

共享内核和防腐层对比

共享内核实际上是两个模型相互依赖。而防腐层则是新建了一个模型,让这个模型和三方模型进行一个依赖,这是一种非常弱的依赖关系,使得两个模型能相对独立。所以其实这里的防腐层也可以是看成一个仓储。productInfo 是 order 上下文的聚合,仓储就是获取聚合的地方,获取方式不仅包括常见的数据库,而且还包含远程服务,缓存等等。

image.png

防腐层看上去很美好,但增加一个模型意味着需要在两个模型之间进行转换,会增加开发成本,增加系统复杂度。试想一下如果 productInfo 和 product 的相似度非常高,会导致大量重复的代码。本来可以直接调用 product 上下文的方法和属性,现在需要多一层转换。并且 product 如果频繁修改,productInfo 如果是依赖这些修改,则也要频繁跟着修改。

从上面防腐层的缺点可以发现几个点决定了用共享内核还是防腐层。一开始可以先设计出该领域自己的模型,然后再对比另一个上下文的模型和自己所需模型的差异。

  1. 两个模型的差异是否足够大。例如如果 product 上下文同时为物流上下文,仓储上下文等等服务,属于支撑子领域,则意味着 product 的模型会比较复杂。和 order 里面所需的模型差异可能会比较大,则建议使用防腐层。如果 product 上下文只被 order 上下文依赖,则可能两个上下文的关系会比较紧密,推荐使用共享内核。
  2. 后续的拓展性。例如 product 在可预见的未来会支撑更多的上下文,则可以提前使用防腐层.
  3. 技术考虑。考虑到 product 的请求量会比较大,需要弹性伸缩,则可以考虑使用防腐层,方便后续进行服务的拆分。

共享内核和防腐层选择之后也不是一成不变的,一开始业务比较简单,可以先使用共享内核快速实现,后续再转换成防腐层也是可以的。

防腐层很容易理解错的点在于认为防腐层只是对其他上下文的接口进行简单的封装,导致其他上下文的领域知识入侵到自己的上下文,例如防腐层接口直接依赖其他上下文的 dto 或者聚合,方法命名几乎一致。看上去是防腐层,但实际上是共享内核。

总结

共享内核和防腐层的区别在于是直接依赖其他上下文模型,还是自建模型。如何选择取决于上下文的依赖程度。