一文了解大厂的DDD领域驱动设计

2年前 (2022) 程序员胖胖胖虎阿
361 0 0

1.什么是DDD?

DDD名为:Domain Driven Design (领域驱动设计) 简称:DDD
概念来源于2004年著名建模专家eric evans发表的他最具影响力的书籍

2.DDD与我们的传统开发又有什么区别和优势?

有过工作的朋友都知道国内大多数开发模式为:
MVC【 Model-View-Controller(模型-视图-控制器) 模式】,MVVM【Model-View-ViewMode(简称:前后端分离)】,MVCC(并发版本控制)以及后面的SOA架构(面向服务架构,软件接口组件调用)等
而DDD我的理解为:将微服务拆分的设计理念改造后嵌入到咱们的代码开发中,只是将服务改造成了领域,不同的领域做着自己相关的业务事情。它能更换的帮助《开发人员》理解:面向对象,系统解耦合,实现高内聚低耦合,并且达到复用的开发层次结构,举个浅显的例子:

一般电商微服务架构会划分为: 将其引入到DDD中就会划分为:
商品中心:负责商品的展示、导航、维护; 商品域:负责商品展示。领导,维护
订单中心:负责订单的生成和生命周期管理; 订单域: 负责订单的生成和生命周期管理
库存中心:负责维护商品的库存; 库存域:负责维护商品的库存;
交易中心:负责交易相关的业务; 交易域:负责交易相关的业务,例如,银行接口对接
促销中心:负责各种促销活动的支持 促销域:负责各种促销活动的支持

而开发的方式一般都是先设计数据库,在去写代码实现:而我对此也做了一个比较:

把各个相对应的层次,做相对应的事,需要扩展,或者业务变动,迭代,咱们可以不用关心之前写的代码逻辑,以及业务,只需要新启一个领域,或者在相同领域中新启一个实现类 调取之前业务或者领域功能模块接口即可实现,维护成本相当之低,相比MVC的代码层次,迭代,业务变动,扩展,咱们需要去Service层去读取和理解之前相关代码以及业务才可到相对应的代码处进行修改扩展等,然而代码风格每个人都有着自己的风格,读取和理解起来相当困难、
DDD它的缺点便是:代码臃肿,修改字段,极其繁琐,层次结构多,开发水平要求高,开发周期相对应较长

3.实践讲解:

好了,前面介绍了这么多,接下来我给大家介绍DDD的架构设计拆分,看看到底是什么东东
DDD一般架构分为:四边形架构,以及六边形架构
四边形架构便是:DIP(依赖倒置)

核心的定义是: 高层模块不应该依赖于底层模块,两者都应该依赖于抽象 抽象不应该依赖于实现细节,实现细节应该依赖于接口

按照DIP的原则,领域层就可以不再依赖于基础设施层,基础设施层通过注入持久化的实现就完成了对领域层的解耦,采用依赖注入原则的新分层架构模型就变成如下所示:
一文了解大厂的DDD领域驱动设计

DIP分层

采用了依赖注入方式后,其实可以发现事实上已经没有分层概念了。无论高层还是底层,实际只依赖于抽象,整个分层好像被推平了,这就引入下一个架构六边形架构

六边形架构(Hexagonal architecture)

六边形架构是Alistair Cockburn在2005年提出,解决了传统的分层架构所带来的问题,实际上它也是一种分层架构,只不过不是上下或左右,而是变成了内部和外部。在《实现领域驱动设计》一书中,作者将六边形架构应用到领域驱动设计的实现,六边形的内部代表了application和domain层。外部代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。
一文了解大厂的DDD领域驱动设计
按照领域分层的模型,在应用层和领域层内置后,一个典型的六边形架构应用有两个端口,一个端口对应用户接口层,用于应用控制,一个对应数据访问层,用于数据获取和持久化。每个端口都可以对应几个适配器,该应用可以被自动化测试,系统层面的回归测试,用户交互操作,远程HTTP调用,REST调用或者其他。在数据方面,通过配置使用外部的数据库,可以是Oracle数据库,mock的数据库,测试数据库或生产数据库,从而实现应用和外部数据库的解耦。
另外值得一提的是,在六边形架构中,自动化测试和用户具有同等的地位,在实现用户界面的同时就需要考虑自动化测试。它们对应相同的端口。六边形架构不仅让自动化测试这件事情成为设计第一要素,同时自动化测试也保证应用逻辑不会泄露到用户界面,在技术上保证了层次的分界。
使用六边形架构的时候,我们应该根据用例来设计应用程序,而不是需要支持的客户数量来设计。任何客户都可能向不同的端口发出请求,但是所有的适配器都使用相同的API。
六边形架构的功能非常强大,可以作为基层架构并用于支持系统的其他架构。
一文了解大厂的DDD领域驱动设计
Alistair Cockburn提出了六边形架构,又被称为端口和适配器架构。观察上图我们发现,对于核心的应用程序和领域模型来说,其他的底层依赖或实现都可以抽象为输入和输出两类。组织关系变为了一个二维的内外关系,而不是上下结构。每个io与应用程序之前均有适配器完成隔离工作,每个最外围的边都是一个端口。基于六边形架构设计的系统是DDD追求的最终形态。

