一文带你学习DDD,全是干货!

2022-12-29 00:00:00 领域 业务 对象 实体 聚合

系统讲解DDD的基础概念,结合具体的Demo讲解DDD落地场景。

前言

说一下我为什么突然想学习DDD,这个肯定不是为了装X,也不是为了以后好跳槽,虽然转到人事团队也快*3个月,由于之前一直做其它项目,所以现在才开始接触招聘相关的业务。因为招聘业务涉及的系统非常多,想借鉴领域驱动设计的思想,看后续如何对系统进行重构,这个就是我想学习DDD的主要原因。

既然要学习DDD,就需要去找学习资料,目前我主要的学习资料就2个,一个是极客时间欧创新老师的《DDD实战课》,还有一个就是掘金的系列博客,《DDD实战课》让我对基础概念有了非常清晰的认识,但是实战的内容感觉落地性不强;对于掘金的系列博客,里面的基础概念比较抽象,但是里面的DDD Demo,让我对DDD有了一个直观的认识,所以两者刚好可以互补。

学习DDD的时间不长,也就学习了一周多时间,后面公司还会开展DDD的系列学习课程,到时也会参加。还是按照之前的学习习惯,每学习一块内容,就通过文章将核心内容总结出来,一方面将学习的知识系统化,也相当于二次学习,另一方面也是为了便于后续好查询,同时也能给大家留一些资料,方便大家学习。

本文主要对学习内容进行总结,然后加入一些个人的理解,前面主要讲述DDD基础,后面再结合具体的Demo,讲解DDD的落地方式。

DDD简介

说到DDD,绕不开MVC,在MVC三层架构中,我们进行功能开发的之前,拿到需求,解读需求。往往先做的一步就是先设计表结构,在逐层设计上层dao,service,controller。对于产品或者用户的需求都做了一层自我理解的转化。

用户需求在被提出之后经过这么多层的转化后,特别是研发需求在数据库结构这一层转化后,将业务以主观臆断行为进行了转化。一旦业务边界划分模糊,考虑不全,大量的逻辑补充堆积到了代码层实现,变得越来越难维护。

DDD作用:

  • 消除信息不对称
  • 常规MVC三层架构中自底向上的设计方式做一个反转,以业务为主导,自顶向下的进行业务领域划分
  • 将大的业务需求进行拆分,分而治之

说到这里大家可能还是有点模糊DDD与常见的mvc架构的区别。这里以电商订单场景为例。假如我们现在要做一个电商订单下单的需求。涉及到用户选定商品,下订单,支付订单,对用户下单时的订单发货。

MVC架构里面,我们常见的做法是在分析好业务需求之后,就开始设计表结构了,订单表,支付表,商品表等等。然后编写业务逻辑。这是个版本的需求,功能迭代饿了,订单支付后我可以取消,下单的商品我们退换货,是不是又需要进行加表,紧跟着对于的实现逻辑也进行修改。功能不断迭代,代码就不断的层层往上叠。

DDD架构里面,我们先进行划分业务边界。这里面核心是订单。那么订单就是这个业务领域里面的聚合逻辑体现。支付,商品信息,地址等等都是围绕着订单实体。订单本身的属性决定之后,类似于地址只是一个属性的体现。当你将订单的领域模型构建好之后,后续的逻辑边界与仓储设计也就随之而来了。

为什么要用DDD

  • 面向对象设计,数据行为绑定,告别贫血模型
  • 降低复杂度,分而治之
  • 优先考虑领域模型,而不是切割数据和行为
  • 准确传达业务规则,业务优先
  • 代码即设计
  • 它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现业务和技术统一的架构演进
  • 领域知识共享,提升协助效率
  • 增加可维护性和可读性,延长软件生命周期
  • 中台化的基石

DDD术语介绍

学习DDD前,有很多基本的概念需要理解,我整理了一下,主要包括:领域、子域、核心域、通用域、支撑域、实体、值对象、聚合、聚合根、通用语言、限界上下文、事件风暴、领域事件、领域服务、应用服务、工厂、资源库。详见下面这幅图:

