Skip to content

如何保证领域模型与代码模型一致

至此,我们了解了如何用事件风暴来构建领域模型。在构建领域模型的过程中,我们会提取很多领域对象,比如聚合根、实体、值对象、命令和领域事件等。我们又根据DDD分层架构模型,建立了标准的微服务代码模型,为不同的代码对象定义了分层和目录结构。

但要想完成微服务的设计和落地,这之后其实还有一步,也是我们这一章的重点:将领域模型的领域对象映射到微服务代码模型中。

为什么这一步如此重要呢?因为这一步是从DDD战略设计向战术设计转换的关键步骤,也是设计微服务代码对象和建立代码对象依赖关系的非常关键的一步。

DDD强调先构建领域模型然后设计微服务,以保证领域模型和微服务设计的一体性,因此我们不能脱离领域模型来谈微服务设计和落地。这也是为什么我一直强调领域模型重要性的关键原因。

领域对象的整理

随着微服务拆分完毕,领域模型的边界和领域对象就基本确定了。

我们要做的第一项重要工作就是,整理领域建模过程中产生的领域对象,将这些领域对象和它们的业务行为记录到表格中。

一个领域模型会包含一到多个聚合。一个聚合会包含多个领域对象,每个领域对象都有自己的领域类型属性。领域类型属性主要有聚合根、实体、值对象、命令和领域事件等类型。

从领域模型到微服务落地

在构建领域模型时,我们往往是站在业务视角,重点关注业务场景和问题,不会过多考虑技术实现方案,有些领域对象还带着业务语言。所以我们还需要将领域模型作为微服务设计的输入,完成领域对象的设计和转换,让领域对象与代码对象建立映射关系,从而完成微服务的概要设计。

换句话说,从领域模型到微服务落地,我们还需要进一步的设计和分析。

领域建模时提取的领域对象,还需要经过进一步的用户故事或领域故事分析,完成微服务设计后,才能用于微服务开发。这个过程会比领域建模的过程更深入、更细致。

分析过程中我们主要关注以下内容:

▪分析微服务内有哪些服务?

▪服务所在的分层?

▪应用服务由哪些服务组合和编排完成?

▪领域服务包括哪些实体的业务逻辑?

▪采用充血模型的实体有哪些属性和方法?

▪有哪些值对象?

▪哪个实体是聚合根等?

最后梳理出所有的领域对象和它们之间的依赖关系。我们会给每个领域对象设计对应的代码对象,定义它们所在的软件包和代码目录。

微服务设计过程建议参与的角色有DDD专家、架构师、设计人员和开发经理等。

领域层的领域对象

事件风暴结束时,领域模型的聚合内一般会有聚合根、实体、值对象、命令和领域事件等领域对象。完成领域故事分析和微服务设计后,微服务的聚合内一般会有聚合根、实体、值对象、领域事件、领域服务、工厂和仓储、持久化对象等领域对象。这里的领域对象是一个广义的概念,它包括领域模型中的所有对象,而不仅仅是指实体等领域对象。

下面我们就来看一下这些领域对象是怎么分析得来的。

  1. 设计聚合根

聚合根来源于领域模型,我们需要找出领域模型内与聚合根关联的所有实体和值对象。在个人客户聚合里,个人客户实体是聚合根,它可以关联并负责管理聚合内的地址、联系电话以及银行账号等实体的生命周期。

聚合根是一种特殊的实体,我们需要设计它的属性和方法。客户聚合根类有自己的实现方法,比如生成客户编码,新增和修改客户信息等方法。同时它也可以管理聚合内实体和值对象等领域对象的生命周期。聚合根可以引用聚合内的所有实体,也可以实现聚合之间的基于聚合根ID的引用。

聚合根类放在领域层聚合的entity目录结构下。

  1. 设计实体

在DDD分层架构里,实体类采用充血模型,在实体类内实现实体的全部业务逻辑。这些实体有自己的业务属性、方法和业务行为。

我们需要分析并设计出这些实体的属性、关联的实体和值对象以及业务行为对应的方法。比如地址实体有新增和修改地址的方法,银行账号实体有新增和修改银行账号的方法。

另外,实体还需要完成持久化操作,所以我们还可以建立实体与持久化对象的关系。大多数情况下,领域模型的实体对象与数据库持久化对象是一一对应的。但领域模型的某些实体在微服务设计时,可能会被设计为一个或多个数据持久化实体,或者实体的某些属性会被设计为值对象。

还有些领域对象在领域建模时不太容易被我们发现,所以在微服务设计时,我们需要根据更详细的需求将其识别和设计出来。

实体类代码对象放在领域层聚合的entity目录结构下。

  1. 设计值对象

一般,在用事件风暴构建领域模型时,我们不需要严格区分DO对象是实体还是值对象。但是在从领域模型映射到代码模型以完成微服务设计时,我们需要根据具体的业务场景将它们区分为实体和值对象,将某些属性或属性集设计为值对象。

