DDD领域模型入门到熟练【概念+实战】

791 阅读8分钟

DDD领域模型

主要参考 GitHub 开源实战项目 本文基于此开源项目文档整理,并补充个人的一些浅薄见解。详细可参考该开源项目的代码与文档,以求获取更深刻的理解。

image.png

上面图左侧为业务设计相关的概念方法,右侧为编码实战设计相关的概念方法。

概念学习

设计概念

设计需要理解的概念:

  • 业务抽象相关概念:领域事件、决策命令、领域名称

  • 统一语言相关概念:限界上下文

  • 问题域划分:核心域、支撑域、通用域

概念解释补充说明&个人理解
领域事件简单的理解就是进行的一些业务行为流程。(1)领域事件具有原子性,必须拆分到不可拆分为止。
决策命令【领域事件】的触发动作(1)与代码层面“API"是映射的。
领域名称【领域事件】和【决策命令】均出现的名词
弹性边界关注是否需要将系统拆分成能够独立开发、部署、运行的服务。如一个微服务。
限界上下文主要是识别业务上下文的边界,(1)如支付上下文、商品上下文等都属于限界上下文。
(2) 多个限界上下文之间的依赖关系需要分析是否存在【双向依赖、传递依赖、过长依赖】的问题。
(3)一个限界上下文必须在一个弹性边界之中,不可以跨越弹性边界。
核心域业务盈利的来源业务都是要赚钱的嘛!
支撑域支撑核心域运作, 具备个性化的需求间接帮助赚钱的东西,具体来说是,区分市场上同类产品的功能亮点 。
通用域业内现成的方案,通过改造可以使用
问题子域包含核心域、支撑域、通用域(1) 问题子域和限界上下文是完全不同的两个概念:
问题子域解决的是问题澄清和优先级排序问题;
限界上下文解决的是业务边界识别和统一语言问题。

编码概念

进行编码设计需要理解的一些概念:

  • 领域模型:聚合根、实体、值对象(领域服务、工厂、仓库)

  • 上下文依赖关系:分层结构、服务模块包拆分、API设计

  • 资源分配策略:开发集成策略、技术栈、团队分组分工

概念解释补充说明&个人理解
领域建模抽象模型,与具体的实现细节无关(1)任务:梳理业务实体的关系,画实体图。
(2)注意:此过程不进行数据库表的设计!
聚合包含聚合根、实体、值对象。(1)聚合封装业务逻辑;
(2)
聚合根(1)聚合根是十分类似数据库主键的东西,根据它能找到该聚合的全部实体
实体有生命周期的、能被修改、有唯一标识的对象(1)会进行查增删改的行为的对象
值对象不可变的对象(1)【不可变】理解为【不进行修改行为】更合理一点。(2)实体的一个原子字段也可以理解为一个值对象
实体vs值对象

===超哥补充说明的点===

  • 实体和值对象的概念是相对于一个聚合的,可能在聚合A,a是一个实体,在聚合B,a是一个值对象。

a到底是实体还是值对象要看在聚合A或者B里面,是否存在对a的修改行为。

(如A聚合存在业务方法对a这条数据库记录进行增删改的行为,那么a就是实体,如果只有查的行为,那么a就是值对象)。

  • 当然,有些情况下,值对象它就是值对象,不可能变为实体。

比如对于用户表的一个字段【用户名】,它就是一个值对象,即使对用户表进行修改,也不过是替换这个值对象,不会对这个值对象本身进行修改。

补充概念:充血模型与贫血模型

什么是领域模型(domain model)?贫血模型(anaemic domain model)和充血模型(Rich Domain Model) - 掘金

可参考上面链接理解,了解这两个概念,就会发现之前传统MVC项目写法与大学课程理论的脱节。 虽然之前一直说Java是面向对象的语言,我们传统MVC确是面对过程的编码。

项目学习

主要参考 GitHub 开源实战项目

分层参考