开始看这幅图,感觉可能是DDD的噱头,当初步学习一轮DDD后,才发现这幅图总结的真的是非常棒!其实这幅图把DDD划分了不同的层级,里层是值、属性、标识等,这个其实就是基本的数据单位,但是不能直接使用。然后是实体,这个其实就是把基础的数据进行封装,是可以直接使用的,在代码中就是封装好的一个个实体对象。之后就是领域层,这个就是按照业务划分为不同的领域,比如订单领域、商品领域、支付领域等。后是应用服务,这个主要是对业务逻辑进行编排,也可以理解为业务层。

领域和子域

在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小服务需要解决的问题域,构建合适的领域模型。

举个例子,比如保险领域,我们可以把保险细分为承保、收付、再保以及理赔等子域,而承保子域还可以继续细分为投保、保全(寿险)、批改(财险)等子子域。

核心域、通用域和支撑域

子域可以根据重要程度和功能属性划分为如下:

  • 核心域:决定产品和公司核心竞争力的子域,它是业务成功的主要因素和公司的核心竞争力。
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能的子域。
  • 支撑域:但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域。

核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

很多公司的业务,表面看上去相似,但商业模式和战略方向是存在很大差异的,因此公司的关注点会不一样,在划分核心域、通用域和支撑域时,其结果也会出现非常大的差异。

比如同样都是电商平台的淘宝、天猫、京东和苏宁易购,他们的商业模式是不同的。淘宝是 C2C 网站,个人卖家对个人买家,而天猫、京东和苏宁易购则是 B2C 网站,是公司卖家对个人买家。即便是苏宁易购与京东都是 B2C 的模式,苏宁易购是典型的传统线下卖场转型成为电商,京东则是直营加部分平台模式。因此,在公司建立领域模型时,我们就要结合公司战略重点和商业模式,重点关注核心域。

通用语言和限界上下文

  • 通用语言:就是能够简单、清晰、准确描述业务涵义和规则的语言。
  • 限界上下文:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

通用语言

通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。那么,通用语言的价值也就很明了,它可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。

这个通用语言到场景落地,大家可能还很模糊,其实就是把领域对象、属性、代码模型对象等,通过代码和文字建立映射关系,可以通过Excel记录这个关系,这样研发可以通过代码知道这个含义,产品或者业务方可以通过文字知道这个含义,沟通起来就不会有歧义,说的简单一点,其实就是统一产品和研发的话术。

直接看下面这幅图(来源于极客时间欧创新的DDD实战课):

限界上下文

通用语言也有它的上下文环境,为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。

限界上下文是一个显式的语义和语境上的边界,领域模型便存在于边界之内。边界内,通用语言中的所有术语和词组都有特定的含义。把限界上下文拆解开看,限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。

实体和值对象

  • 实体 = 身份标识 + 可变性【状态 + 行为】
  • 值对象 = 将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。

实体

DDD中要求实体是的且可持续变化的。意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。性由的身份标识来决定的。可变性也正反映了实体本身的状态和行为。

实体以 DO(领域对象)的形式存在,每个实体对象都有的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

值对象

当你只关心某个对象的属性时,该对象便可作为一个值对象。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。

还是举个订单的例子,订单是一个实体,里面包含地址,这个地址可以只通过属性嵌入的方式形成的订单实体对象,也可以将地址通过json序列化一个string类型的数据,存到DB的一个字段中,那么这个Json串就是一个值对象,是不是很好理解?下面给个简单的图(同样是源于极客时间欧创新的DDD实战课):

聚合和聚合根

聚合

聚合:我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。

聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。

聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

聚合根

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

  • 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  • 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  • 后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

上面讲的还是有些抽象,下面看一个图就能很好理解(同样是源于极客时间欧创新的DDD实战课):

