DDD重构项目
云图库项目ddd重构
-
把原项目复制一份,用副本进行项目重构
-
原结构为 com.yupi.yupicturebackend.xxx,保留 com.yupi,新建一个和 yupicturebackend 包同级的包来当作ddd重构包
-
把主类 xxxApplication 先拖到新建的根包中。因为主类会扫描相同包下的路径,我们要在重构的ddd的项目架构中运行程序,所以先将原包下的主程序移动到当前包下,不然项目没法启动。
先重构*** infrastructure 基础设施层***,因为代码通用、调用广泛,很多基础的代码会被领域层等等的业务依赖。
infrastructure 基础设施层:repository imlp(数据访问层实现,也就是mapper、dao、repository层) 、缓存、OSS、第三方工具等
-
annotation包直接拖,先不用管可能调用别的,这是一个很基础的代码,自己写的注解适用比较广泛。 -
aop , api, common , config , exception , utils这些也都拖过来 -
mapper拖过来。因为对mapper在config包中写了一个配置类,所以要去修改对应路径。虽然mapper是用 Mybatis-X 插件自动生成的,但是生成的mapper类继承了
BaseMapper<T>类,其实是自动交给了mybatis-plus去实现的,所以也看成是实现类。也就是 domain领域层的 依赖倒置原则 ,具体的技术实现全部下放到 infrastructure基础设施层,domain层只留接口调用,接口提供方是怎么实现的无需理会。
-
然后将
manager包下的CosManager类可以移过来。没有调用mapper、没有调用service,就是一个很干净的、可以独立于项目去提供服务的一个类。所以可以拖到api目录下。(如果有需要也可以更名为CosApi。) -
可以发现这些包都是一些通用代码,全局可用,都放在基础设施类里等待调用。
注意是 直接拖动, 不要复制,复制可能导致代码中的路径和包名等和原位置一致,直接拖动的话会自动重构为当前目录。
ok,至此,基础设施层重构完毕。
层层递进,接下来是重构 domain领域层 。
领域层包含:聚合(domain service)、entity实体、Value Object值对象、repository api。
!!! 一个领域一个领域地去重构,而不是一次性把多个领域的代码同时改造,这样出了问题不好还原。
===
一个领域一个领域地拆的同时,回顾当初的开发流程 :先开发的 model,再开发的 service,最后开发 controller。所以,先确定了哪个领域,然后按照开发流程顺序解构原包。但是原包中肯定不可能只包含我们正在重构的领域的代码,秉持拆一个包就拆干净的原则,其他代码该移动到哪里就直接一步到位即可。
那么现在就可以发现(初见端倪),以及不按照目标重构了,而是按开发流程重构。因为按照目标重构(解读一下什么叫按照目标重构:就是在设计ddd的时候已经差不多约定好了的原项目的哪些包应该放在重构后的项目的哪个包下,以结果为驱动去重构,但是出了问题不好还原,所以这里按照开发流程顺序重构可以尽量地在重构期间不出纰漏。(但是还是先捡着目标整,比如先重构用户领域,那按照开发流程解构的时候就要先把包内和用户相关的实体等用户领域的相关归属放到domain.user下对应的包中以及interfaces用户接口层,比如原包下的model.dto.user => interfaces.dto.user;model.vo.UserVO和LoginUserVO => interfaces.vo.user下)。
===
用户领域
重构 model 包
重构 constant 包
其实也一个用户角色的常量类,直接拖到domain.user下就好了。
至于为什么不和枚举类一样放到 domain.user.valueobject 下,是因为枚举类里是会有一些逻辑的,比如根据value获取枚举类的方法,而常量类就单纯的常量而已,所以直接连带着包拖过去单独存在,区分度会更好一点。
重构数据访问层(也就是mapper)
应用上了 - 依赖倒置原则 - ,在 domain.repository 中定义与数据库交互的接口,然后下放到 infrastructure基础设施层中写相应的实现,领域内 只 提供接口,不实现逻辑。
又由于项目中使用的是 Mybatis-Plus 框架,可以让接口直接继承其提供的 IService 接口,接口的实现继承 ServiceImlp 类,这样不用写任何逻辑,只需要定义一个接口类和一个该接口的实现类,借助框架就可以直接拥有一批操作数据库的方法,简化开发。
流程就是:在domain包中定义一个repository 包,创建 UserRepository 接口并继承框架提供的 IService
重构 Service(ps:最重要 )
先移动接口和实现类是有原因的,首先,重构之前,是controller接口层中调用了service的方法,也就是说原本的service是被controller调用的,现在新的应用服务层就是替代了原本的service,这样接口层在我们重构service的时候会自动重构为引用我们新创建的应用服务层的业务代码。
流程:将原包下 service.UserService 和 service.impl.UserServiceImpl 拖到新包下的 application.service 和 application.service.impl 下,然后选择 UserService 按IDEA快捷键 shift + f6 进行全局重命名。-- 然后就会发现 未改动的 controller包中调用的原UserService方法的方法名都变成了新包下重命名后的方法(将原始服务接口的调用改为新应用服务接口的调用),减少了手动修改的代码量。
复制过来之后将service接口和实现类中的方法中的继承都删除掉(在最底层的基础设施层infrastructure中已经定义了与数据库交互的接口UserRepository,在领域层直接注入调用即可实现对数据库的操作),因为ddd中是要求上层调用下层,domain只需要乖乖等着上层的应用层调用即可。
尽量采用充血模型。
观察service实现类中,只要不涉及调用了其他业务服务和领域的逻辑, 都可以下沉到领域层(领域服务或者实体中)。
应用服务要组合调用实体和领域服务。
牢记三个原则:
- 应用服务类要调用领域服务类,把领域服务类注入进来,把很多的调用改为调用领域服务;同时组合上领域类中实体的调用,要把应用服务类的逻辑下沉到领域服务和实体类中。
- 什么情况下不能拆:这个方法中调用了其他领域的领域服务或者调用了应用服务,那就只能放到应用服务中,牢记上层只能调用下层 原则(也尽量不要跨层调用)。
- 要替controller层“擦屁股”,controller层需要调用的代码以及复杂的逻辑都需要改写到服务层(为之后的重构controller到用户接口层interfaces做铺垫)。当然,如果发现写在应用服务层中还可以下沉,那么还可以继续将这些逻辑下沉到领域服务中。
在领域服务层中,在该下沉的下沉完了、该重构的重构完了之后,还差给应用服务层兜底。应用服务层还需要实现数据库增删改查的方法,要么继承mybatis plus提供的 IService<> 接口(偷懒的方法,因为不符合ddd架构原则),要么就...自己写呗~~其实也就是把方法定义出来后直接 return userRepository.xxx() 就好了。
小技巧:如果在修改领域服务的时候发现有方法没被调用(即在接口中显示为灰色),那这个方法就可以被移除掉的。这样可以避免同样的一个方法既在实体里出现了,又在service里出现。
越往上层,都越尽可能地只用接口调用,逻辑下沉。
小练手:将controller层中的addUser方法也试着拆分一下。
/**
* 创建用户
*/
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest) {
ThrowUtils.throwIf(userAddRequest == null, ErrorCode.PARAMS_ERROR);
// 把所有参数取出来,再调用 UserService
User user = new User();
BeanUtils.copyProperties(userAddRequest, user);
// 默认密码
final String DEFAULT_PASSWORD = "123456789";
String encryptPassword = userApplicationService.getEncryptPassword(DEFAULT_PASSWORD);
user.setUserPassword(encryptPassword);
// 插入数据库
boolean result = userApplicationService.save(user);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
return ResultUtils.success(user.getId());
}
~~以上是addUser方法,其中对象转换的那一步可以联想到在infrastructure基础设施层中创建的 UserAssembler 用户转换类,那么想要调用它就需要基础设施层的上一次domain领域层调用,也就是需要用户的领域服务类中调用转换类其中的方法,所以先在用户领域服务类和接口中实现用户转换的方法,再到领域层的上一层application应用服务层中调用领域服务实现用户转换的方法,再到顶层的interfaces用户接口层中调用应用服务层的方法替换原逻辑,由此实现了逻辑下沉。(当然这里可以直接调用,因为ddd四层结构中,每一层都可以调用最底层infrastructure基础设施层的逻辑)~
上述描述错误!转换类的包是定义在了顶层的用户接口层,牺牲掉一些合理性,直接在向上调用即可。 直接在controller方法中用转换类方法替换原转换逻辑(原转换逻辑被拆分到了转换类),属于同级调用。
然后,controller层中的操作数据库的代码,应用服务层实现一层调用领域服务的接口,然后领域层中实现对基础设施层中数据库交互接口调用,实现逻辑下沉。尽量的保证越往上越精简代码越少。
现在又发现一种思路,还是比如这段addUser方法,可以在用户领域服务中实现所有controller方法的逻辑,然后在应用服务层中接口调用方法,最后在controller中用该方法替换原方法中的所有逻辑即可,这样使得上层保留的代码非常简洁明了,而且也不用像刚刚上边说的那样,连着实现两个用户转换的方法只能使得部分逻辑下沉,这样可以近乎全部的逻辑都下沉到领域服务中。
改完之后是这个效果
public BaseResponse
return ResultUtils.success(userApplicationService.addUser(userAddRequest));
}
由此可见,仅需要一行调用即可。
如果说我们发现应用服务层既为controller提供服务了,也将逻辑下沉到领域服务中了,但是有些情况下还是会报错。可能是因为有些应用服务,除了上层下层的调用,还会被同级的其他领域的应用服务调用。所以引申出,我们的应用服务层还有一层职责就是,为其他的应用服务提供调用(如果有需要的话)。
至此,第一个领域 用户领域 算是重构完毕了,接下来如法炮制,重构 图片领域、空间领域,就可以了。
依旧,按照开发流程,从 model 层开始重构。
理论上从领域服务的接口都要删掉对于框架接口的继承,自己实现需要的增删改查等基本方法,但是由于用户领域我们已经实操过一遍了,剩下的这两个领域可以用这种偷懒的方法,这样就可以既取得了便利,有学会了两种重构方法。
在应用服务层解构原Picture的service时,发现有部分逻辑调用了其他领域的逻辑,所以按照严格意义上来讲是不能够将逻辑从应用服务层下沉到图片的领域服务中的,因为下层不能调用上层。但是代码过多地冗余在上层不符合我们ddd重构项目的初衷,并且,如果按照拆解,需要重构完空间领域之后将图片应用服务中相关逻辑分摊给空间并完成逻辑下沉,空间还有调用其他领域的逻辑,也需要同样的做法,但是这时候,编码过程中实践大于理论就显现出来了,如果真的严格按照ddd重构原则,刚才说的后者重构方法会花费更高的改造成本,那如果不严格符合原则直接将逻辑在图片领域中下沉,就可以做到节约精力和成本的同时简化开发流程,所以,实际生产活动中,还是以灵活为主。
所以,要时刻考虑成本,在不牺牲过多合理性的前提下,可以降本增效。
就像图片应用服务的getPictureVOPage方法,其中调用了用户应用服务的方法,一定是不可以下沉逻辑的,和上边讲的为了降低成本而将逻辑下沉到图片领域服务中不一样,上边涉及到的只是空间领域的校验服务,下沉到图片领域服务中其实是无伤大雅的,因为就是一个校验逻辑而已,但是这里涉及到的用户应用服务的逻辑是功能逻辑或者说是属于业务逻辑了,一定是平级调用,所以为保证合理性,这里不能下沉。
然后还是一样的,等图片的应用服务类逻辑都下沉处理好了之后,在领域服务类中处理,然后再利用小技巧点开领域服务接口类观察呈灰色的方法(也就是没有被使用的方法),直接从接口和实现类中删掉。
对于需要转换类的逻辑,因为转换类包定义在了用户接口层,所以需要将沿途的接口调用都改成转换之前的类,然后层层传送到用户接口层之后,在用户接口层同级调用转换类中的转换方法,由此来保证下级不能调用上级的原则。
至此,图片领域重构完毕(但是并没有像用户领域一样重构那么细粒度)。
重构 空间模块
在解构service时,对于空间分析实现类,它所有的方法实现都是通过调用空间应用服务类和图片应用服务类来完成的,也就是通过其他两个空间类调用它们对应的数据库表达到空间数据分析的目的,所以空间分析类本身没有数据库表,因此不需要下放。所以复制空间和空间成员两个接口及其实现类到领域层即可。
同样的,还是不用去掉领域层和应用服务层中两个模块的继承,这样可以牺牲可接受范围内少量的合理性,来达到提高开发效率的目的。
ps:复制到领域层后,impl下的实现类不要直接快捷键重命名,这样不会自动重构实现类实现的接口类类名,要在类里该名然后alt + enter重命名。
不光是调用了应用服务的方法不能下沉,调用了包含调用应用服务的方法的方法也不能下沉。所以还有一种方法,倒反天罡不处理~。不改就好了。
按照流程顺序重构,最后是重构controller为用户接口层interfaces,第一步是将空间相关的controller类移动过去,第二步 是再assembler包中编写对应的转换类。最后查看逻辑是否需要下沉。
!!! 对于用户接口层中除了调用方法以外的业务逻辑,只需要在其对应的应用服务类中实现即可,如果在应用服务层中还可以下沉,那就放到其对应的领域服务中。
至此,三个模块的重构都已经完成,剩下的核心处理就是 公共服务manager包 了。
首先,分析代码中的成分。
比如:StpInterfaceImpl 类,它是Sa-Token权限校验类,里边调用了多个领域的应用服务类,这就是一个公共服务。
由图片中的指引,观察到,manager.upload 包下的类,最多只是用到了基础设施层的代码,不涉及领域层和应用服务层的逻辑,所以可以放到基础设施层中。
当然也可以放到common里而不必新建一个manager包,但是分开放更有区分度,因为common包下的类基本是没有业务逻辑的,而manager还是有一些业务逻辑的,分开发区分度更好一些。
最后剩下的 manager.sharding 包,存放的是分库分表逻辑。可以放到空间应用服务层中,但是归根究底地说,分库分表最根本还是针对图片进行的,所以空间还是图片就存在歧义,于是我们不如把该分库分表包放到公共服务里。
最后的最后,只剩下一些闲杂的东西了,按门分类地放到对应的层即可。
然后由于路径的改变,需要全局搜索一下原项目根目录名,接着替换为新项目路径。
至此,整个云图库项目的DDD领域驱动设计重构就完成了。
可以看一下结构对比:
和传统开发目录结构对比,业务实现更清晰,但是我大概率还是会选择传统开发结构。
因为首先ddd设计的话,前期的设计成本是要远高于传统结构的;其次分包太多也相对比较麻烦,而且强行多分一层,对于有些业务情况不一定就比直接写在service中要简单,如果是为了分而分就没有必要了,一开始就已经指出了不要为了用ddd去用,而是要为了解决问题而去用。
通过对原项目进行整个的DDD重构,掌握DDD重构的思想和方法即可,在实际情况中是否要适用这种设计方式进行开发还是要针对实际问题灵活使用。
话又说回来,就算是用传统的分层架构开发,难道就不能将ddd思想应用在其中吗?答案当然是可以的,因为DDD是一种思想,甚至现在都没有一个标准明确的架构规范,我们可以做到比如将controller层中的逻辑精简到和ddd重构时的一样,将逻辑在service中实现后调用,那么这样也算是DDD思想的一种灵活运用。所以不管是什么设计、怎么开发,灵活和逻辑清晰是最重要的。
重构完之后启动项目,不会报编译错误,但是前后端联调会报错(也就是开始登陆的时候)。------- 这个是因为我们的登录态是存储在 redis 中的,其中存储了一个包名(完整带目录的),但是由于我们架构修改了其原位置,所以登录态找不到对应的实体类的包名,所以将redis里的数据清掉就可以了。
本文来自博客园,作者:sevenShaw,转载请注明原文链接:https://www.cnblogs.com/sevenShaw/p/18805391

浙公网安备 33010602011771号