java
└── com
    └── gtw
        └── business
            ├── application                -- 用户接口层
            │   ├── controller                -- HTTP 请求
            │   ├── mq                        -- mq 消费入口
            │   ├── report                    -- 报表类、查询入口
            │   ├── rpc                       -- rpc 服务提供入口(这里指rpc的实现,rpc接口提供单独放在可打包发布的module中)
            │   └── scheduler                 -- 定时任务调度入口
            ├── common                     -- 公共通用层
            │   ├── component                 -- 通用基础设施层的接口,如 mq,cache
            │   ├── model                     -- 公用的数据对象和抽象接口
            │   └── utils                     -- 工具类
            ├── domain                     -- 领域服务层
            │   ├── aggregate                 -- 领域模型
            │   └── service                   -- 领域服务
            ├── infrastructure             -- 基础设施层
            │   ├── cache
            │   ├── db                        -- 对领域服务中的仓储实现
            │   │   └── reponsitory           
            │   ├── event                     -- 对领域服务中的事件实现
            │   │   ├── listener
            │   │   └── publisher
            │   ├── mq                        -- mq 生产者
            │   └── rpc                       -- rpc 服务的调用
            └── service                    -- 应用服务层(CQRS)
                ├── command
                │   ├── cmd                -- 命令的请求参数XXXCommand对象
                │   └── impl               -- 命令请求服务的实现(请求接口直接定义在command包下)
                └── query
                    ├── dto                -- 查询结果DTO对象
                    ├── impl               -- 查询服务的实现(查询接口直接定义在command包下)
                    └── qry                -- 查询的条件参数XXXQuery对象

分层与代码细则

  • 洋葱模型(只允许外层依赖内层,不允许内层知道外层的细节。)

image.png

正如架构图中看到的,基础实施层位于其他所有层的上方,接口定义在其它层,基础实施实现这些接口

或者可以这样来表述:领域层等其他层不应该依赖于基础实施层,两者都应该依赖于抽象。

这也就是意味着一个重要的落地指导原则: 所有依赖基础实施实现的功能,抽象和接口都应该定义在领域层或应用层中

  • CQRS(Command Query Resonsibility Segregation,命令查询的责任分离)

(1)所有查询的条件封装成XXXQuery对象,所有命令的请求封装成XXXCommand对象

(2)查询和命令写在不同的Service类中(如上面service下分出command,query层)

  • application.controller作为六边形架构的适配器

image.png

  • domain的aggregate与service,业务逻辑何时放在aggregate(领域模型)、何时放在service(领域事件)?

放在domain.service的场景:

(1)不属于单个聚合的业务(多个聚合关联的业务)

(2)静态方法

(3)rpc等外部服务调用的业务

  • 消息队列 生产者放infrastructure.mq,消费者放application.mq(接口也类似,都是安装出入的原则)

Demo的业务逻辑

假设正在为一家货运公司开发新的软件,最初的需求包括三项基本功能:

  1. 事先预约货物

  2. 跟踪客户货物的主要处理流程

  3. 当货物到达其处理过程中的某个位置时,自动向客户寄送发票

cargo货物领域代码解读

根据CQRS原则,看代码也分成两个部分看:Query查询与Command命令。

Query 查询

(1)层级调用:application.controller->service.query

(2)关键代码:

application.controller:

image.png

service.query:

image.png

Command 命令

(1)层级调用:application.controller -> service.command -> domain.aggregate & domain.service

(2)关键代码:

controller:

image.png

service.command:

image.png

基本流程:


-> 请求参数校验

-> 获取到领域对象(Cargo)

-> 调用到领域对象的方法(Cargo.changeDelivery)

-> 使用存储服务修改保存领域对象(CargoRepository.save(cargo))`

(3)关联的重点接口:

CargoResository(此接口由infrastructure.db.repository层级实现,CargoRespositoryImpl类中还做了CargoDO<->Cargo的相互转化)

image.png

数据模型与转化
数据模型
  • Cargo:

位于domain.aggregate层,货物的领域模型

  • CargoDO

位于infrastructure层,货物的数据库对象映射

  • CargoDTO

位于service.query层,要返回给前端的货物对象,包含CargoDO不能直接拿到的位置信息。

(需要根据CargoDO的位置码OriginLocationCode查询一次位置表,以获取到完整的信息)

数据转化Assembler&Converter
  • CargoDO -> CargoDTO

service.query.assembler,代码如下:

image.png

  • Cargo <-> CargoDO

db.converter,代码如下:

image.png

【杂谈】

  • 本人喜欢学习一们技术的时候做总结,您现在看到的这篇文章可能是以前写好很久,之后也可能会对这篇文章继续修改!

  • 由于文章一开始“初衷”是写给自己看的,所以内容可能有些地方过于简单或者含糊不清,请加以包容宽待,谢谢!

  • 另外,本人写文章比较重视的是文章的目录结构,聪明的读者通过目录结构阅读我的文章也能比较容易清除内容的细节!

  • 最后,创作不易,喜欢与不喜欢的读者点赞支持一下,谢谢!