当前位置: 首页 > 科技观察

建筑师必备——DDD的落地实践

时间:2023-03-22 15:52:33 科技观察

大家好,我是北君。今天给大家介绍的是DDD,一个听起来很垃圾但真的很牛逼的设计思路。这对建筑师来说是必须的!前言在日常工作中,大部分接手或维护的项目都采用三层架构,即controller、service、dao。在使用过程中,会遇到很多问题:面向数据建模,面向过程编程,不是真正的“面向对象”,只关注结果,不关注过程。service层动辄几百上千行,全是流程代码,胶水代码,或臃肿,或流水账,或不重复,或逻辑散乱,后期难度极大。维护代码耦合严重。层层相互调用,反向调用,整个代码无法体现业务。如果大家不喜欢写注释,久而久之,代码业务逻辑就会无人化。我明白了,我不敢改变也不能改变。那么有什么好的解决办法吗?今天要讲的DDD是个不错的选择。DDDDDD,领域驱动设计,完美解决了以上问题:面向领域建模,面向对象编程,代码直接映射现实世界的概念,贴近业务,更贴近客户领域逻辑内聚性高,符合Java开发原则和技术细节变更数据库、缓存、定时器等变更对业务逻辑影响相对较小,非常适合插件化架构代码,可读性和可维护性更好,对后续扩展、移植等支持更好,以及更科学的层次。DDD的概念,网上很容易找到,这里就不赘述了。不过,虽然网上DDD的文章很多,但大部分都是理论知识,介绍无非是一些名词:战略设计、战术设计、核心领域、支撑领域、价值对象、实体、聚合……实际执行它对我们来说并不是很重要。多帮忙介绍下我在SpringBoot中应用DDD的落地方案。落地方案一、代码分层Codelayering用户界面层:图中的api包(也就是controller层,我觉得controller后缀太长了。。。)应用层:这里用到了command模式,阅读和写作分为两部分。一个包(command,query),如果不使用command方式可以组合成一个服务包domain层:domainpackage,使用JPA(对DDD的良好支持)infrastructure层:infrapackage,其他通用组件都放在这里,如果使用DIP依赖倒置,那么实现类也放在这里。model模型:模型包,用于存放不同层之间传递的对象。试过把这些对象放在很多地方,最后发现还是放在一个包下比较好(方便服务间调用时共享对象)2.层次关系和模型传递层次和调用关系3.层次详解描述api包(控制器)@Tag(name="user",description="user")@RestController@RequestMapping(value="/api/sys-user")publicclassSysUserApiextendsBaseApi{@ApiResult@Operation(summary="通过ID查询用户")@GetMapping("/{id}")publicSysUserVoget(@PathVariableLongid){returnqueryExecutor.execute(newSysUserByIdQry(id));}@Pagination(total=true)@ApiResult@Operation(summary="分页查询用户")@GetMappingpublicListgetList(SysUserQosysUserQo){returnqueryExecutor.execute(newSysUserListQry(sysUserQo));}@ApiResult@Operation(summary="添加新用户")@PostMappingpublicvoidsave(@Valid@RequestBodySysUserDtosysUserDto){commandExecutor.execute(newSysUserCommonCmd(sysUserDto));}}在BaseApi类中封装了两个命令执行queryExecutor和commandExecutor在调用应用层时可以执行不同的命令,不需要@Autowired引入不同的服务@ApiResult添加这个自定义注解后,返回结果统一封装。@Pagination加上这个自定义注解后,分页参数会自动保存在线程变量中,后面查询的时候会自动获取分页参数。当返回的结果被统一封装后,还会被添加分页信息。Qo为查询参数对象,Dto为增删改查等命令参数对象,返回对象为Vo。这里需要注意的是,Entity一定不能暴露在这一层。需要转换成Vo再返回到这一层。每个方法几乎都是一行语句来执行命令。一般情况下,不执行任何业务逻辑(当然也有特殊情况)。命令包@AllArgsConstructorpublicclassSysDeptAddCmdimplementsCommand{privateSysDeptDtosysDeptDto;@OverridepublicVoidexecute(Executorexecutor){//获取命令的接收者:domainserviceSysDeptManagerreceiver=executor.getReceiver(SysDeptManager.class);//对象模型转换,从DTO到Entity,使用MapStructSysDeptsysDept=SysDeptMapper.INSTANCE.toSysDept(sysDeptDto);//使用JPA保存receiver.save(sysDept);返回空值;}}增删改查命令,一层很薄,作为工作的组织者,几乎没有业务逻辑,调用领域服务和拥挤的对象方法命令方式,实现自定义Command接口,返回值是泛型。通过属性和构造方法接收参数(使用lombok注解)。一个命令中只有一个执行方法。缺点是会产生大量的命令类。一个类相当于上一个服务类中的一个方法,但这符合单一职责的原则。通过executor.getRecerver方法获取领域服务(manager)DTO,永远不会下到领域层。需要先从DTO转换为Entity(这里转换方式使用MapStruct,后面会详细介绍)查询包@AllArgsConstructorpublicclassSysDeptByIdQryextendsCommonQry{privateLongid;@OverridepublicSysDeptVoexecute(Executorexecutor){if(id==null){thrownewBusinessException("部门ID不能为空");}QSysDept系统部门=QSysDept。系统部;返回queryFactory.select(this.fields()).from(sysDept).where(sysDept.deleted.eq(false),sysDept.id.eq(id)).fetchOne();}/***部门VO映射**@returnQBean*/publicstaticQBeanfields(){QSysDeptsysDept=QSysDept.sysDept;返回Projections.fields(SysDeptVo.class,sysDept.deptName,sysDept.orderNum,sysDept.id);}}Command模式,继承自定义CommonQry基类(该类也实现了自定义Command接口,引用QueryDSL的queryFactory类,封装了分页方法),泛型为返回值查询中的查询命令package与命令包中的命令基本相同,唯一的区别是查询命令理论上直接查询数据库,没有调用领域层。由于JPA对于复杂的查询不太好用,这里强烈推荐使用QueryDSL(后面单独介绍)详细),图片是一个简单的使用示例。领域包较多,代码部分不一一列举:数据库表由Entity类自动生成,在设计Entity@Annotations时根据实际业务灵活使用仅Entity类(屏蔽数据库)比如OneToMany,@OneToOne(聚合根的概念)聚合根不要太大,80%的情况一个聚合根只包含一个实体(不要过度设计成大聚合根)不要贫血模型,但要面向对象,属于对象的方法应该放在对象中,但不建议将存储库类引入对象中。需要操作数据库的方法写在域服务管理器中。业务逻辑尽量写在domainservice(manager)中,不断抽取和抽象出不同的方法。应用层调用适当使用领域事件。JPA可以在Entity中使用@DomainEvents注解来发送领域事件。通过DDD对业务的理解更透彻,写出的代码更能传达客户的业务诉求。想写多少就写多少低耦合的代码,符合单一职责、开闭、封装、继承、多态性原则的代码是很惬意的。与传统架构相比,前期代码量更大,开发者前期投入更多:合理划分领域,合理实体设计大量数据对象,如DTO,VO,大量数据对象转换方法,大量命令类……不过,除非是特别简单的功能,否则对于一个中等复杂的系统来说,这些初期的努力还是值得的。一张图展示:总结以上简要介绍了我对DDD的理解和实践,并通过实际代码展示了如何在SpringBoot中应用DDD,希望能给大家提供一个思路