DDD你真的理解清楚了吗(4)限界上下文_复杂度

限界上下文该如何划分

这些年,我认为DDD走到了一个死胡同里了,总让人感觉晦涩难懂、不易落地。晦涩难懂,是因为DDD中有太多的概念说不清、道不明,使大家在使用DDD的时候感觉非常困惑,不知道该怎么设计,这样设计到底有什么好处;不易落地,是因为DDD落地编码时,开发成本过高,需要编写的代码过多,在团队中落地就非常困难。正因为如此,我期望用我这些年的认识,通过一系列的文章,将DDD晦涩的概念都讲明白了,将DDD的落地编码大刀阔斧地简化。通过这些讲解,让业务梳理更加清晰,设计编码也更加容易。

RT,今天要探讨的主题是“限界上下文该如何划分?”前面我们谈到,DDD的核心思想是将软件设计与真实世界对应,但真实世界又是十分复杂的,事物与事物间有着纷繁复杂的联系,就会在领域模型中形成密密麻麻的网状结构,增加软件的复杂度。在绘制领域模型的时候,是密密麻麻地绘制成一张大图呢?还是以业务场景为中心,绘制成无数个小图呢?显然应当是后者。

DDD将一个系统非常复杂的业务,通过“分而治之”的思想划分为多个限界上下文。每个限界上下文就是一个业务场景,有一个非常明确的主题,所有领域对象的绘制都是围绕着这个主题进行的。这样的方式,一方面让领域模型更加易于理解,另一个方面又让每个领域模型的复杂度降低。

譬如,在一个电子商务系统中,按照业务划分为用户、产品、订单与库存几个上下文。用户上下文的领域模型都是围绕着用户展开的,有用户的地址、账户、会员;产品上下文围绕着产品展开,有产品的品牌、分类、供应商;订单上下文围绕着订单展开的,有订单的用户、地址、订单明细和产品。上下文之间可能会有相同的对象,但每个上下文的主题是不同的。

限界上下文的划分,对于我们梳理纷繁复杂的业务,设计大规模的系统,具有革命性意义。过去,为什么系统随着时间的推移越来越难于维护,是因为随着时间的推移,系统业务会越来越复杂。每个维护系统的开发人员都必须要了解所有的业务,并且各种复杂的业务相互交织在一起,使得每次变更到底要变更哪些代码,识别起来越来越困难,风险也越来越高,因为业务的复杂程度已经超越了每个人的认知上限。

通过上下文的划分,将整个系统的问题空间划分为多个子域。这时,每个人对业务的掌握仅仅限于自己的上下文范围内,与其它上下文无关,我们的维护成本就降低了。譬如我现在的职责是维护订单上下文,我只需要梳理订单上下文的业务。但在下单的过程中,需要“支付”,那是支付上下文的业务;需要“库存扣减”,那是库存上下文的业务。我不需要掌握其它上下文的业务,只需要知道如何调用它们的方法与接口就可以了,我的学习和维护成本就降低了。

然而,限界上下文该如何划分呢?这里我给大家三个重要的原则:

1)业务的相关度

2)业务的复杂度

3)主题域/支撑域

业务的相关度,就是不同的业务与业务之间的联系是否紧密。比如,用户和它的地址联系就比较紧密,而用户与会员的紧密度就要差一些;订单与它的明细联系紧密,而订单与它的支付的联系就要差一些。业务的相关度决定了谁和谁应当划分在一起。

DDD你真的理解清楚了吗(4)限界上下文_复杂度_02

