每一种架构都是为了解决实际工程中的问题,就像设计模式看起来什么用都没有,但是其实是解决了现实工程中遇到的各种问题,主要是为了降低代码的维护与修改的代价,而这个 DDD 个人认为也是这个作用
对实体类的属性进行显示化
数据验证和错误处理:每个入参都需要方法校验,就算前端已经校验过了,后端为了程序健壮性以及规范,还是要校验一次。虽然现在可以用注解来简化校验过程,但是还有一些需要业务校验的情况在代码中经常出现,在每个方法里这段校验逻辑还是会被重复
在需要新增校验规则与维护原来的校验规则时,会比较麻烦,有没有一种方法,能够一劳永逸的解决所有校验的问题以及降低后续的维护成本和异常处理成本呢?
大量的工具类:问题时从一些入参里抽取一部分数据,然后调用一个外部依赖获取更多的数据,然后通常从新的数据中再抽取部分数据用作其他的作用。这种代码通常被称作“胶水代码”,其本质是由于外部依赖的服务的入参并不符合我们原始的入参导致的。为了解决这个问题,一个常见的办法是将这段代码抽离出来,变成独立的一个或多个方法
可测试性:假如一个方法有 N 个参数,每个参数有 M 个校验逻辑,至少要有 N * M 个 TC,要如何降低测试成本呢?
将隐性的概念显性化:比如下面例子
public class TestEntityPo {
private String username;
private String email;
}
原来 username 仅仅是TestEntityPo 的一个参数,属于隐形概念,如果此时 username 参与了真正的业务逻辑,为了减少维护成本我们需要将username的概念显性化
public class UserName {
@NotNull
String str;
public String getName() {
return str;
}
public isEng() {
// 判断逻辑
...
}
}
我们将之前的username写成一个类,此时:
校验逻辑都放在了 UserName 里面,确保只要该类被创建出来后,一定是校验通过的,数据验证和错误处理都在类中处理
只对该属性操作的方法变成了 UserName 类里的方法
刨除了数据验证代码、胶水代码,在业务层剩下的都是核心业务逻辑
如果忽略应用内部的架构设计,很容易导致代码逻辑混乱,很难维护,容易产生bug而且很难发现
一个应用最大的成本一般都不是来自于开发阶段,而是应用整个生命周期的总维护成本,所以代码的可维护性代表了最终成本,强依赖其他三方组件与基层数据库的脚本式代码通常可维护性能差,它可能出现以下几个问题
事务脚本式代码的第二大缺陷是:虽然写单个用例的代码非常高效简单,但是当用例多起来时,其扩展性会变得越来越差。
可扩展性减少做新需求或改逻辑时,需要新增/修改多少代码
设计模式六大原则给了我们不错的解决思路,依赖与抽象而不依赖与具体。调用每一个三方时都使用接口或者加防腐层,调用每一个底层组件时都使用抽象,同时按逻辑分离代码操作,使代码复用性增加
DDD 最直观的体现就是包名跟 MVC 不一样,领域驱动设计的四层结构为:
设计人员可以根据实际问题填充不同的模块到这四层中,填充的原则如下:
Web模块包含Controller等相关代码,同时在该模块中可以加入 VO,Param 等,该层即为协议层
我们单独会抽取出来 Interface 接口层,作为所有对外的门户,将网络协议和业务逻辑解耦,该层需要做以下这些事情:
主要包含 Application Service,该模块依赖 Domain 模块。Application 层主要职责为组装 domain 层各个组件及基础设施层的公共组件,完成具体的业务服务。Application 层可以理解为粘合各个组件的胶水,使得零散的组件组合在一起提供完整的业务服务
该层的出参应该是标准的 DTO,并且不应该做任何逻辑处理,而入参则是 CQE 对象,一般入参的校验应该在这一层,这样就保证了非业务代码不会混杂在业务代码之间
判断是否业务流程的几个点:
业务核心模块,包含有状态的 Entity、领域服务 Domain Service、Types、以及各种外部依赖的接口类
有状态的 Entity 指对应原来 MVC 中的 DO,只不过加入了对 DO 中属性的一些操作;Types 包下装了前文说的 DP;Domain Service则是多个 Entity 的复合操作
包含数据库 DAO 的实现,包含 PO、ORM Mapper、Entity 到 PO 的转化类等;包含 Service,不过没有业务代码,都是一些类似日志什么的 Service 操作;包含要具体依赖的 ORM 类库配置,比如 MyBatis
模型对象代码规范其实只有3种模型,Entity、Data Object (DO)和Data Transfer Object (DTO),不过思路都是类似的,先来看看包括了大众理解的模型
在实际开发中DO、Entity 和 DTO 不一定是1:1:1的关系,一个 Entity 应该可以对应多个 DO,应该 DTO 又可以对应多个 Entity
@mapper 注解可以直接作用与接口上,实现模型之间的转换,默认让对象中相同名称的属性相互转换,入参为需要转换的对象,返回值为被转换的对象,同时,list也可以进行转换
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
UserVo convert(UserPo userPo);
UserPo convert(UserVo userVo);
List<UserPo> convert(List<UserVo> userVo);
}
如果有不相同名称的属性需要转换,可以加上 @Mapping 注解
@Mapper
public interface UserConvert {
@Mapping(source = "name", target = "userName")
UserVo convert(UserPo userPo);
}
其对应的信息不仅仅来自一个类, 那么, 我们也可以通过配置来实现多到一的转换
@Mapping(target = "periodId", source = "studentDetailsPo.periodId")
@Mapping(target = "studentName", source = "userPo.name")
StudentInfoVo convert(UserPo userPo, StudentDetailsPo studentDetailsPo);
首先应该理清自己负责的系统的关系,画好时序图和类结构图。同时进行数据库的建表与接口的设计,一切以需求为核心
进行参数校验,写代码的过程中额外注意空指针的情况,变量命名,应该从上向下的实现功能