简单概括一下:

  • 通过事件风暴(我理解就是头脑风暴,不过我们一般都是先通过个人理解,然后再和相关核心同学进行沟通),得到实体和值对象;
  • 将这些实体和值对象聚合为“投保聚合”和“客户聚合”,其中“投保单”和“客户”是两者的聚合根;
  • 找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象;
  • 在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。

领域服务和应用服务

领域服务

当一些逻辑不属于某个实体时,可以把这些逻辑单独拿出来放到领域服务中,理想的情况是没有领域服务,如果领域服务使用不恰当,慢慢又演化回了以前逻辑都在service层的局面。

可以使用领域服务的情况:

  • 执行一个显著的业务操作
  • 对领域对象进行转换
  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象

这个“领域服务的情况”的实际场景感觉讲的比较模糊,这个可以看后面具体Demo的讲解。

应用服务

应用层作为展现层与领域层的桥梁,是用来表达用例和用户故事的主要手段。

应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。

我理解应用层,主要是处理一些业务逻辑,如果业务的处理非常复杂,我觉得这一层并不“薄”,比如商城的下单和支付业务,涉及的业务渠道和支付方式非常多,且每一种渠道的处理逻辑还不一样,所以从个人经验来看在,这一层应用层还是非常复杂的。

领域事件

领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联,下面简单说明领域事件:

  • 事件发布:构建一个事件,需要标识,然后发布;
  • 事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;
  • 事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;
  • 事件处理:先将事件存储,然后再处理。

比如下订单后,给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用,那么不同用户,赠送的东西不同,逻辑就会变得又臭又长。这里的比较好的方式是,用户下订单成功后,发布领域事件,积分聚合与优惠券聚合监听订单发布的领域事件进行处理。

我开始还以为“领域事件”是一种特殊的事件,后来我找了比较多的文章,发现领域事件其实就是把异步IO包装成另外一种说法,也就是通过异步方式实现系统的解耦,这个在MVC中使用也很常见嘛,的区别就是领域事件在发布方和订阅方一定需要存储。这个在极客时间中花了整整一个篇幅来讲解“领域事件”,对于我个人来说,这块知识点收获很小,文章主要是将异步的方式,结合微服务和具体的业务场景进行讲解。

如果大家对“领域事件”有其它的见解,可以私信我,我们一起学习探讨哈!

其它

工厂

按照个人理解,工厂的概念其实并不属于领域驱动的范畴,它其实是实现领域驱动相关功能的一种方式,用于构建领域模型、聚合根和领域服务等。

领域模型中的工厂:

  • 将创建复杂对象和聚合的职责分配给一个单独的对象,它并不承担领域模型中的职责,但是领域设计的一部份;
  • 对于聚合来说,我们应该一次性的创建整个聚合,并且确保它的不变条件得到满足;
  • 工厂只承担创建模型的工作,不具有其它领域行为;
  • 一个含有工厂方法的聚合根的主要职责是完成它的聚合行为;
  • 在聚合上使用工厂方法能更好的表达通用语言,这是使用构造函数所不能表达的。

聚合根中的工厂:

  • 聚合根中的工厂方法表现出了领域概念;
  • 工厂方法可以提供守卫措施。

领域服务中的工厂:

  • 在集成限界上下文时,领域服务作为工厂;
  • 领域服务的接口放在领域模型内,实现放在基础设施层。

资源库【仓储】

是聚合的管理,仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想

“工厂”和“资源库”的总结来源于掘金,感觉“听君一席话,如听一席话”,如果不结合实际的应用场景,感觉很难理解。

DDD分层架构

严格分层架构:某层只能与直接位于的下层发生耦合。

松散分层架构:允许上层与任意下层发生耦合。

在领域驱动设计(DDD)中采用的是松散分层架构,层间关系不那么严格。每层都可能使用它下面所有层的服务,而不仅仅是下一层的服务。每层都可能是半透明的,这意味着有些服务只对上一层可见,而有些服务对上面的所有层都可见。

