每一种架构都是为了解决实际工程中的问题,就像设计模式看起来什么用都没有,但是其实是解决了现实工程中遇到的各种问题,主要是为了降低代码的维护与修改的代价,而这个 DDD 个人认为也是这个作用
这是 DDD 最与众不同的地方
对实体类的属性进行显示化,我们一般对有限制的数据类型以及复杂的数据类型使用 DP,但是使用 DP 会增加代码膨胀,除去 PO、DTO 转化为 DP 的成本,在使用到属性的时候还需要进行转换
数据验证和错误处理:每个入参都需要方法校验,就算前端已经校验过了,后端为了程序健壮性以及规范,还是要校验一次。虽然现在可以用注解来简化校验过程,但是还有一些需要业务校验的情况在代码中经常出现,在每个方法里这段校验逻辑还是会被重复
在需要新增校验规则与维护原来的校验规则时,会比较麻烦,有没有一种方法,能够一劳永逸的解决所有校验的问题以及降低后续的维护成本和异常处理成本呢?
大量的工具类:问题时从一些入参里抽取一部分数据,然后调用一个外部依赖获取更多的数据,然后通常从新的数据中再抽取部分数据用作其他的作用。这种代码通常被称作“胶水代码”,其本质是由于外部依赖的服务的入参并不符合我们原始的入参导致的。为了解决这个问题,一个常见的办法是将这段代码抽离出来,变成独立的一个或多个方法
可测试性:假如一个方法有 N 个参数,每个参数有 M 个校验逻辑,至少要有 N * M 个 TC,要如何降低测试成本呢?
将隐性的概念显性化:比如下面例子
public class TestEntity {
private String username;
private String email;
}
原来 username 仅仅是TestEntityPo 的一个参数,属于隐形概念,如果此时 username 参与了真正的业务逻辑,为了减少维护成本我们需要将username的概念显性化
public class UserName {
@NotNull
String name;
public String getName() {
return name;
}
public UserName (String name) {
if (name == null) {
throw new ValidationException("number不能为空");
}
this.name = name;
}
public isEnglish() {
// 业务校验逻辑
...
}
public isUser() {
// 业务校验逻辑
...
}
}
我们将之前的 username 写成一个类,此时:
像这种类被称作 TYPE,可以在 entity 中组合 type,它们一起被称作 DP,一般在业务层使用 DP
public class TestEntity {
private UserName username;
private Email email;
}
如果忽略应用内部的架构设计,很容易导致代码逻辑混乱,很难维护,容易产生bug而且很难发现
一个应用最大的成本一般都不是来自于开发阶段,而是应用整个生命周期的总维护成本,所以代码的可维护性代表了最终成本,强依赖其他三方组件与基层数据库的脚本式代码通常可维护性能差,它可能出现以下几个问题
事务脚本式代码的第二大缺陷是:虽然写单个用例的代码非常高效简单,但是当用例多起来时,其扩展性会变得越来越差。
可扩展性减少做新需求或改逻辑时,需要新增/修改多少代码
设计模式六大原则给了我们不错的解决思路,依赖与抽象而不依赖与具体。调用每一个三方时都使用接口或者加防腐层,调用每一个底层组件时都使用抽象,同时按逻辑分离代码操作,使代码复用性增加
原来的 MVC 架构非常容易理解,从 Controller 层接受前端数据,Service 层做处理,而数据层则对应数据库。一般来说 Controller 依赖 Service 层,而 Service 层依赖的东西过多,有可能是第三方组件与接口,比如消息队列、Dubbo 调用等;又有可能是数据层的东西,我们的惯性思维就是在 Service 层从数据库中取出数据的,此时数据库可能是 MySQL、PG、redis 等等
无论如何,这些三方与数据库都是有可能变动的,其他公司的烂代码可能会腐蚀我们自己写的代码,而数据库的表也可能会增减字段,此时牵一发而动全身。说了这么多,DDD 到底是如何解决这些问题的呢?
DDD 最直观的体现就是模块名跟 MVC 不一样,领域驱动设计的四层结构为:
设计人员可以根据实际问题填充不同的模块到这四层中,填充的原则如下:
Web 模块包含 Controller 等相关代码,同时在该模块中可以加入 VO,Param 等,该层即为协议层
我们单独会抽取出来 Interface 接口层,作为所有对外的门户,将网络协议和业务逻辑解耦,该层需要做以下这些事情:
这些大多都不都是用拦截器做的吗。而且,虽然说 App 层是组合下层的模块,但是一些非常简单的需求只需要查询一次数据库即可,我们也允许在 Web 层直接调用 Mapper
主要包含 Application Service,该模块依赖 Domain 模块与基础设施层。Application 层主要职责为组装 domain 层各个组件,完成具体的业务服务。Application 层可以理解为粘合各个组件的胶水,使得零散的组件组合在一起提供完整的业务服务
Application Service 是业务流程的封装,不处理业务逻辑
并且,ApplicationService 应该永远返回 DTO 而不是 Entity
举个例子,查询数据库的方法调用应该在该层做,而数据库接口的定义应该在 Domain 层,基础层则是实现查询的地方
该层的出参应该是标准的 DTO,并且不应该做任何逻辑处理,而入参则是 CQE 对象,一般入参的校验应该在这一层,这样就保证了非业务代码不会混杂在业务代码之间
注意此处的校验与上层 web 层的校验不一样,web 层主要用来校验不需要访问 dao 层即可校验的数据,比如权限,入参是否为 null 等,而 app 层的校验则是需要访问数据库来获取数据的情况。但是还有一个问题,就是在 app 层校验时,校验结果如何返回给用户,此处推荐增加一个 OperateResult 来返回操作结果,举个例子:
OperateResult:
/**
* description 操作结果,主要用于单纯操作,记录操作日志
*/
@Data
public class OperateResult implements Serializable {
private boolean ret;
private String msg;
public static OperateResult success() {
OperateResult operateResult = new OperateResult();
operateResult.setRet(true);
return operateResult;
}
public static OperateResult error(String msg) {
OperateResult operateResult = new OperateResult();
operateResult.setRet(false);
operateResult.setMsg(msg);
return operateResult;
}
}
web 层代码:
@PostMapping("/transfer.json")
public JsonResult<String> transfer(String clueId) {
OperateResult opt = salesClueService.transfer(clueId);
if (opt.isRet()) {
return JsonResult.success("成功");
}
return JsonResult.error(opt.getMsg());
}
app 层/service 层:
@Override
public OperateResult transfer(String clueId) {
SalesClue salesClue = salesClueRouteService.selectById(clueId);
if (salesClue == null) {
return OperateResult.error("未查询到线索");
}
return OperateResult.success();
}
Application层的几个核心类:

