手把手在小项目中实践领域驱动设计(含详细代码和实践过程)

⼿把⼿在⼩项⽬中实践领域驱动设计(含详细代码和实践过程)
前⾔
前⾯已经简介过领域驱动的基本概念,前⽂介绍的COLA框架在⼤型项⽬或者微服务架构中⽬测有较好的实践,但是对于⼀个中⼩项⽬或者⼩公司来说管理⼤量依赖包模块简直就是噩梦,或者就是项⽬达不到那种规模,采⽤分包模式也是⼀种浪费,但是采⽤领域驱动设计在本⼈实践过程中确实⼤⼤提升了代码质量,最主要的改善就是使开发⼈员不再以数据库驱动开发,⽽是真正的开始从业务和领域⼊⼿,这样开发出的代码往往能更好的实现⾯向对象,将代码划分出边界,使代码的可读性更强,代码更加健壮。本⽂结合现实中使⽤领域驱动设计时遇到的问题进⾏了总结,如果错误还需海涵。
项⽬说明
本项⽬主要有3个⼤包分别包含3次不同的实践,实践的具体内容如全⽂所述
user 为第⼀次实践包含各种模型设计和简介
user2 正对user包下存在的⼀些问题做了⼀些优化设计,请查看全⽂来看具体说明
project 为了针对实际项⽬中出现的⼤聚合来做的⼀些设计,这个包下只建⽴了模型设计,其余部分如果感兴趣可以⾃⼰补充(其实就是本⼈偷懒)
使⽤⽅法如果你使⽤的是mysql数据库那么修改application.properties中的数据源即可,liquibase会⾃动将所需要的表建⽴完毕
分包
和COLA框架采⽤模块不同,我采⽤⼀个项⽬下分不同的包的模式来区分领域设计的各个模块项⽬结构如下
demo
<└─com
└─liu
└─demo
├─app            客户端服务代码
├─controller    控制层代码
├─domain        领域层
│├─client        领域层防腐对象
│└─modal        领域模型
└─infrastructure 基础层
├─repository    仓库
│└─mapper    mybatis持久包
└─serviceimpl  领域服务包
app包:客户端代码存放的地⽅,负责组装调⽤领域模型,仓库,控制事务,对应六边形架构的应⽤服务层
净烟器controller包: 控制层代码,我⽤SpringMVC实现,对应六边形架构的输⼊适配器
domain包: 为项⽬中最核⼼的领域模型相关类存放的地⽅,对应六边形架构的领域(domain)层,另外在此根⽬录下会存放领域服务的接⼝,该接⼝由基础设施层去实现,因为领域层是最核⼼的层,根据六边形架构领域层需要放在最⾥层,但是领域服务却有需求调⽤基础设施层(infrastructure)下的仓库(repository),因此在这个层中定义⼀个接⼝由infrastructure层去实现,实现依赖倒置。
client包: 我创建此包是为了反腐,为了不使领域模型外泄,有效的控制代码的边界访问⽽设⽴,举例在http协议调⽤中dto对象从controller层到app层,当要进⼊到领域层(domain)时必须将其转化成领域模型,同样数据持久化在数据库中,从数据库中直接查到的数据对象和领域对象同样存在差异,因此需要对外创建⼀个过渡对象提供给基础设施层调⽤,也许很多⼈会对这些对象放在domain层有疑问,但是我认为外部数据的访问领域对象数据的范围和权限是由领域模型去控制的,因此我觉得将其放在领域(domain)包中和适合的。
modal此包主要存放实体(Entity),值对象(VO),⽣成领域模型的⼯⼚⽅法,领域对象验证类.
infrastructure基础设施层:主要存放基础设施的地⽅,⽐如数据库持久化,调⽤外部服务,队列等
repository仓库,对持久化的抽象,屏蔽数据库对象⽣成领域对象,领域对象从创建开始就已经开始⽣命周期,⼀直到删除才结束,中间会把领域对象存储在数据库中,存储在数据库时领域对象仍然处于⽣命周期,因此仓库层的作⽤就是屏蔽持久层,让调⽤者觉得领域对象⼀直存在内存中⼀样.
mapper 由于我使⽤的是mybatis,所以我创建此层建mybatis的类放在此
serviceimpl 领域服务包,同样有很多⼈可能会有疑问为什么我讲领域服务的实现类放在基础设施层中,这⼀点我上⾯提过,为了实现依赖导致,只要是领域服务的接⼝存放在领域层(domain)那么我们仍然认为领域服务属于领域层,因为接⼝规定了领域服务的功能和⽅法。
建模
在这⾥我们假设我们和业务⽅沟通需要实现这样的功能,⽤户可以有⾃⼰的基础信息,这些信息包括⽤户名,email地址,且⽤户可以根据⽤户id和密码登录系统,且⽤户可以单独修改登录密码,也可以修改⽤户信息,根据需求分析我们可以得出⽤户有⼀个唯⼀型标识⽤户id,因此我们得出⽤户是实体,⽤户名和email这两个属性对⽤户来说并不需要维护状态的变化,修改时候为了简单将其整个对象替换即可,因此我们将其设计成值对象VO,由于⽤户可以单独修改密码因此修改密码对应前端⼀个单独⼊⼝,所有我们将密码这个属性放在⽤户对象中,因此我们得到以下模型,实体对象UserE中有⼀个修改⽤户的⽅法,只有⼀个构造⽅法,并且可以进⾏密码验证和获取⽤户基础信息,注意这⾥并没有set⽅法,⽽是⽤了类似changePassword等⽅法名代替set⽅法,这是为了使领域模型充⾎,为了使模型更好的体现业务,如果使⽤set修改密码的话,那我们怎么和业务⼈员解释修改密码这个⽅法?难道说我set了密码?这明显⽆法表⽰出领域对象的意图,反之将其命名changePassword修改密码那么就可以很好的表⽰出领域模型的意图,领域⽅法名需要表⽰出领域和业务的意图。
public class UserE  {
private String userId;
private String password;
private BaseInfoVO baseInfo ;
/**
* 修改⽤户密码
*/
public void changePassword(String password){杯芳烃
if(password==null){
throw new IllegalArgumentException("密码不能为空");
}
this.password=password;
}
public UserE(String userId, String password, BaseInfoVO baseInfo) {
this.userId = userId;
this.password = password;
过氧化氢酶活性测定this.baseInfo = baseInfo;
}
public String getUserId() {
return userId;
}
public String getPassword() {
return this.password;
}
/**
* 认证服务,查询传⼊密码是否匹配
* @param password 需要认证的密码
* @return 认证结果
*/
public boolean authentication(String password) {
return password != null && password.equals(this.password);
}
public BaseInfoVO getBaseInfo() {
return baseInfo;
}
/**
降噪轮胎
* 修改⽤户基础信息
*/
public void changeInfo(BaseInfoVO baseInfoVO){
this.baseInfo=baseInfoVO;
}
}
下⾯是BaseInfoVO为⽤户的基础信息,同样我们也没有暴露set⽅法,由于它只是⼀个⽤户的值对象,因此并没有那么多的领域⽅法,⾄此我们的核⼼领域对象就已经建⽴完成了
/**
* @author Liush
* @description ⽤户基础信息
* @date 2019/9/5 9:48
**/
public class BaseInfoVO {
private String username;
private String email;
public BaseInfoVO(String username, String email) {
if(username==null){
throw new IllegalArgumentException("⽤户名不能为空");
}
if(email==null){
throw new IllegalArgumentException("邮箱不能为空");
}
this.username = username;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
}
现在让我们考虑如何新增⼀个⽤户,创建⽤户对应领域模型就是创建⼀个⽤户实体(UserE),那我们如
何做到将领域层的模型信息不外泄到其它地⽅呢?因为新增⽤户也属于领域(业务的⼀部分),举个例⼦我们去银⾏开个户也要到银⾏才能办理,我们不能到公安局去开银⾏账户,所以我们把创建⽤户对象放在领域层,⽽创建⽤户实体(UserE)有两种⽅法,⼀种是直接调⽤其构造,⼀种是通过⼯⼚类来创建,但是这⾥⼜会出现⼀个问题,⽤户实体(UserE)需要⼀个BaseInfoVO(基础信息)来构造,但是按照领域驱动设计的理念来设计
BaseInfoVO(基础信息)只能有领域在领域层中才能去创建,因为我们的通⽤语⾔是⽤户创建和修改了基础信息,如果我们将BaseInfoVO(基础信息)放在领域层外创建就好⽐⼀句话少了主语。
我采⽤在领域层中使⽤⼯⼚类去创建⽤户实体(UserE),在⼯⼚⽅法中传⼊⼀个DTO来隔离领域层外部的信息,代码如下,其创建了⼀个⽤户实体
(UserE)并且使⽤UUID分配了⼀个默认的⽤户ID给⽤户,最后调⽤⽤户实体(UserE)的构造⽅法去创建⽤户实体对象,执⾏完这⼀⾏代码,⼀个⽤户对象就已经进⼊了⽣命周期,直到在数据库中删除或者将⽤户状态改成不可能⽤这个⽤户的⽣命周期才结束。
package com.liu.demo.dal;
import com.liu.demo.user.domain.client.UserDTO;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @author Liush
* @description 领域⼯⼚类
* @date 2019/9/5 14:10
**/
@Component
public class UserFactory {
public UserE createUser(UserDTO userDTO ){
BaseInfoVO baseInfoVO=new Username(),Email());
return new UserE(UUID.randomUUID().toString(),Password(),baseInfoVO);
}
}
持久化
现在是时候考虑⽤户对象持久化的问题了,毕竟⽤户对象不能永远存留在内存中,必须在不使⽤对象时将其持久化到硬盘中基础设施层包infrastructure下的repository就是为了解决这个问题,它的作⽤是屏蔽数据库持久化的⼀些代码,让代码看起来更贴近领域设计⼀些,我们可以从仓库中根据查条件直接还原出⼀个⽤户实体对象,对领域代码来说数据库持久化代码就好像不存在⼀样,下⾯是⽤户仓库代码,这⾥注意⼀下⼀个⽅法findUsersByName,这是⼀个查询⽅法,从数据库中查询出UserPO然后将其转成UserDTO,这⾥我们看到我们并没有⾛领域模型,因为查询往往为了效率特别是批量查询我们做了⼀部分妥协,但是这部分妥协是可以接受的,因为我们并没有执⾏领域动作(command)的代码,只是返回⼀个dto对象给前端。
package com.liu.demo.pository;
import com.liu.demo.user.domain.client.UserDTO;
import com.liu.demo.user.domain.client.UserPO;
import com.liu.demo.dal.UserE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.liu.demo.pository.mapper.UserMapper;
import java.util.ArrayList;
import java.util.List;
/**
* @author Liush
* @description ⽤户仓库
* @date 2019/9/5 11:17
**/
@Repository
public class UserRepository {
@Autowired
乳酸环丙沙星氯化钠private UserMapper userMapper;
@Autowired
private UserRepositoryConvert userRepositoryConvert;
/**
*  根据⽤户id查⽤户
*/
public UserE findUser(String userId){
UserPO userPO =userMapper.findUser(userId);
vertToUserE(userPO);
}

本文发布于:2024-09-22 17:34:38,感谢您对本站的认可!

本文链接:https://www.17tex.com/tex/1/148917.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:领域   对象   设计   模型
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议