有些领域对象既可以设计为值对象,也可以设计为实体。我们需要根据具体情况进行分析。如果这个领域对象在其他聚合内进行生命周期管理,并且引用它的实体对象只允许对它整体替换,我们就可以将它设计为值对象。如果这个领域对象有多条数据记录且需要基于它进行频繁的查询统计,则建议将它设计为实体。

在个人客户聚合中,客户拥有客户证件类型,它以枚举值的形式存在。一般我们可以将枚举值类型的属性设计为值对象。

值对象类放在领域层聚合的entity目录结构下。如果值对象比较多,你也可以在entity目录下再增加一个值对象代码目录结构。

  1. 设计领域事件

如果领域模型中领域事件会触发下一步业务操作,那么我们就需要设计领域事件了。

首先确定领域事件是发生在微服务内还是微服务之间,判断是否需要引入事件总线或消息中间件。

然后设计事件实体对象、事件的发布和订阅机制,以及事件的处理机制。

在个人客户聚合中有客户已创建的领域事件,因此就有客户已创建事件这个实体。

领域事件实体类放在领域层聚合的event目录结构下。领域事件的订阅建议放在应用层的event目录结构下。领域事件发布相关代码放在领域层或者应用层都是可以的。

  1. 设计领域服务

如果领域模型里面的一个业务动作或行为需要多个实体协同完成,我们就需要设计领域服务。

领域服务通过对多个实体和实体方法进行组合和编排,完成多个实体组合的核心业务逻辑。你也可以认为领域服务是位于实体方法之上和应用服务之下的一层业务逻辑。

按照严格分层架构层的依赖关系,如果实体的方法需要暴露给应用层,它需要封装成领域服务后才可以被应用服务调用。所以如果实体方法需要被前端应用调用,我们需要将它封装成领域服务,然后再封装为应用服务。

个人客户聚合根创建个人客户信息的方法,会被封装为创建个人客户信息领域服务,然后再被封装为创建个人客户信息应用服务,最后会被封装成facade接口发布到API网关,向前端应用暴露。

跨多实体的业务逻辑在聚合根方法和领域服务中都可以实现。建议你将这类业务逻辑尽量放在领域服务中实现,避免聚合根内的业务逻辑过于庞杂。

一个聚合可以建立一个领域服务类,你可以将聚合中所有的领域服务都在这个领域服务类中实现。

领域服务类放在领域层聚合的service目录结构下。

  1. 设计工厂和仓储

一个聚合只有一个仓储。仓储包括仓储接口和仓储实现,通过依赖倒置原则实现应用业务逻辑与数据库资源逻辑的解耦。

个人客户聚合可以通过工厂和仓储模式两者组合,完成聚合内实体和值对象等DO对象的构建、数据初始化和持久化。

工厂类(factory)放在领域层聚合的service目录结构下。仓储相关代码放在领域层聚合的repository目录结构下。

  1. 设计持久化对象

持久化对象PO主要完成DO对象的数据库持久化操作,PO一般与数据库表是一对一的关系。持久化对象设计过程的本质就是完成从领域模型到数据模型的设计过程。

[大多数情况下实体对象与PO是一对一的关系,但为了简化数据库设计,减少数据库表的数量,值对象往往以属性嵌入方式或序列化大对象方式嵌入实体表中。]

因此在持久化对象PO设计时,我们需要考虑实体或值对象等DO对象与PO对象的映射关系。在持久化之前,我们采用工厂模式完成从DO对象到PO对象的转换,然后采用仓储模式完成DO对象的持久化操作。

持久化对象PO相关代码放在领域层聚合的repository目录结构下。

应用层的领域对象

应用层主要有应用服务和领域事件的发布和订阅。

在事件风暴或领域故事分析时,我们往往会根据外部用户或系统发起的命令,来设计服务或实体方法。为了响应这个命令,我们需要分析和记录以下内容。

▪在应用层和领域层分别会发生哪些业务行为?

▪各层分别需要设计哪些服务或者方法?

▪这些业务行为需要哪些聚合协同,需要哪些领域服务?

▪这些方法和服务所在的分层以及领域类型(比如实体方法、领域服务和应用服务等)是什么,它们之间的调用和组合的依赖关系是什么?

在严格分层架构模式下,不允许服务的跨层调用,每个服务只能调用它紧邻的下一层服务。服务从下到上依次为:实体方法、领域服务、应用服务和facade接口。

如果需要实现服务的跨层调用,应该怎么处理?建议采用服务逐层封装的方式。服务封装主要有以下几种方式。

  1. 实体方法的封装

实体的方法是最底层的实体的原子业务逻辑,它体现的是实体的业务行为。

在采用严格分层架构时,如果实体方法需要被应用服务调用,你可以将它封装成领域服务。这样领域服务就可以被应用服务组合和编排了。如果它还需要被用户接口层调用,你还需要将这个领域服务封装成应用服务。