判断是否业务流程的几个点:
业务核心模块,包含有状态的 Entity、领域服务 Domain Service、Types、以及各种外部依赖的接口类,注意,只是接口类
有状态的 Entity 指对应原来 MVC 中的 DO,只不过加入了对 DO 中属性的一些操作;Types 包的作用就是前文说的作用;Domain Service 则是核心的复合操作
外部依赖的接口类包括 Repository、ACL、中间件等,我们把这些打包成接口,使用 LSP、策略模式等做防腐,增加扩展性等等。这些接口对应到 MVC 中,就是 Service 层的接口
领域服务不依赖任何其他功能,只做最纯粹的算法操作,这里面不应该有任何依赖注入的情况(当然因为这个模块不依赖任何模块,也无法注入其他东西)
包含数据库 DAO 的实现,包含 PO、ORM Mapper、Entity 到 PO 的转化类等;包含 Service,不过没有业务代码,都是一些类似日志什么的 Service 操作;包含要具体依赖的 ORM 类库配置,比如 MyBatis
对于三方或者数据库的具体实现可以使用转换器模式
// 代码在Infrastructure层
@Repository // Spring的注解
public class OrderRepositoryImpl implements OrderRepository {
private final OrderDAO dao; // 具体的DAO接口
private final OrderDataConverter converter; // 转化器
public OrderRepositoryImpl(OrderDAO dao) {
this.dao = dao;
this.converter = OrderDataConverter.INSTANCE;
}
}

可以看到,该模块主要负责提供数据的来源以及储存数据,但是一些配置信息也需要放在这里
如果觉得 PO 太过麻烦,可以直接忽略它,将 DO 作为该层的传入传出以及层次内部的传输对象,DDD 是一种设计理念,没有规定具体需要如何实现
综上所述,考虑到最终的依赖关系,我们在写代码的时候可能先写 Domain 层的业务逻辑,然后再写 Application 层的组件编排,最后才写每个外部依赖的具体实现。这种架构思路和代码组织结构就叫做 Domain-Driven Design(领域驱动设计,或DDD)
模型对象代码规范其实只有3种模型,Entity、Data Object (DO) 和 Data Transfer Object (DTO),不过思路都是类似的,先来看看包括了大众理解的模型

在实际开发中 DO、Entity 和 DTO 不一定是1:1:1的关系,一个 Entity 应该可以对应多个 DO,应该 DTO 又可以对应多个 Entity

综上,DDD 的包架构应该是下面这样

但是在 DDD 开发的过程中会有很多障碍,比如 mybtais 几乎不能满足 DDD 架构的需求,如果强制在 domain 层定义接口,代码会变的十分臃肿。同时市面上大多数 ORM 框架也有这个缺点
其次,type 的想法虽好,但是就算有了 mapper 的帮助也无法很好的转换代码,过程往往变的更加复杂
但是其思想是值得我们借鉴学习的,领域驱动设计的理论甚至可以用在非模块开发上
mvc 架构我们平时用的就比较多了
CRUD 是指在做计算处理时的增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写。主要被用在描述软件系统中 DataBase 或者持久层的基本操作功能。对应这里的 crud 方法的命名,每个人有不同的实践
个人理解 dao 层是数据库的映射,因此这里的方法应当对应与数据库的关键字或者函数
而 Service 接口命名应当偏向业务,与单纯的查询分离开来
而 Dao 层,应当只负责接收最终的 sql 语句,具体到某一张表的增删查改
Service 层也不是就非有不可,对于极小的项目而言,加了 Service 层,反而增加了代码量,而且 Dao 层中已经预见了可能出现的情况,并进行了相应的扩展。那么,此时就不需要了