大家都知道软件开发不是一蹴而就的事情,我们不可能在不了解产品(或行业领域)的前提下进行软件开发,在开发前通常需要进行大量的业务知识梳理,然后才能到软件设计的层面,最后才是开发。而在业务知识梳理的过程中,必然会形成某个领域知识,根据领域知识来一步步驱动软件设计,就是领域驱动设计(DDD,Domain-Driven Design)的基本概念 。
在业务初期,功能大都非常简单,普通的 CRUD 就基本能满足要求,此时系统是清晰的。但随着产品的不断迭代和演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。各个模块之间彼此关联,甚至到后期连相应的开发者都很难说清模块的具体功能和意图到底是啥。这就会导致在想要修改一个功能时,要追溯到这个功能需要修改的点就要很长时间,更别提修改带来的不可预知的影响面
比如:
订单服务中提供了查询、创建订单相关的接口,也提供了订单评价、支付的接口。同时订单表是个大表,包含了非常多字段。我们在维护代码时,将会导致牵一发而动全身,很可能原本我们只是想改下评价相关的功能,却影响到了创建订单的核心流程。虽然我们可以通过测试来保证功能的完备性,但当我们在订单领域有大量需求同时并行开发时将会出现改动重叠、恶性循环、疲于奔命修改各种问题的局面,而且大量的全量回归会给测试带来不可接受的灾难
但现实中绝大部分公司都是这样一个状态,然后一般他们的解决方案是不断的重构系统,让系统的设计随着业务成长也进行不断的演进。通过重构出一些独立的类来存放某些通用的逻辑解决混乱问题,但是我们很难给它一个业务上的含义,只能以技术纬度进行描述,那么带来的问题就是其他人接手这块代码的时候不知道这个的含义或者只能通过修改通用逻辑来达到某些需求。
实际上,领域模型本身也不是一个陌生的单词,说直白点,在早期开发中,领域模型就是数据库设计。因为你想:我们做传统项目的流程或者说包括现在我们做项目的流程,都是首先讨论需求,然后是数据库建模,在需求逐步确定的过程不断的去变更数据库的设计,接着我们在项目开发阶段,发现有些关系没有建、有些字段少了、有些表结构设计不合理,又在不断的去调整设计,最后上线。在传统项目中,数据库是整个项目的根本,数据模型出来以后后续的开发都是围绕着数据展开,然后形成如下的一个架构 :
很显然,这其中存在的问题如下:
我们试想一下如果一个软件产品不依赖数据库存储设备,那我们怎么去设计这个软件呢?如果没有了数据存储,那么我们的领域模型就得基于程序本身来设计。那这个就是 DDD 需要去考虑的问题。
当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。比如当两个对象的标识不同时,即使两个对象的其他属性全都相同,我们也认为他们是两个完全不同的实体。
当一个对象用于对事物进行描述而没有唯一标识时,那么它被称作值对象。因为在领域中并不是任何时候一个事物都需要有一个唯一的标识,也就是说我们并不关心具体是哪个事物,只关心这个事物是什么。比如下单流程中,对于配送地址来说,只要是地址信息相同,我们就认为是同一个配送地址。由于不具有唯一标示,我们也不能说"这一个"值对象或者"那一个"值对象。
一些重要的领域行为或操作,它们不太适合建模为实体对象或者值对象,它们本质上只是一些操作,并不是具体的事物,另一方面这些操作往往又会涉及到多个领域对象的操作,它们只负责来协调这些领域对象完成操作而已,那么我们可以归类它们为领域服务。它实现了全部业务逻辑并且通过各种校验手段保证业务的正确性。同时呢,它也能避免在应用层出现领域逻辑。理解起来,领域服务有点facade的味道。
聚合是通过定义领域对象之间清晰的所属关系以及边界来实现领域模型的内聚,以此来避免形成错综复杂的、难以维护的对象关系网。聚合定义了一组具有内聚关系的相关领域对象的集合,我们可以把聚合看作是一个修改数据的单元。
聚合根属于实体对象,它是领域对象中一个高度内聚的核心对象。(聚合根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法)
若一个聚合仅有一个实体,那这个实体就是聚合根;但要有多个实体,我们就要思考聚合内哪个对象有独立存在的意义且可以和外部领域直接进行交互。
DDD中的工厂也是一种封装思想的体现。引入工厂的原因是:有时创建一个领域对象是一件相对比较复杂的事情,而不是简单的new操作。工厂的作用是隐藏创建对象的细节。事实上大部分情况下,领域对象的创建都不会相对太复杂,故我们仅需使用简单的构造函数创建对象就可以。隐藏创建对象细节的好处是显而易见的,这样就可以不会让领域层的业务逻辑泄露到应用层,同时也减轻应用层负担,它只要简单调用领域工厂来创建出期望的对象就可以了。
资源仓储封装了基础设施来提供查询和持久化聚合操作。这样能够让我们始终关注在模型层面,把对象的存储和访问都委托给资源库来完成。它不是数据库的封装,而是领域层与基础设施之间的桥梁。DDD 关心的是领域内的模型,而不是数据库的操作。
我们知道,这些 O 不管叫什么名字,其本质都还是对象(Object),既然本质都一样,为什么非要给他们套上各种马甲?个人认为原因有三:第一,随着编程工业化的发展,需要有一套合理的体系出现。中国人喜欢造神,外国人喜欢造概念,于是 MVC、MVP、MVVM 等编程模型就出现了,为了搭配这些编程模型的使用,需要对 Object 的功能进行划分,于是我们便看到了这些层出不穷的 Object。当然这里并没有批评这些概念的意思。其二,我认为在团队协作编码中,一个好的命名方式是可以节约很多时间成本的。就比如 getItemById
一眼看去就知道是通过 id 获取一个 item 对象, ItemVO
一眼看去就知道是前端透出的 json 对应的对象。其三,如此划分,可以让项目结构更加清楚,不至于出现东一块西一块,对象乱扔的局面。尽可能避免了在多人协作时对象混乱的情况。总的来说,这一切都是为了让软件编程更加合理、更加规范、更加高效。
关系图:
我们应该从上面这张图可以看出来,一个vo是由多个dto组成而一个dto是由多个do组成而每一个do都是对应关系型数据库的一个表
DAO (Data Access Object 数据存取对象) 存储访问数据库完成数据处理操作的方法的对象。
是指位于业务逻辑和持久化数据之间实现对持久化数据的访问。通俗来讲,就是将数据库操作都封装起来。(接口设计+SQL编写,不涉及业务代码)
Po 本质上是和 Do 一样,但是在DDD领域驱动设计中统一为Do(贫血模式和充血模型)
Persistant Object(持久对象),基本上,PO对象中的属性就是对应着数据库中表的字段,加上一些get和set方法的组成。
例:个人信息表中分别有:id,name,age,sex,birthday
则PO对象中的属性有:
{
"id": 1,
"name": "张三",
"age": 20,
"sex": "男",
"birthday": "2000-03-24"
}
就是从现实世界中抽象出来的有形或无形的业务实体。一般和数据中的表结构一 一对应。
Business Object(业务对象),相比于PO来说,BO的信息则是在PO信息的基础上进行扩充,也可以理解为多个PO对象的信息按照业务流程必要的拼凑在一起形成的对象。 (用于关联表的数据汇总地)
例:
个人信息表中分别有:id,name,age,sex,birthday
1.
个人学历表中分别有:id,school,educational_background
按照个人信息表与学历表进行关联,将用户的个人信息集合在一起。
{
"id": 1,
"name": "张三",
"age": 20,
"sex": "男",
"birthday": "2000-03-24",
"school":"石家庄铁道大学",
"educational_background":"本科"
}
Data Transfer Object(数据传输对象),顾名思义,dto的作用是传递数据。但是我们按照业务流程处理得到的数据,并不是全部都要进行显示,或者并不能完全都按照当前形势进行展示,按照业务要求,还要在已有数据的基础上进行过滤删减。
例:个人信息表中分别有:id,name,age,sex,birthday
我们可能只需要用户的 名字、年龄和性别 来显示,像生日这样的信息就没有必要进行传输了,所以对已有的数据进行删减,只传输需要的信息。
则DTO对象中的信息为:
{
"id": 1,
"name": "张三",
"age": 20,
"sex": "男"
}
下面2种情况都可以升级使用Dto
Bo进行增减-> Dto
Po进行增减-> Dto
列:
Po 有10个属性(默认都是private修饰) 而前端只需要5个属性 那么我可以将需要的5个属性修饰符改为(protected)
然后Dto继承Do,这样就可以实现了,如果这个时候前端说在加一个属性,而这个属性是和数据库没有关系的,生成的方式是在这个5个属性中某两个属性相加拼接成第6个属性,我们只需要在Dto中添加这个属性,然后在这个属性的get中处理下就行了。
是不是感觉到了方便了, 扩展性比较好
Value Object(值对象),可以理解为展示要用的数据,传递到前端页面上,直接进行展示。为了保证数据可以直接展示使用,就要对数据进行处理。
例:个人信息表中分别有:id,name,age,sex,birthday
我们需要展示的是用户的当前状态,像年龄和性别
则没有必要分开显示,可以进行合并。
则vo对象中的信息为:id,name,type
,birthday
{
"id": 1,
"name": "张三",
"type":"少年",
"birthday": "2000-03-24"
}
上面这种情况还是好的,有些时候后端存储的都是一些各种业务流程状态,当显示到前端时候需要将这些状态转换为实际的值
注意:在有些时候vo对象可以不需要,直接使用Dto代替,因为有些时候前端需要的的数据,不需要转换处理,那么就把Dto直接给前端就行
以上图片仅供参考,从PO之后,其实也不一定都是按这个流程走下来。(最好是按照这个流程)
偷懒办法:
有的PO可能不需要扩充,直接删减得到DTO。
1.
PO也可能不用删减和扩充,那么直接使用Po。
1.
DTO也可能不需要再处理就可以直接到页面显示,和VO无甚差别 (那么这时候DTO可以代替VO)。
1.
最完美的形式是PO不用删减和扩充,同时显示到页面上时候也不需要再处理就可以直接到页面显示 (那么这时候PO可以代替DTO和VO)。
这些都是有可能的,这些概念不用分的很清楚进行使用,主要还是取决于当前项目。
如果每一步都要进行加工,则PO-BO-DTO-VO的对象分别存放,会使得开发时候很累,
虽然日后维护的话会好很多但是,这和你有什么关系??
反正我开发就是,跟着感觉走,不要强制自己非要这样使用,而是跟着需求走.可能你开发的过程中绝大部分都是一个Po就完事了
但是在需求复杂的时候自己不经意就会使用这些O
进行配合了
,
贫血模型:
定义对象的简单的属性值,没有业务逻辑上的方法
充血模型:
充血模型也就是我们在定义属性的同时也会定义方法,我们的属性是可以通过某些方式直接得到属性值,那我们也就可以在对象中嵌入方法直接创建出一个具有属性值的对象。也就是说这个对象不再需要我们在进行进一步的操作,这也就复合了OOP的三大特性之一的封装
为什么大家都在使用贫血模型?
使用贫血模型的传统开发模式,将数据与业务逻辑彻底分离,通过get set方法改变对象属性,对象属性可以随意被修改,这也就如上面所说违反了OOP的三大特性之封装特性。这样的编程方式也就是面向过程的编程方式,面向过程的编程方式是符合人类大脑逻辑的,不用使用太多的设计模式和过多的设计。还有就是在开发中大家经常说的一句及其不负责任的一句话:“怎么方便怎么来”,就一直在堆代码,完全不像以后的可拓展性。也就是说基于贫血模型的编程方式是面向过程编程,人类的思考逻辑方式很符合,在编程过程️也很方便,所以大家都很愿意接受这种编程方式。
综上所述:
充血模型的设计要比贫血模型更加有难度
大家一致使用基于贫血模型的面向过程编程已经成为习惯,比较难转换思想
还有就是对代码不负责任的态度。(这是大数程序员的通病吧)
是所有的场景都适合使用充血模型吗?
使用充血模型也就是使用基于充血模型的DDD的开发模式,上文也一再强调,充血模型也是定义模式复杂,设计难等,代码开发量也许时其他模型的多,其主要原因还是设计起来难。那就是说如果我们设计一个很简单的业务逻辑,那我们还需要这么复杂的设计思想吗? 并且这个业务在后续的迭代也不变复杂,那我个人认为我们就使用我们的基于贫血模型的面向过程的编程思想。简单的东西何必复杂化呢。这里突然想到我同时讲的一个段子:
普通程序员写hello word 直接print
*
高级程序员写hello word 各种设计模式各种可拓展最后输出hello word
*
技术专家写hello word ,直接打印hello word
当然我们在进行一个复杂的业务场景,那就需要进行基于充血模型的DDD(领域驱动模型)开发模式了。其实DDD的开发模式也就是充分的遵循OOP发三大特性(或者四大特性,封装,继承,多态,(抽象)),如果是贫血模型的面向过程编程那到最后的结果就是点练成线,由线变成网,密密麻麻不可维护。所以说复杂业务逻辑基于充血模型的进行开发。但是也会是有问题的那就是类膨胀,一个类有很多代码。这个还是可以解决的,那就是通过设计模式,和业务逻辑细分进行解决
用人话来说: 就是把放在service层对实体类的操作… 反正就是和实体类相关联的都放入实体类中进行处理, 一般都是在get 和 set中进行处理, 业务比较复杂的话那么可能会产生实体类的行为…
这时候service就是真正的服务层了 ,来协调不同服务和资源 ,最终将接口完善后交付给->infrastructure层
DDD领域驱动模板有很多每个人对DDD的理解不同所创建的模板也相差很大,下面这套DDD是我最常使用的也是我在工作中总结出来的
以上架构只是提供参考的实际中可能会发生变化,但是domain层是最核心的,所有层都是围绕domain进行处理的
估计大家看这些结构看的都闷逼了…我们接下来就来对这个架构的用法进行讲解.
我们先对每一层进行剖解
在DDD设计思想中,Application层主要职责为组装domain层各个组件及基础设施层的公共组件,完成具体的业务服务。Application层可以理解为粘合各个组件的胶水,使得零散的组件组合在一起提供完整的业务服务。在复杂的业务场景下,一个业务case通常需要多个domain实体参与进来,所以Application的粘合效用正好有了用武之地。
比如: 在application层定义一个接口 Integer userSave(UserDto dto); //用户信息保存接口
那么我们在某些场景下: 需要在reids领域
保存到缓存中 ,用户领域
保存到数据库中, 那么我们就可以在对应的领域继承这个接口然后进行实现
调用application接口的时候都会有多个实现类,需要指定具体的实现类,有两种方式
第一种方式,
@Autowired
@Qualifier("实现类首字母小写(全称)")
第二种方式
@Resource(name = "实现类首字母小写(全称)")
这层用的比较少 ,就是简单的定义了一层接口(需求)告诉各个层接下来这个接口需要协调开发。
一般用于协调多个组件或者领域,结合干一件事情的时候使用。
比如 :
如果还是不知道怎么使用这一层的话,那么你可以这样.当前领域(biz->quote)需要开发一个需求,但是还需要其他领域或者基础设施层的协助. 那么你就可以在application定义一个接口,然后让她们能干这些事情的领域层或者基础设施层,实现这个接口,最后我们统一都在interfaces->assembler->quote里进行合并
注意: 一般一个领域或者子领域,只会有一个application接口
一般一个domain中会有一个biz层这个层下面就是公司项目的业务层,和项目上的导航栏基本一 一对应
比如:
像这种导航栏一般都是一个模块等于一个领域
比如: 报价管理模块,那么我就可以在biz下面创建一个quote领域,专门就只干报价这一个模块的事情
domain->factory
领域对象工厂。用于复杂领域对象的创建/重建。重建是指通过respostory加载持久化对象后,重建领域对象。
在spring框架中提供了ioc控制翻转,所以不需要这一层,也就是不需要我们自己造轮子了,写工厂代码
就是从现实世界中抽象出来的有形或无形的业务实体。与数据库表结构一一对应,通过DAO层向上传输数据源对象
。
do层不做任何业务处理,但是可以做一些数据的效验
由于ddd提倡充血模型的缘故,属性的逻辑放到domain object中(比如entity, dto中)
比如: 用户年龄限制…一些数据效验,还可以把属性的行为可以写出来,避免堆积到service层
bo就是把业务逻辑封装成一个对象,这个对象可以包括一个或多个对象
简单来说: 用于结合多个do/po (多个相互关联的表数据汇总)
dto依赖do或者bo, 在很多时候dto属性和do属性基本都是一致的
,只有当数据库字段不能改变但是do内属性需要增加或者减少的时候,需要依赖dto对do进行重组
首先,我认为“没必要过度执着于DTO”,在小型项目中,真的很多情况没有必要非要用DTO返回数据。
直接用原生对象也完全ok。当你的项目需要DTO的需求的时候,你在组装也没事。
然后说说我对DTO的理解:
首要的作用,我认为就是减少原生对象的多余参数。包括为了安全,有时候也为了节约流量。例如:密码,你就不能返回到前端。因为不安全。 其次假如说:获取博客列表的时候,也不能返回博客全文吧。顶多就返回标题,id,和前几句。
1.
我认为第二个作用是减少重复代码。组装对象。假设用户的基本信息,详细信息,以及一些额外的附加数据。现在要返回给前台,你可以选择拼成map返回。但是map的缺点是,无法复用。假如需要增加或者删除某个参数还好,假如某个参数名写错了,而你又有几十个地方用到了这个单词,那就需要改几十个。而DTO,你只需要改一处即可。其次,可以规范参数类型,因为map返回大多都是string直接返回,在存数据的时候也无法校验数据类型是否是想要的类型。假如前台要的单位是分,你后台直接传了个浮点回去,前端就会找你麻烦。但是如果用DTO,你直接把前端的数据要求放进DTO里,你如果直接把不符合要求的类型放进去,就会报错。同时,也更便于你在DTO类里直接写注释,便于别人阅读。
1.
简单来说dto就只关心数据的读和取
的最佳方式
仓库。我们将仓库的接口定义归类在domain层,因为他和domain entity联系紧密。仓库接口定义了和基础实施的持久化层交互契约,完成领域对应的增删改查操作。domain层的repository(dao)只是定义契约的接口
领域事件。领域中产生的一些,消息事件, 异步事件…其他事件,可以在性能和解耦层面得到好处。
我们通常借助于消息中间件,通过事件通知/订阅的方式落地。
event的方法一般都是public void xxx(参数) 的方法
当然如果比较简单的逻辑话那么我们可以直接处理
比如: 添加用户的个人信息
需求: 如果添加的用户年龄大于18岁那么给他发送一条广告推销短信
提取事件: 年龄在18岁以上触发事件
我们就可以写一个事件方法
@Component
public class UserEvent {
/** * 年龄大于18-触发,发送短信事件 */
public void ageGreater18(UserDo user){
if (user.getAge()>18) {
String name = user.getName();
System.out.println("调用发短信的接口给"+name+"用户发短信");
}
}
}
然后我们在对应需要这个事件的service层中调用这个事件就行
Service,业务层或服务层,主要负责业务模块的逻辑应用设计。 Service层的实现,具体调用到已经定义的DAO接口,封装service层的业务逻辑有利于通用的业业务逻辑的独立性和重复利用性。如果把Dao层当作积木,那么Service层则是对积木的搭建。同时还作用协调不同服务组装调用来最终完成一个功能接口
infrastructure这一层没什么好说的,因为这一层都是一些项目的基础配置和公用的东西,自己可以根据自己项目情况自行调整
client 层 (用于访问其他服务) 一般只要是微服务都会需要远程调用
common层(项目公用的东西)
config层(项目的所有配置) 只要是类上携带@Configuration注解的都是配置类
durable层(项目码表) 项目中多个类或者模块,都需要数据: 变量 常量,枚举…
module层(组件层) 比如: es redis rabbitmq poi … 这些组件的操作都放在这层下面
utils(通用工具) 简单来说就是放自己封装的一些好用并且常用的工具
其他层根据自己项目情况自行补充
结构要和domain领域一 一对应
Assembler是组装器,负责完成domain model对象到dto的转换,组装职责包括:
完成类型转换、数据格式化;日志格式化,状态enum 转换为前端认识的string;
1.
将多个domain领域对象(do)组装为需要的dto对象,
简单来理解就是: 不相干的多个领域配合当前领域整合成一个,当前领域需要的Dto。
1.
将不同的domain领域操作组装成一个方法
1.
…反正就是各种组装,和小时候后玩拼图一样,把一幅图拆分成多个可组装的小图, 或者将多个小图组装成一幅完整的图.而且想怎么拼就怎么拼,比如: 拼人图,我只想组装头,其他不要,那么我们就可以将关需要的部分先进行组装, 这就是我们assembler层的概念
注意: 一般assembler里的方法只要设计到数据库操作,那么就会产生事务,我们需要在方法上面添加事务注解
@Transactional(rollbackFor = Exception.class)
作用在于入参数过滤、和将dto转换为vo返回前端 , 以及部分加解密和简单的逻辑处理 ,或者对于页面数据的封装,你可以理解为一个入口或者出口.
就和家里的大门一样,没钥匙进不去,有的门还有指纹识别,和人脸识别开门呢
视图层,其作用是将指定页面的展示数据封装起来,通常用于controller返回一个页面需要展示的所有信息
常用于,页面初始化的时候前端调用的接口
个领域配合当前领域整合成一个,当前领域需要的Dto。
将不同的domain领域操作组装成一个方法
1.
…反正就是各种组装,和小时候后玩拼图一样,把一幅图拆分成多个可组装的小图, 或者将多个小图组装成一幅完整的图.而且想怎么拼就怎么拼,比如: 拼人图,我只想组装头,其他不要,那么我们就可以将关需要的部分先进行组装, 这就是我们assembler层的概念
注意: 一般assembler里的方法只要设计到数据库操作,那么就会产生事务,我们需要在方法上面添加事务注解
@Transactional(rollbackFor = Exception.class)
作用在于入参数过滤、和将dto转换为vo返回前端 , 以及部分加解密和简单的逻辑处理 ,或者对于页面数据的封装,你可以理解为一个入口或者出口.
就和家里的大门一样,没钥匙进不去,有的门还有指纹识别,和人脸识别开门呢
视图层,其作用是将指定页面的展示数据封装起来,通常用于controller返回一个页面需要展示的所有信息
常用于,页面初始化的时候前端调用的接口
而且一般一个模块最多也就是1~2个vo所以不用在分层了直接方在vo目录下就行
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_45203607/article/details/120248829
内容来源于网络,如有侵权,请联系作者删除!