分层的作用,从上往下:

  • 用户交互层:web请求,rpc请求,mq消息等外部输入均被视为外部输入的请求,可能修改到内部的业务数据。
  • 业务应用层:与MVC中的service不同的不是,service中存储着大量业务逻辑。但在应用服务的实现中(以功能点为维度),它负责编排、转发、校验等。(在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的服务就会演化为传统的三层架构,业务逻辑会变得混乱。)
  • 领域层:或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手。
  • 基础设施层:主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现。

应用服务层直接调用基础设施层的一条线,这条线是什么意思呢?领域模型的建立是为了控制对于数据的增删改的业务边界,至于数据查询,不同的报表,不同的页面需要展示的数据聚合不具备强业务领域,因此常见的会使用CQRS方式进行查询逻辑的处理。其它的直接调用,原理类同。

DDD Deom分析

这个Demo不是我写的,下面我将围绕这个Demo,来分析作者是如何落地DDD的。

github地址:https://github.com/louyanfeng25/ddd-demo

工程结构

整个项目的工厂结构如下,其中核心是baiyan-ddd-base和baiyan-ddd-core:

baiyan-ddd-base:

baiyan-ddd-core:

表结构

  • 用户表t_user
  • 角色表t_role
  • 用户角色关联表t_user_role,一个用户会有多个角色
  • 地址表
create table t_user_role (
    id           bigint auto_increment comment '主键id' primary key,
    user_id      bigint                             not null comment '用户id',
    role_id      bigint                             not null comment '角色id',
    gmt_create   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改时间',
    deleted      bigint   default                  not null comment '是否已删除'
)comment '用户角色关联表' charset = utf8;

create table t_user (
    id           bigint auto_increment comment '主键' primary key,
    user_name    varchar(64)                        null comment '用户名',
    password     varchar(255)                       null comment '密码',
    real_name    varchar(64)                        null comment '真实姓名',
    phone        bigint                             null comment '手机号',
    province     varchar(64)                        null comment '用户名',
    city         varchar(64)                        null comment '用户名',
    county       varchar(64)                        null comment '用户名',
    unit_id      bigint                             null comment '单位id',
    unit_name    varchar(64)                        null comment '单位名称',
    gmt_create   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
    deleted      bigint   default                  not null comment '是否删除,非0为已删除'
)comment '用户表' collate = utf8_bin;

create table t_role (
    id           bigint auto_increment comment '主键' primary key,
    name         varchar(256)                       not null comment '名称',
    code         varchar(64)                        null comment '角色code',
    gmt_create   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
    deleted      bigint   default                  not null comment '是否已删除'
)comment '角色表' charset = utf8;

# 地址表省略...

实体

我们定义了3个实体,分别为角色实体、地址实体、单位实体:

// 角色实体
public class Role implements Entity {
    private Long id;
    private String code; // 角色
    private String name; // 角色名
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
    @TableLogic(delval = "current_timestamp()")
    private Long deleted;
}
// 地址实体
public class Address implements ValueObject<Address{
    private String province; // 省
    private String city; // 市
    private String county; // 区
    /**
     * 比较地址相等
     * @param address 地址
     * @return
     */

    @Override
    public boolean sameValueAs(Address address){
        return Objects.equals(this,address);
    }
}
// 单位实体
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Unit implements Entity {
    private Long id;
    private String unitName; // 单位名称
}

聚合&聚合根&值对象

  • 聚合&聚合根:我们把用户、地址、角色和用户单位等聚合在一起,表述一个完整的用户信息含义,里面的聚合根就是用户,它作为信息的载体。比如电商的订单、理赔的保单,这些都可以为聚合根。
  • 值对象:对于用户数据,其实是通过聚合的方式将所有信息聚合在一起,比如地址信息,如果我们希望DB中能直接存入用户的地址信息,且应用场景中不会通过地址去查询人员信息,仅作为展示使用,可以将地址作为Json值对象,保存在t_user表中。
