3.3.5 问题域和限界上下文
前面我们通过用户下单这个场景讲解了领域驱动设计的建模、分析与设计的过程。然而,站在更大的电商网站的角度来看,用户下单只是其中一个很小的场景。那么,如果要对整个电商网站进行领域驱动设计,应当怎么做呢?它包含那么多场景,每个场景都要包含那么多的领域对象,就会形成很多的领域对象,并且每个领域对象之间还有那么多复杂的关联关系。这时候,怎样通过领域驱动来设计这个系统呢?怎么去绘制领域模型呢?是绘制一张密密麻麻的大图,还是绘制成一张一张的小图呢?
假如将整个系统中那么多的场景,涉及的那么多领域对象,全部绘制在一张大图上,可以想象这张大图将绘制出密密麻麻的领域对象,它们之间有着纷繁复杂的对象间关系。绘制这样的图,绘制的人和看图的人都非常费劲。这样的图就不利用我们厘清思路、交流思想、提高设计质量。因此,正确的做法就是将整个系统划分成许多相对独立的业务场景,在一个一个的业务场景中进行领域分析与建模,我们称这样的业务场景为“问题子域”,简称“子域”。
领域驱动核心的设计思想就是将对软件的分析与设计还原到真实世界中,首先去分析和理解真实世界的业务与问题。因此,把真实世界的业务与问题叫做“问题域”,这里面的业务规则与知识叫“业务领域知识”。如电商网站的问题域是人们如何进行在线购物,购物的流程是怎样的;在线订餐系统的问题域是人们如何在线订餐,饭店如何在线接单,系统又如何派送骑士去配送。
然而,不论是电商网站还是在线购物系统,都有一个非常庞大而复杂的问题域。要一次性分析清楚这个问题域对我们是有难度的,因此我们采用“分而治之”的策略,将这个问题域划分成许多个问题子域。比如,电商网站包含了用户选购、下单、支付、物流等多个子域,而在线订餐系统包含了用户下单、饭店接单、骑士派送等子域。如果某个子域比较复杂,在子域的基础上还可以进一步划分子域。
因此,对一个复杂系统的领域驱动设计,就是以子域为中心进行领域建模,绘制出一张一张的领域模型,然后以此为基础指导程序设计。这一张一张的领域模型,称为“限界上下文”(Context Bound,CB)。
DDD中限界上下文的设计,很好地体现了高质量软件设计中“单一职责原则”的要求,即每个限界上下文中实现的业务都是因同一个原因而变更的软件的功能。比如,“用户下单”这个限界上下文都是实现用户下单的相关业务。这样,当“用户下单”的相关业务发生变更的时候,只与“用户下单”这个限界上下文有关,只需要对它进行修改就行了,与其他限界上下文无关。这样,需求变更的代码修改范围缩小了,维护成本就降低了。
但是,在用户下单的过程中,对用户信息的读取是否也应该在“用户下单”这个限界上下文实现呢?答案是否定的,即读取用户信息不是用户下单的职责,当用户下单业务发生变更的时候,用户信息不一定变;当用户信息变更的时候,用户下单也不一定变;它们是软件变化的两个原因。因此,应当将读取用户信息的操作交给“用户信息管理”限界上下文,“用户下单”限界上下文只是对它的接口进行调用。这样的划分实现了限界上下文内的高内聚和限界上下文间的低耦合,可以很好地降低日后代码变更的成本,提高软件设计质量。而限界上下文之间的这种相互关系,我们称之为“上下文地图”(Context Map)。
所谓“限界上下文内的高内聚”,就是每个限界上下文内实现的功能都是因同一个原因而变更软件的代码,因为这个原因的变化才需要修改这个限界上下文,除此之外的修改与它无关。正是因为限界上下文有如此好的特性,才使得现在很多微服务团队用限界上下文作为微服务拆分的原则。每个限界上下文对应一个微服务,按照这样的原则拆分出来的微服务系统在之后变更维护时,可以很好地将每次的需求变更快速落到某个微服务中变更。这样,变更这个微服务就实现了该需求,升级该服务就可以交付用户使用了。这样的设计使得越来越多大规模开发团队今后可以实现低成本维护与快速交付,进而快速适应市场变化,提升企业竞争力。
所谓“限界上下文间的低耦合”,就是限界上下文通过上下文地图相互调用时是通过接口进行调用,如图3-19所示。图中,模块A需要调用模块B,那么它就与模块B形成了一种耦合。这时,如果需要复用模块A,所有有模块A的地方都必须有模块B,否则模块A就会报错。如果模块B还要依赖模块C,模块C还要依赖模块D,那么所有使用模块A的地方都必须有模块B、C、D,使用模块A的成本就会非常高昂。然而,如果模块A不依赖模块B,而是依赖接口B′,那么所有需要模块A的地方就不一定需要模块B。如果模块F实现了接口B′,那么模块A调用模块F就可以了。这样,调用方和被掉方的耦合就被解开了。
图3-19 限界上下文之间的低耦合
在实现代码时,可以通过微服务来实现“限界上下文间的低耦合”。比如,“下单”微服务要去调用“支付”微服务。在设计时,首先在“下单”微服务中增加一个“支付”接口,这样在“下单”微服务中所有对支付的调用就变成了对该接口的调用。接着,在其他“支付”微服务中实现支付。比如,现在设计了A、B两个“支付”微服务,在系统运行时配置的是A服务,那么“下单”微服务调用的就是A;如果配置的是B服务,调用的就是B。这样,“下单”微服务与“支付”微服务之间的耦合就被解开了,使得系统可以通过修改配置去应对各种不同的用户环境与需求。
限界上下文的设计使系统在应对复杂应用时更轻松,设计质量得到了提高,变更成本得以降低。过去,每个模块在读取用户信息时都是直接读取数据库中的用户信息表,一旦用户信息表发生变更,各个模块都要变更,变更成本就会越来越高。现在,采用领域驱动设计,读取用户信息的职责交给了“用户管理”限界上下文,其他模块都是调用它的接口。当用户信息表发生变更时,只与“用户管理”限界上下文有关,与其他模块无关,变更维护成本就降低了。通过限界上下文将整个系统按照逻辑进行了划分,但从物理上它们都还是一个项目,运行在一个JVM中。这种限界上下文只是“逻辑边界”。
今后,将单体应用转型成微服务架构以后,各个限界上下文都是运行在各自不同的微服务中,是不同的项目、不同的JVM。不仅如此,在进行微服务拆分的同时,数据库也进行了拆分,每个微服务使用不同的数据库。这样,当各个微服务要访问用户信息时,它们没有访问用户数据库的权限,就只能通过远程接口去调用“用户”微服务开放的相关接口。这时,这种限界上下文就真正变成了“物理边界”。限界上下文的架构变迁过程如图3-20所示。
图3-20 限界上下文的架构变迁过程