经过逐层服务的封装,实体方法就可以暴露给上面不同的层,实现跨层调用了。

  1. 领域服务的组合和封装

领域服务主要完成对多个实体和实体方法的组合和编排,供应用服务调用。

如果领域服务需要暴露给用户接口层,领域服务就需要封装成应用服务。

  1. 应用服务的组合和编排

应用服务会对多个领域服务进行组合和编排,在用户接口层完成服务和数据封装后,就可以发布到API网关,供前端应用调用。

在应用服务组合和编排时,你需要关注一个现象:多个应用服务可能会对多个同样的领域服务重复进行同样业务逻辑的组合和编排。当出现这种情况时,你就需要分析这些领域服务是不是应该进行沉淀和演进了。

此时,你可以将这几个不断被重复组合的领域服务在领域层组合成新的领域服务。这样既省去了应用服务的反复编排,也实现了领域服务的演进。领域模型也会变得越来越精炼,更能适应业务的要求。

应用服务类放在应用层service目录结构下。领域事件的订阅处理逻辑放在应用层event目录结构下。

关于服务类的命名,你可以参考以下规则。如果为一个聚合设计一个服务类,那么服务前面的名称就可以与聚合名保持一致,然后你可以用*DomainService或*AppService作为后缀,来区分它们是领域服务还是应用服务。比如,对于Person聚合,用PersonDomainService命名领域服务类,用PersonAppService命名应用服务类。

领域对象与代码对象的映射

在完成微服务各层领域对象的分析和设计后,我们就可以建立领域对象与微服务代码对象的映射关系了。

  1. 富领域模型

在个人客户领域模型中有个人客户聚合,聚合内有多个实体、值对象以及它的聚合根,我们可以很容易地建立聚合根与实体和值对象的依赖关系。这种领域模型是富领域模型。

我们在对个人客户聚合进一步分析后,找到了个人客户这个聚合根,设计了客户类型值对象,以及电话、地址、银行账号等实体,为实体方法和服务做了封装和分层,建立了领域对象的关联和依赖关系,完成了仓储服务等设计。注意,最关键的是,这个过程我们建立了领域对象与微服务代码对象的映射关系。

▪[层]:定义领域对象位于分层架构中的哪一层,比如用户接口层、应用层、领域层和基础层等。

▪[领域对象]:领域模型中领域对象的具体名称。

▪[领域类型]:根据DDD知识体系定义的领域对象的属性或类型,如限界上下文、聚合、聚合根、实体、值对象、领域事件、方法、应用服务、领域服务和仓储服务等。

▪[包名]:代码模型中的包名,对应代码对象所在的代码目录。

▪[类名]:代码模型中的类名,对应代码对象的类名。

▪[方法名]:代码模型中的方法名,对应代码对象的方法名。

另外,我们还可以建立这些对象的依赖关系或在不同分层的服务之间的调用依赖关系等。

在建立这种领域对象与代码对象的映射关系后,我们就可以得到领域对象在微服务中的代码结构.

  1. 贫领域模型

有些业务场景可能并不能如你所愿,这类业务有多个实体,实体之间相互独立、互不依赖,是一种松耦合的关系,它们主要参与分析或者计算,你找不出聚合根。这种领域模型是贫领域模型。

就业务本身来说它们是高内聚的,它们所组合的业务能力与其他聚合在一个限界上下文内,你也不大可能将它单独设计为一个微服务。这种业务场景其实很常见。比如,在个人客户领域模型内有客户归并的功能,它扫描所有客户数据,按照身份证号码、电话号码等是否重复的业务规则,判断是否是重复的客户,然后对重复的客户进行归并。在这种业务场景你就找不到聚合根!

那对于这类贫领域模型的场景,应该如何处理呢?

我们仍然可以借鉴聚合的设计思想,用聚合来定义这部分功能,并采用与富领域模型同样的分析方法,建立实体的属性和方法,对方法和服务进行封装和分层设计,设计仓储,建立领域对象之间的依赖关系。

唯一可惜的就是我们找不到聚合根。不过也没关系,除了聚合根管理功能外,我们仍然可以用DDD的其他设计方法。

小结

从领域模型到微服务设计,是微服务落地过程中非常关键的一步。这个过程也是建立业务和技术关联的关键设计过程。

你需要从微服务代码模型的角度,对领域模型做更深入、更细致的分析,为领域对象分层,找出各个领域对象的依赖关系,建立领域对象与微服务代码对象的映射关系,保证领域模型与代码模型的一致性,最终完成微服务设计。

在建立这种业务模型与微服务系统架构的关系后,整个项目团队就可以在统一的通用语言下工作,按照统一的代码规范完成微服务开发。即使是不熟悉业务的开发人员,或者不熟悉代码的业务人员,也能很快找到业务逻辑的代码目录位置。

Released under the MIT License.