数据驱动和领域驱动对比

一文了解大厂的DDD领域驱动设计

领域驱动设计与之前的系统设计开发过程有很大的不同:

  1. 就在于系统的参与角色,产品、开发、测试等,需要形成一套通用语言;
  2. 在于方案设计不再把db设计放在一个核心问题去解决,更加专注于业务模型本身,进行领域、业务聚合的设计,领域层的聚合及实体才是整个系统的核心内容;
  3. 真正的面向对象编程,由过程式的事务脚本方式,转变为真正的面向对象。

4.将架构解刨一般可以分为:

分层架构

一文了解大厂的DDD领域驱动设计

用户界面层/表示层Facade
用户界面层负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
该层包含与其他应用系统(如Web服务、RMI接口、Web应用程序以及批处理前端)交互的接口与通信设施。它负责Request的解释、验证以及转换。另外,它也负责Response的序列化,如通过HTTP协议向web浏览器或web服务客户端传输HTML或XML,或远程Java客户端的DTO类和远程外观接口的序列化。
该层的主要职责是与外部用户(包括Web服务、其他系统)交互,如接受用户的访问,展示必要的数据信息。
用户界面层facade目录:
(1)api存放Controller类,接受用户或者外部系统的访问,展示必要的数据信息。
(2)handle存放GlobalExceptionHandler全局异常处理类或者全局拦截器。
(3)model存放DTO(Request、Reponse)、Factory(Assembler)类,
Factory负责数据传输对象DTO与领域对象Domain相互转换。
应用层Application
应用层定义了软件要完成的任务,并且指挥表达领域概念的对象来解决问题。
该层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要通道。应用层要尽量简单。它不包含任务业务规则或知识,只是为了下一层的领域对象协助任务、分配工作。它没有反映业务情况的状态,但它可以具有反映用户或程序的某个任务的进展状态。应用层主要负责组织整个应用的流程,是面向用例设计的。
该层非常适合处理事务,日志和安全等。相对于领域层,应用层应该是很薄的一层。它只是协调领域层对象执行实际的工作。应用层中主要组件是Service,因为主要职责是协调各组件工作,所以通常会与多个组件交互,如其他Service,Domain、Factory等等。

应用层application目录:
(1)service存放Service类,调用Domain执行命令操作,负责Domain的任务编排和分配工作。
(2)external存放ExternalService类,负责与其他系统的应用层进行交互,通常是我们主动访问第三方服务。
(3)model存放DTO(ExtRequest、ExtReponse)类。
应用层application可以合并到领域层biz目录。领域层/模型层Biz领域层主要负责表达业务概念,业务状态信息和业务规则。

Domain层

是整个系统的核心层,几乎全部的业务逻辑会在该层实现。领域模型层主要包含以下的内容:

实体(Entities):具有唯一标识的对象值对象(Value Objects): 无需唯一标识。 领域服务(Domain): 与业务逻辑相关的,具有属性和行为的对象。 聚合/聚合根(Aggregates & Aggregate Roots):
聚合是指一组具有内聚关系的相关对象的集合。 工厂(Factories): 创建复杂对象,隐藏创建细节。 仓储(Repository):
提供查找和持久化对象的方法。领域层biz目录:

(1)domain存放Domain类, Domain负责业务逻辑,调用Repository对象来执行数据库操作。
Domain没有直接访问数据库的代码,具体的数据库操作是通过调用Repository对象完成的。
注意,除了CQRS模式外,Repository都应该是由Domain调用的,而不是由Service调用。

(2)repository存放Repository类,调用Dao或者Mapper对象类执行数据库操作。
(3)factory存放Factory类,负责Domain和实体Entity的转换。

基础设施层Infrastructure
基础设施层为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件。基础设施层以不同的方式支持所有三个层,促进层之间的通信。基础设施包括独立于我们的应用程序存在的一切:外部库,数据库引擎,应用程序服务器,消息后端等。基础设施层

Infrastructure目录:
(1)commons存放通用工具类Utils、常量类Constant、枚举类Enum、BizErrCode错误码类等等。(2)persistence存放Dao或者Mapper类,负责把持久化数据映射成实体Entity对象。注意,领域对象是具有属性和行为的对象,是有状态的,数据和行为都是可以重用的。在很多Java项目中,Service类是无状态的,而且一般是单例,业务逻辑直接写到Service类里面,这种方式本质上是面向过程的编程方式,丢失了面向对象的所有好处,重用性极差。应用“贫血模型”会把属于对象的数据和行为分离,领域对象不再是一个整体,破坏了领域模型。只有对领域驱动有足够的认知,工程师才会正确运用领域的理念去编程,告诉工程师业务逻辑是写在Domain不管用,他们可能还是会在Service里面写业务逻辑代码,创建很多private私有方法。

为了方便大家更好的理解:
我引用了开源项目《xtoon-boot》的架构图:

技术选型

  • Springboot
  • Apache Shiro
  • Mybatis-plus
  • Swagger
  • Hibernate-validator
  • Alibaba Druid
  • Element-ui

主要模块

  1. 登录注册:账号、手机号验证登录,租户注册;
  2. 用户管理:用户新增,分配角色,禁用等;
  3. 角色管理:角色新增,查看,维护菜单等;
  4. 菜单管理:树形菜单管理,可配置菜单和按钮权限等;
  5. 租户管理:租户列表,禁用等;
  6. 日志管理:记录操作日志记录和查询;

项目结构

xtoon-boot
├─db                            数据库SQL脚本
│ 
├─xtoon-common                  公共模块
│    │ 
│    └─java 
│         ├─domain              领域通用类
│         └─util                工具类
│   
├─xtoon-api                     接口模块(API接口层)
│    │ 
│    ├─web        
│    │    ├─common              接口通用类
│    │    ├─util                接口工具类
│    │    └─controller          controller类 (提供给前端接口,对外接口。类似于mvc的c层)
│    └─resources 
│        ├─static.swagger       swagger文件
│        ├─application.yml      全局配置文件
│        └─logback-spring.xml   日志配置文件
│ 
├─xtoon-sys                     系统管理子域
│    │ 
│    └─java 
│         ├─application         应用层
│         │    ├─assembler      DTO转换类(也叫装配器,实现模型转换,APIModel和Domain等转换)
│         │    ├─command        命令入参
│         │    ├─service        应用服务 (轻业务,非核心服务,一般做入参校验)
│         │    ├─task            任务定义 (一般协调领域模型,或者做定时任务)
│         │    ├─dto            DTO (有些项目为Model,试图模型,数据模型定义(vo,dto为大多数))
│         │    └─impl           应用接口实现
│         ├─domain              领域层(核心)
│         │    ├─model          领域模型(类似于mvc的实体pojo)
│         │    ├─service        领域服务 (一些不能归属某个居停领域模型的行为,有些项目回合dict领域划分结合,类似于mvc的service)
│         │    ├─events        领域事件
│         │    ├─dict          领域划分(也叫子域划分,与:xtoon-org (组织管理子域)相同,有些项目没单独抽取)
│         │    │	├─dictVO.java     领域值对象(本身的CURD操作在此处)
│         │    │	├─dictEntity.java     领域实体(通常为充血模型,需要聚合根)
│         │    │	├─dictAgg.java     领域聚合(通常为实体聚合)
│         │    │	└─dictService.java     领域服务(不能归入上述模型,如分页条件查询可写在此处)
│         │    │	dict(领域划分)一般与上述的service(领域服务)相似,因此有些项目没有dict,
│         │    │    dict(领域划分) 直接写在了service(领域服务)中
│         │    ├─specification  规格校验
│         │    ├─factory  领域工厂(负责复杂的领域对象创建,封装调节)
│         │    └─external       外部接口(防腐层)
│         └─infrastructure      基础设施层
│              ├─persistence    持久化类
│              ├─po             持久化对象
│              ├─repository     仓储类,持久化接口或实现,可与ORM框架映射结合(类似于mvc的dao层)
│              ├─config			配置类,存放配置文件或者类的地方
│              ├─toolkit        工具类,存放工具的地方,有些项目叫util
│              ├─common     基础工具模块,与上面的:xtoon-common模块相同,这里是没抽取前放置地方,抽取后删除
│              └─external       外部服务类 (向其他层输出通用服务,通讯技术支持等,有些项目命名为:General)
│   
├─xtoon-org                     组织管理子域
│         │    ├─dictorg          领域划分(也叫子域划分,与:xtoon-org (组织管理子域)相同,有些项目没单独抽取)
│         │    │	├─dictVO.java     领域值对象(本身的CURD操作在此处)
│         │    │	├─dictEntity.java     领域实体(通常为充血模型,需要聚合根)
│         │    │	├─dictAgg.java     领域聚合(通常为实体聚合)
│         │    │	└─dictService.java     领域服务(不能归入上述模型,如分页条件查询可写在此处)
├─xtoon-resource                      资源层
│              ├─statics              静态资源
│              ├─template             静态模板,系统页面
│              ├─application.yml      全局配置文件
│              ├─等等。。等等。。