// 用户聚合根
public class User extends BaseUuidEntity implements AggregateRoot {
    private String userName; // 用户名
    private String realName; // 用户真实名称
    private String phone; // 用户手机号
    private String password; // 用户密码
    private Address address; // 用户地址
    private Unit unit; // 用户单位
    private List<Role> roles; // 角色
    // ...

工厂

下面我们通过工厂模式,创建用户实体:

// 用户聚合创建工厂
public class UserFactory {
    // 新建用户聚合
    public static User createUser(CreateUserCommand command){
        User user = new User(command.getUserName(), command.getRealName(), command.getPhone(), command.getPassword());
        user.bindAddress(command.getProvince(),command.getCity(),command.getCounty());
        user.bindRoleByRoleId(command.getRoles());
        return user;
    }
    // 修改用户聚合
    // ...
}

领域服务

我们需要通过领域服务,对用户单位信息做一些关联:

// 用户领域服务
public class UserDomainServiceImpl implements UserDomainService {
    @Autowired
    UnitAdapter unitAdapter;
    @Override
    public void associatedUnit(Long unitId, User user){
        UnitDTO unitByUnitId = unitAdapter.findUnitByUnitId(unitId);
        user.bindUnit(unitId,unitByUnitId.getUnitName());
    }
}

领域事件

我们插入、更新和删除数据时,希望能做一些后续处理,这个我们就可以通过领域事件来处理,由于是在系统内处理该事件,直接使用Java的Event事件作为示例(如果想做到完全隔离,或者有一些其它的要求,比如时序、频率限制等,也可以借助消息队列)。

发布领域事件:

// 领域事件发布接口
public interface DomainEventPublisher {
    // 发布事件
    void publishEvent(BaseDomainEvent event);
}
// 领域事件基类
public abstract class BaseDomainEvent<Timplements Serializable {
    private static final long serialVersionUID = 1465328245048581896L;
    // 发生时间
    private LocalDateTime occurredOn;
    // 领域事件数据
    private T data;
    public BaseDomainEvent(T data) {
        this.data = data;
        this.occurredOn = LocalDateTime.now();
    }
}

领域事件:

// 用户新增领域事件
public class UserCreateEvent extends BaseDomainEvent<User{
    public UserCreateEvent(User user) {
        super(user);
    }
}
// 用户删除领域事件
public class UserDeleteEvent extends BaseDomainEvent<Long{
    public UserDeleteEvent(Long id) {
        super(id);
    }
}
// 用户修改领域事件
public class UserUpdateEvent extends BaseDomainEvent<User{
    public UserUpdateEvent(User user) {
        super(user);
    }
}

应用服务

这个就是对业务逻辑进行编排,我们看如何对用户的逻辑进行编排:

  • 通过工厂创建一个用户对象;
  • 通过领域服务关联用户单位信息;
  • 通过仓库存储用户信息
  • 发布用户新建的领域事件
public class UserApplicationServiceImpl implements UserApplicationServic {
    @Autowired
    UserRepository userRepository;
    @Autowired
    DomainEventPublisher domainEventPublisher;
    @Autowired
    UserDomainService userDomainService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void create(CreateUserCommand command)
{
        //工厂创建用户
        User user = UserFactory.createUser(command);
        //关联单位单位信息
        userDomainService.associatedUnit(command.getUnitId(),user);
        //存储用户
        User save = userRepository.save(user);
        //发布用户新建的领域事件
        domainEventPublisher.publishEvent(new UserCreateEvent(save));
    }
}

资源库【仓储】

示例中的ORM框架使用MyBatis。

实体

前面其实也有个“实体”,但是那个实体更多是针对业务层面,这个实体主要针对数据库层面,包括用户实体、角色实体和用户角色实体,下面我们只给部分实体示例:

// 基础表结构实体
public class BaseUuidEntity {
    // 主键id 采用默认雪花算法
    @TableId
    private Long id;
    // 创建时间
    private LocalDateTime gmtCreate;
    // 修改时间
    private LocalDateTime gmtModified;
    // 是否删除,0位未删除
    @TableLogic(delval = "current_timestamp()")
    private Long deleted;
}
// 角色实体
@TableName("t_role")
public class RolePO extends BaseUuidEntity {
    /** 角色名称 */
    private String name;
    /** 角色code */
    private String code;
}
// 用户实体
@TableName("t_user")
public class UserPO extends BaseUuidEntity {
  // ...
}
// 用户角色实体
@TableName("t_user_role")
public class UserRolePO extends BaseUuidEntity {
  // ...
}

Mapper映射关系:

// 用户角色关联关系mapper
@Mapper
public interface UserRoleMapper extends BaseMapper<UserRolePO> {
}
// 用户信息mapper
@Mapper
public interface UserMapper extends BaseMapper<UserPO> {
    // 用户信息分页查询
    Page<UserPO> userPage(KeywordQuery query);
}
// 用户角色关联关系mapper
@Mapper
public interface UserRoleMapper extends BaseMapper<UserRolePO> {
}

用户领域仓储

