DDD 实践手册(6. Bounded Context - 限界上下文)

DDD 实践手册(6. Bounded Context - 限界上下文)

之前的几篇文章中更多谈及的是有关具体代码层面的设计与实现,这在 DDD 中更多的被成为是「战术设计」,而接下来的几篇文章我会谈及 DDD 中设计的另一部分概念,更加偏向上层的「战略设计」。本篇会介绍「战略设计」的核心概念,Bounded Context,称之为限界上下文(之后简称为 BC),以及在项目中如何实现。

什么是 BC?

如果你之前读过系列文章的第二篇,DDD 的分层设计,那么会发现其中已经谈及了 BC 的概念。这里会更加详细的解释一下。

设想一下,当一个业务系统逐渐复杂,愈发庞大之时,系统代码不仅应该在架构层次进行划分,以区分不同抽象层次以及功能。我们更应该从业务的角度出发,对系统进行某种维度的拆分。这种维度可能是按照不同的业务功能,也有可能是按照业务流程的不同阶段,根据系统的具体业务不一而同。

我们依然拿保险业务来作为参考。一般通用的业务按照不同的职能可以分为: 新契约(负责承保,可以简单的理解订立保险契约),核保,保全,理赔,续期,产品,渠道,收付费等。在最初的设计中,这些都可以对应到一个独立的 BC。

看到这里你应该已经明白 BC 是什么了,它其实就是系统内部按照不同业务目的进行划分的「模块」。这里需要注意的是 BC 的划分更多是基于领域知识,它与技术的分层应该是正交的,两者之间互不影响。

既然你已经了解了什么是 BC,接下来就需要明确以下两个问题:

  1. 如何设计 BC 的范围?
  2. 如何实现 BC?

如何划分 BC?

这个问题其实很难回答,在我经历的项目中并没有一个非常成型的分析方法来帮助架构师分析找到系统中的不同 BC。造成这种情况的原因我觉得有以下几条。其一,我参与的都是金融行业的项目,而金融行业大部分的业务逻辑或是业务流程都是非常规范化的,因此不同客户之间的差异非常小,所以在划分 BC 时有大量的经验可以借鉴,除了在一些细节上需要按照客户的需求做一些变化,其他基本不需要做任何的改变。

其二就是软件架构往往受到「康威定理」的影响,即你的软件架构往往与组织部门的架构相一致。这其实很好理解,在许多系统中你会发现功能模块的划分是按照使用组织的部分而划分的。

综合以上两点,在很多项目中没有花必要的时间对领域知识进行深入的研究,而是通过一些不是那么合适的方法得到了一个所谓的「限界上下文」。上述两种方法最大的问题是对于领域知识没有进一步的分析,混淆了 Operational Level(操作层)Knowledge Level(知识层) 的模型。在设计上只是将业务流程照搬到代码层面,而忽略了对于业务规则的模型与抽象。这也是为什么许多业务系统在需求不断增加之后无法有效的控制复杂度,最终导致整个系统代码的腐化,难以维护。

要解决这个问题,需要架构师有着丰富的行业知识或者需要一个有些系统分析经验的业务分析人员。因为 BC 的划分不仅需要系统分析与架构的知识,行业或者说是领域知识是同样重要的。但是如果你的团队中没有这样的人选,或者说你进入了一个完全陌生的行业,又该怎么做呢?此时一种可行的方法就是参考敏捷的实践,先划分一个粗略的 BC 模型,然后在每个迭代中细化,不断的明确每个领域对象的职责,提炼业务规则背后的模型。通过 code review 和迭代后的会议分析现有 BC 的合理性并加以修改。而这也需要类似 CI,单元测试等其他敏捷实践的支持,才能保证模型的不断演进。

BC 的实现

在有了 BC 之后,如何在我们的代码中实现呢?同样的这也有几种不同的选择。先回顾一下我在第二篇文章中的图片,可以在图片中看到 BC 的结构:

如果你当前的项目是一个单体应用的话,通过 Java 语言提供的 package 机制是最简单的一种方法。如上图所示,在 domain package 下按照不同的 BC 划分不同的下层 package,在对应 bc 的package 下进行代码的编写。这种解决方案的优势在于简单易行,对于开发人员也非常容易理解,无非就是按照架构设计将不同的业务代码放到不同的业务模块之下。另一方面它的缺点也很明显,就是不同 BC 直接缺乏必要的隔离。

很容易想到不同 BC 之间是可能会发生交互的,举一个例子,无论是新契约,还是理赔,抑或是保全模块,都有可能产生财务费用,例如需要客户缴纳额外的费用,或是需要退费给客户。也由此需要与收付费的 BC 进行交互,调用相关的服务。针对这样的需求 DDD 同样提供了数个模式,而我比较建议且使用最对的还是 Anticorruption Layer(防腐层) 模式。「防腐层」对外提供了基于 Facade 模式的粗粒度接口,并通过 Adapter 将输入的数据适配为 BC 内部服务所需的数据对象。具体可参考下面的图片:

在实际项目中仍然需要防止开发人员绕过「防腐层」直接调用另一个 BC 的代码。拿 Java 举例,在 Java9 之前,Java 在语言层面上提供的可见性封装无法限制开发人员的代码,因此需要额外的,类似 ArchUnit 这样的工具进行静态检查。如果项目使用的是 Java9 之后的版本,则可以通过 JigSaw 提供的 Module 特性进行进一步的封装。具体的方式就是将每个 BC 下的代码作为一个独立的 Jar 包进行构建,并在 module-info.java 中配置 BC 对外访问的可见性。

BC 与微服务

单体应用的问题在于缺乏有效的架构解耦手段控制日趋增长的系统复杂度。可以预见的是当系统越来越庞大,BC 也越来越多,单个 BC 也越来越复杂时,系统的可维护性也会变得越来越差。此时可以考虑的另一种实现 BC 的手段就是将单体应用拆分为多个微服务。按照 BC 划分微服务的边界显的顺理成章,能够非常清晰的划分出每个微服务的功能边界。而在每个微服务的内部,可以根据 BC 功能的复杂程度再次拆分不同的 Sub BC。

同时在微服务的架构下可以将「防腐层」的功能放到 API Gateway 上,彻底的将一些数据转换和适配的工作从业务系统中剥离出去,具体的架构可以参考下图:

微服务本身也是一个比较大的话题,之后会有专门的文章讨论。

不同 BC 中的领域对象

最后在设计 BC 时需要注意的是区分具有相同名称的领域对象在不同 BC 中的实现。思考一下,在「新契约」与「理赔」这两个不同的 BC 中,「保单」无疑都是核心的领域对象,但是在上述两个不同的 BC 中的侧重点,业务逻辑,数据属性都不同,因此可能领域对象的名称,或是对应 Java Class 的名称都成为 Policy,但这是两个截然不同的领域对象。

面对这种问题,一方面需要通过「统一语言」这一模式,在团队内部(不仅仅是研发团队,还应该包括业务分析团队和使用系统的业务部门)统一对于这两个不同领域对象的理解,避免歧义。而另一方面,更好的做法是给这两个领域对象各自起一个更为恰当的名称,例如 IssuedPolicyClaimPolicy,帮助团队加深理解。

小结

这次我们讨论了 DDD 中战略设计的核心概念 Bounded Context,以及如何在代码层面实现这一设计。在下一篇中我们会讨论 DDD 中其他一些常用的模式以及设计技巧,希望你不会错过。

欢迎关注我的微信号「且把金针度与人」,获取更多高质量文章

发布于 2020-03-02 09:51