5.细分概念性知识

(1) domain 领域模型

  • domain 领域模型(领域实体),一般项目中会划分多个领域,也会有多个领域实体
  • domain (领域模型/实体中一般分为:贫血模型,充血模型,失血模型,一般失血模型较为少用)
  • 失血模型:基于数据库的领域设计方式其实就是典型的失血模型,以 Java 为例,POJO 只有简单的基于 field(字段/属性) 的 setter、getter 方法,POJO 之间的关系隐藏在对象的某些 ID 里,由外面的 manager 解释,比如 student.teacherId,学生(student) 并不知道他跟老师(teacher)有关系,但 manager 会通过 student.teacherId 得到一个 Teacher对象,有点类似于父子类中的多肽
  • 贫血模型:和失血模型类似,但更为丰富一些,会被各种service调用,例如学生选课,借书,(student)类会分在不同的业务中,所以一般(student)实体类下会嵌套其他对象属性,通过类的getter方法可以得到一个借书类(Borrowbooks),同理借书类,也可以根据特定方法获取学生类以及其他类
  • 充血模型:充血模型的存在让对象失去了血统的纯正性,他不再是一个纯的内存对象,这个对象里除了有贫血模型的一些方法属性外,还埋藏了一个对数据库的操作,一些业务操作,为保证模型的完整性,充血模型在有些情况下是必然存在的,比如在一个学习里可以有好几千个学生,每个学生有好几百门课程。如果我在构建一个学校的时候把所有学生或者课程都拿出来,这个效率就太差了:

提问:如何解决将(student)实体类改成充血模型?
可以将Student类加入upgrade()方法或者run()方法,进行和自身状态相关的业务逻辑
每个实体操作自己实体变化,跨实体的变化,可以通过领域服务自治,自我发展。
例如:一个(student)类下,写入一个graduate()方法,当调用graduate()方法时,及完成了学生毕业状态的改变,表面这个学生不在学校了
(2)仓储(repository)

一般作为以数据库打交道,有点类似于dao,但它并不是dao!不是dao!不是dao!
dao和repository在领域驱动设计中都很重要。dao是面向数据访问的,是关系型数据库和应用之间的契约。
repository:位于领域层,面向AggregationRoot(聚合根),repository是一个独立的抽象,使用领域通用语言,它与dao进行交互,并提供领域理解的语言方式进行数据业务数据访问,dao方法时细颗粒度的,更贴近于数据库,而repository方法的粒度粗一些,而更接近领域,领域对象应该只依赖于repository接口
它能解决,充血模型与其他充血模型进行交互,(例如:student充血与course充血,A学生选取了一门计算机课程,通过repository将学生与课程进行数据库的外键绑定)所以说类似于dao,但又并不是dao