  • 删除用户和用户角色数据;
  • 根据用户ID,获取用户信息和用户角色的详细信息;
  • 保存用户和用户角色信息。
@Repository
public class UserRepositoryImpl implements UserRepository {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;

    @Override
    public void delete(Long id){
        userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,id));
        userMapper.deleteById(id);
    }

    @Override
    public User byId(Long id){
        UserPO user = userMapper.selectById(id);
        if(Objects.isNull(user)){
            return null;
        }
        List<UserRolePO> userRoles = userRoleMapper.selectList(Wrappers.<UserRolePO>lambdaQuery()
                .eq(UserRolePO::getUserId, id).select(UserRolePO::getRoleId));
        List<Long> roleIds = CollUtil.isEmpty(userRoles) ? new ArrayList<>() : userRoles.stream()
                .map(UserRolePO::getRoleId)
                .collect(Collectors.toList());
        List<RolePO> roles = roleMapper.selectBatchIds(roleIds);
        return UserConverter.deserialize(user,roles);
    }

    @Override
    public User save(User user){
        UserPO userPo = UserConverter.serializeUser(user);
        if(Objects.isNull(user.getId())){
            userMapper.insert(userPo);
            user.setId(userPo.getId());
        }else {
            userMapper.updateById(userPo);
            userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,user.getId()));
        }
        List<UserRolePO> userRolePos = UserConverter.serializeRole(user);
        userRolePos.forEach(userRoleMapper::insert);
        return this.byId(user.getId());
    }
}

查询仓储

// CQRS模式,用户查询仓储
public interface UserQueryRepository{
    // 用户分页数据查询
    Page<UserPageDTO> userPage(KeywordQuery query);
}
// 用户信息查询仓储
@Repository
public class UserQueryRepositoryImpl implements UserQueryRepository {
    @Autowired
    private UserMapper userMapper;
    @Override
    public Page<UserPageDTO> userPage(KeywordQuery query){
        Page<UserPO> userPos = userMapper.userPage(query);
        return UserConverter.serializeUserPage(userPos);
    }
}

后记

持续2周多时间的学习,对DDD有了一个整体的认识,但是感觉目前停留的层面还不够深入,如果让我用DDD的方式去重构一个项目,目前还是不具备这项能力。

后面打算买一本DDD的书籍,再系统学习这块内容,然后找一个公司内部DDD的项目,学习实际场景的落地方式,那个时候应该就具备DDD项目重构的能力了。

这篇文章算是DDD的入门篇,后续DDD的学习,以及实战的一些经验,会持续更新到我的博客和微信公账号中。

后感谢极客时间的欧创新,对于DDD的基础概念讲解的非常通透,大家如果对DDD感兴趣,也可以订阅他的课程《DDD实战课》;然后感谢掘金的柏炎,他的4篇系列文章在DDD场景落地给了我很大启发,特别是他写的Demo,实战意义很强,文章后半部分也是对他的Demo进行的总结。

参考文章:

  • 极客时间:https://time.geekbang.org/column/article/158248?cid=100037301
  • 掘金系列博客:https://juejin.cn/post/7004002483601145863

相关文章