相反,业务的复杂度决定了谁和谁应当分开。许多的业务系统,起初的业务是比较简单的,但随着需求的不断变更,业务会越来越复杂,代码越来越多。这时,就应当适时地重构系统,将越来越复杂的业务划分成不同的上下文,进而划分为不同的微服务。譬如,用户与会员的联系没有那么紧密,它们是分开还是合并呢?一切要看当前的业务复杂度。比如在系统开发的初期,会员的业务比较简单,就划分到用户上下文中,这样的设计没有问题。但随着日后的变更,会员业务变得越来越复杂,就应当通过重构,将会员单独形成一个上下文,进而单独拆分出一个会员微服务。同样,在用户下单的时候会有折扣,但在业务初期,折扣的业务比较简单,因此直接将折扣写在订单上下文中。随着日后的变更,折扣业务变得越来越复杂,就将折扣单独拆分成一个上下文,因而单独形成微服务。

DDD你真的理解清楚了吗(4)限界上下文_领域模型_03

总之,业务相关度与复杂度形成了一对矛盾,我们就会在这对矛盾中进行权衡,在不同阶段有不同的设计,保持一种动态平衡。最终的效果就是,每个限界上下文都不会过于复杂,也不会太简单,保持一种平衡。

主题域/支撑域,是另外一种上下文划分的原则。每个业务系统要解决的真实世界中的业务问题,这个叫“问题空间”。然而,在一个复杂的业务系统中,“问题空间”非常庞大,因此就划分为多个子域“分而治之”。子域是我们对真实世界某个部分的理解,而限界上下文是我们对这个子域的设计方案。

子域又被分为3种不同的类型:主题域、支撑域和通用域。主题域就是整个系统的核心业务,是整个系统对用户的价值所在。比如,电子商务系统的“订单”、“支付”、“物流”,都是这个系统的核心业务所在,都是主题域。主题域中的核心业务对象都是以领域事件的形式出现,都是某个人在某个时间完成的某种操作,都有一个时间字段,如“订单”中的“下单时间”、“支付”中的“支付时间”,等等。

DDD你真的理解清楚了吗(4)限界上下文_领域驱动设计_04

支撑域是支撑主题域的其它相关业务对象及其相互关系。比如,订单上下文、支付上下文与库存上下文都需要读取产品信息,那么产品对象应当属于它们中的哪个上下文呢?哪个都不属于,而是单独拎出一个产品上下文,为其它各个上下文所共享。这里的产品上下文就是支撑域,进而还会形成产品微服务。在订单微服务、支付微服务、库存微服务中虽然也有产品对象,但它是值对象,只存在于内存中,需要通过远程调用产品微服务才能获取产品的相关数据。

通过主题域与支撑域的区分,也能很好地划分上下文。主题域就是整个系统的核心业务,而支撑域是从主题域中提取出来的,为多个主题域所共享的那些基础数据和字典。譬如,产品从订单上下文中抽取出来,形成产品上下文,然后基于产品的分析,又会形成与产品相关的供应商、产品分类、品牌等更多基础数据,从而形成一个完整的上下文。

DDD你真的理解清楚了吗(4)限界上下文_领域模型_05

最后是通用域,它实现的是一些比较通用的功能。这种通用的功能,可能会在系统的多个功能中共享,甚至在多个子系统、多个产品线中共享。譬如支付功能,会在用户下单、账户充值、会员服务等多个模块中使用,甚至会在公司的多个产品线中使用。那么,就需要思考这个功能的设计方案,是外部采购通用的产品,还是单独拎出来放到业务中台中建设。

言而总之,DDD的设计思想是首先将一个非常复杂的业务系统划分成多个限界上下文,对每个上下文进行领域建模,形成多个领域模型。然后,在设计编码时,将限界上下文的划分直接形成微服务,进而形成数据库的划分。然而,这样的划分不是固定的,它会随着日后业务的变更不断调整,从而划分出更多的上下文,进而拆分出更多的微服务。程序的设计永远是和业务复杂程度相匹配的,从而让每个微服务既不会太复杂,也不会过于简单,保持一种动态平衡,进而让系统始终保持高质量的代码与低成本的维护,可持久地不断发展下去。有了这样的划分,剩下的问题就是考虑如何将每个领域模型落地到微服务的开发,这个问题我们后面再继续探讨。

(待续)