(3)工厂模型

-顾名思义就是设计模式:factory (也叫领域工厂),为domain来组装负责对象实体
用好repository和factory 来应对复杂的实体组装

(4)限界上下文

context:上下文,语言或系统运行的环境,用来封装通用语言和领域对象,提供上下文环境,保证在领域内的一些术语,业务相关的对象等,这个边界定义了模型的适用范围

(5)子域

子域一般分为:核心子域,通用子域(一个领域都能够用到的子域),支持子域(支撑域实际上就是不包含核心竞争力的功能,但又是必须的支撑)等

(6)领域事件

event:一个按钮按下,产生中断事件,一个回车,前端页面有侦听事件,在事件风暴建模活动中,事件也是作为领域建模的突破口,事件的重要性不言而喻。
领域事件主要用途有:

  • 从事件角度丰富了领域模型
  • 保证聚合间的数据一致性
  • 实现事件事件溯源和 CQRS 等
  • 限界上下文间集成(发布订阅模式)

(7)领域服务

当我们考虑领域逻辑时,首先想到的应该是实体与值对象中具有的领域逻辑,而有些场景下,实体与值对象无法承载这些领域行为,如对多个领域对象作为输入,进行计算并产出一个值对象;又或是需要将操作成集合化的聚合,
如在school下需要将所有student 中的course汇总,而本身 school和 student 是为两个聚合,若考虑借助 student 去完成该业务操作,不太妥当,在此场景下,可通过领域服务来承载着这些领域行为。
领域服务存在如下特征:

  • 执行一个显著的业务操作过程
  • 对领域对象进行转换
  • 需要使用多个聚合内的实体和值对象编排业务逻辑
  • 领域行为需要访问外部资源

虽说领域服务能够承载领域逻辑,却不能说将所有的领域逻辑都往里塞,如此,导致领域对象贫血。只有当实体与值对象承载不住或是本身并不属于实体或值对象的职责内时,才考虑领域服务来承载,领域服务是一种妥协的结果,并不是说领域服务越多越好

(8)聚合

聚合就是归类的意思,把同类事物统一处理
聚合划分,经统一语言与业务分析阶段,借助事件风暴,用例分析法,四色建模法等活动后获取一系列相关对象,可形成对象关联图,而对象中嵌套对象就变成了嵌套的对象就变成了聚合根
聚合根:如果把聚合比作组织,聚合根则是组织的负责人,聚合根也叫做根实体,它不仅仅是实体,还是实体的管理者;
职责:
1,作为实体,具备自己的业务属性,业务行为,业务逻辑
2,作为聚合的管理者,
在聚合内部,负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑;
在聚合之间:它是聚合对外的接口人,以聚合根ID的方式接受外部请求和任务,实现上下文中的聚合之间的业务协同;
聚合之间通过聚合根关联引用,如果需要访问其他聚合的实体,先访问聚合根,再导航到聚合内部的实体;即外部对象不能直接访问聚合内的实体;

(9)值对象

对于值对象,我倾向于将它理解为,基础类型之延伸,既能封装基础类型,又能约束内部属性间关系,还能拥有着自身的领域行为,而与实体的区别是,没有唯一身份标识,尽管带来了持久化的一些问题,但还是存在解决方案。以
DateTime 理解值对象最好不过了,DateTime 内部的自身约束保证了,每一次变动的 DateTime 都是最新的,当我们想在 2月 28 日加 1,这便要依靠 DateTime 中的行为去约束内部的属性。

(10)实体

对于实体来讲,这个概念对于我们并不陌生,拥有者唯一的身份标识符,内含属性作为该实体的静态特征,作为聚合所拥有的领域知识,拥有着与自身相关的领域行为

好了就介绍这么多,关于DDD领取驱动设计,还有很多玩法和简介,鄙人比较才疏学浅,就暂且介绍到这里,如果还想深入学习理解可以参考
阿里盒马DDD
美团技术团队的DDD
去看看,对于我的文章收货会更大哦!
有兴趣的朋友可以加入裙:947405150进行交流学习

版权声明:程序员胖胖胖虎阿 发表于 2022年9月15日 上午7:48。
转载请注明:一文了解大厂的DDD领域驱动设计 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...