Show Me Code——领域驱动设计中的代码实现

1,039 阅读8分钟

1. 初步填充分层架构实现

分层架构设计后我们确定了代码实现的层间关系,现在就先以布控任务这个领域模型为例填充下我们的代码骨架。代码已托管至Gitee

1.1 领域对象实现

二话不说先把领域对象连人带椅子搬上来,在domain层创建ControlTask。主键外键、审计字段、关键属性悉数登场。

 public class ControlTask {
 ​
     private Long id;
     private Long userId;
     private String name;
     private String target;
     private LocalDateTime startAt;
     private LocalDateTime endAt;
     private LocalDateTime createdAt;
     private Long createdBy;
     private LocalDateTime lastUpdatedAt;
     private Long lastUpdatedBy;
 ​
     public ControlTask() {
     }
     
     // Getter and Setter
 }

然后可以按照类间调用时序或者层间依赖逐个填充各层实现。

1.2 被动适配器实现

作为被动触发器,需要http端点接收调用请求,依赖应用层的应用服务。通过Dto(Data Transfer Object)和领域对象做数据转换。

 @RestController
 public class ControlResource {
 ​
     private ControlService controlService;
 ​
     @Autowired
     public ControlResource(ControlService controlService) {
         this.controlService = controlService;
     }
 ​
     @PostMapping("api/controls")
     public ControlDto addControl(@RequestBody ControlDto request) {
         // Get UserId
         return controlService.addControl(request, userId);
     }
 }

1.3 数据传输对象与应用服务实现

实现过程始终要注意不坏层间依赖关系。涉及对象转换这里采用的方法是将ControlDto直接放到应用层。

应用服务作为领域层的门面,负责整合领域层的业务逻辑,封装成更粗粒度的数据对象。

这里应用服务需要做的事情具有代表性——带有业务规则的校验请求参数、创建领域对象、持久化、封装数据传输对象返回。从而先形成对领域对象和仓库的依赖。

 @Service
 public class ControlService {
 ​
     private final ControlValidator controlValidator;
     private final ControlRepository controlRepository;
 ​
     @Autowired
     public ControlService(
             ControlValidator controlValidator,
             ControlRepository controlRepository
     ) {
         this.controlValidator = controlValidator;
         this.controlRepository = controlRepository;
     }
 ​
     public ControlDto addControl(ControlDto request, Long userId) {
         controlValidator.validate(request);
         ControlTask controlTask = buildControlTask(request, userId);
         controlTask = controlRepository.save(controlTask);
         return buildControlDto(controlTask);
     }
 ​
     private ControlDto buildControlDto(ControlTask controlTask) {
         // todo
     }
 ​
     private ControlTask buildControlTask(ControlDto request, Long userId) {
         // todo
     }
 }

1.4 领域服务实现

至于业务规则的参数校验,可以进一步抽象为领域服务(Domain Service),划拨到领域层。这个过程对应一种DDD设计模式——表意接口(Intention-Revealing Interfaces),是DDD特别重视代码命名反映领域知识,与统一语言保持一致的一种实践方法。

通过布控任务校验器ControlValidator统一封装请求对象的业务规则校验,比如这里对布控时间段以及布控任务名称的校验。

 @Component
 public class ControlValidator {
 ​
     private final InputTextValidator inputTextValidator;
     private final DateTimeValidator dateTimeValidator;
 ​
     @Autowired
     public ControlValidator(InputTextValidator inputTextValidator, DateTimeValidator dateTimeValidator) {
         this.inputTextValidator = inputTextValidator;
         this.dateTimeValidator = dateTimeValidator;
     }
 ​
     public void validate(ControlDto request) {
         inputTextValidator.defaultInputShouldNotLongerThan(request.getName());
         dateTimeValidator.startTimeShouldEarly(request.getStartAt(), request.getEndAt());
     }
 }

这里留一个隐患,Validator位于领域层,ControDto位于应用层,validate导致层间依赖关系又被破坏了,我们将在下一章节彻底处理掉。

补充一下应用层与领域层内的业务逻辑归属,在应用层和领域层都会处理业务逻辑,区别是只能由研发自己了解的放在应用层,需要和领域专家共同探讨的放在领域层,领域对象是不能包含只有研发自己才懂的内容。

1.5 主动适配器实现

再回到应用服务上,需要持久化领域对象。在分层架构设计中我们重点介绍了引入DIP保证层间依赖关系。

我们直接给出依赖倒置后的类图效果。

应用服务ControlService到仓库接口有三个箭头,代表着ControService使用了这三个接口定义的属性,能够通过属性导航到仓库,但是仓库没有属性能够导航到ControlService,所以关联关系是单向的,应用层依赖领域层。

另一方面,主动适配器的仓库实现也有虚线箭头指向仓库接口,代表着仓库实现关系,适配器也依赖于领域层。从而顺利解决层间依赖关系。

DIP倒置主动适配器依赖举例.png

面向过程的三层架构直接是在service中依赖repository,这里只需要把原来的仓库接口变为仓库的实现类,抽离出的接口移入领域层。就像这样,ControlRepositoryJdbc就是原来的仓库接口,变为了仓库实现。

 @Repository
 public class ControlRepositoryJdbc implements ControlRepository {
 ​
     @Override
     public ControlTask save(ControlTask controlTask) {
         // todo
     }
 }

ControlRepository移入到了领域层,领域服务的依赖还在领域层,被动适配器作为仓库接口的实现还在适配器层,并没有像原来一样依赖领域层或应用层,从而保证了层间依赖关系的稳定。

 public interface ControlRepository {
 ​
     ControlTask save(ControlTask controlTask);
 }

1.6 代码结构

经过这么一番折腾,布控任务管理的领域模块基本建成,代码结构是这样的:

 |-- adapter
     |-- driven
         |-- restful
             |-- ControlResource.java
     |-- driving
         |-- persistence
             |-- ControlRepositoryJdbc.java
 |-- application
     |-- controlmng
         |-- ControlService.java
         |-- ControlDto.java
 |-- domain
     |-- controlmng
         |-- ControlTask.java
         |-- ControlValidator.java
         |-- ControlRepository.java
 |-- SafetymonitorApplication.java

2.领域对象创建规则复杂度降解

我们不难发现,领域对象的创建逻辑也会领域层的一部分。简单创建可以直接在构造器完成,但是对于复杂创建就需要考虑解耦保证领域对象的简洁与聚焦,降低领域对象的创建复杂度。

创建逻辑复杂包括规则复杂与结构复杂,上文领域服务的抽取就是应对规则复杂的解决办法,更准确的说是校验规则复杂。后面的文章我们还会碰到结构复杂的情况。这里就规则复杂看下常用降解复杂度的方法。

2.1 表意接口与领域服务

拿到领域模型后我们创建实体,对照业务规则表写了一大片校验逻辑,这些校验逻辑互相依赖较少,而且会涉及访问仓库接口的情况。

如何重构呢,首先提取函数,可以通过现代IDE的快捷键迅速完成,比如说IDEA的Extract Method功能(Alt+Shift+M)。方法与变量命名要能体现领域知识,也就是表意接口,比如verify,startTimeShouldEarly等。

接着,将这些方法移动到领域层成为各个校验器如DateTimeValidator,通过校验器ControlValidator组合其他校验器,ControlValidator也就是领域服务,应用服务从耦合校验逻辑变为依赖领域服务。

布控模块业务逻辑的代码结构变成了这样:

 |-- application
     |-- controlmng
         |-- ControlService.java
         |-- ControlDto.java
 |-- domain
     |-- controlmng
         |-- validator
             |-- DateTimeValidator.java
             |-- InputTextValidator.java
         |-- ControlValidator.java
         |-- ControlTask.java
         |-- ControlRepository.java

2.2 工厂模式

对于领域对象的创建逻辑,DDD提供了工厂(Factory)模式来聚焦。从而达成一种隐喻(metaphor)——工厂创造产品然后存入仓库。以及上一章节留下的一个坑,领域服务依赖了应用层的数据传输对象,解决办法就是采用Builder模式。

首先,创建ControlBuilder,接管并替换掉原来的领域服务ControlValidator,将规则校验以及领域对象创建封装起来。

 public class ControlBuilder {
 ​
     private final InputTextValidator inputTextValidator;
     private final DateTimeValidator dateTimeValidator;
 ​
     private Long userId;
     private String name;
     private String target;
     private LocalDateTime startAt;
     private LocalDateTime endAt;
     private Long createdBy;
 ​
     @Autowired
     public ControlBuilder(InputTextValidator inputTextValidator, DateTimeValidator dateTimeValidator) {
         this.inputTextValidator = inputTextValidator;
         this.dateTimeValidator = dateTimeValidator;
     }
 ​
     public ControlBuilder userId(Long userId) {
         this.userId = userId;
         return this;
     }
 ​
     // 省略其他build属性
 ​
     public ControlTask build() {
         validate();
 ​
         ControlTask controlTask = new ControlTask();
         controlTask.setUserId(this.userId);
         controlTask.setName(this.name);
         controlTask.setTarget(this.target);
         controlTask.setStartAt(this.startAt);
         controlTask.setEndAt(this.endAt);
         controlTask.setCreatedBy(this.createdBy);
         controlTask.setCreatedAt(LocalDateTime.now());
 ​
         return controlTask;
     }
 ​
     private void validate() {
         inputTextValidator.defaultInputShouldNotLongerThan(this.name);
         dateTimeValidator.startTimeShouldEarly(this.startAt, this.endAt);
     }
 }

然后,创建布控任务领域对象Builder工厂ControlBuilderFactory生产Builder。

注意因为创建ControlBuilder有可变属性,所以不能单例注入到ControlService中(没有使用@Component注解),可以使用Spring的prototype原型模式,或是就像这样直接new一个新的ControlBuilder对象。

 @Component
 public class ControlBuilderFactory {
 ​
     private final InputTextValidator inputTextValidator;
     private final DateTimeValidator dateTimeValidator;
 ​
     @Autowired
     public ControlBuilderFactory(InputTextValidator inputTextValidator, DateTimeValidator dateTimeValidator) {
         this.inputTextValidator = inputTextValidator;
         this.dateTimeValidator = dateTimeValidator;
     }
 ​
     public ControlBuilder create() {
         return new ControlBuilder(inputTextValidator, dateTimeValidator);
     }
 }

最后,再来看下我们布控任务领域模型的门面——应用服务ControlService。

属性依赖只有工厂与仓库,是不就能和上面的隐喻对上了,代码整洁边界清晰。

通过Builder模式,创建领域对象也变得直观,减少了参数顺序不对导致参数错误的失误。

 @Service
 public class ControlService {
 ​
     private final ControlBuilderFactory controlBuilderFactory;
     private final ControlRepository controlRepository;
 ​
     @Autowired
     public ControlService(
             ControlBuilderFactory controlBuilderFactory,
             ControlRepository controlRepository
     ) {
         this.controlBuilderFactory = controlBuilderFactory;
         this.controlRepository = controlRepository;
     }
 ​
     public ControlDto addControl(ControlDto request, Long userId) {
         ControlBuilder controlBuilder = controlBuilderFactory.create();
         ControlTask controlTask = controlBuilder.userId(userId)
                 .name(request.getName())
                 .target(request.getTarget())
                 .startAt(request.getStartAt())
                 .endAt(request.getEndAt())
                 .createdBy(userId)
                 .build();
 ​
         controlTask = controlRepository.save(controlTask);
         return buildControlDto(controlTask);
     }
 ​
     private ControlDto buildControlDto(ControlTask controlTask) {
         // todo
     }
 }

3.面向对象还是面向过程

经典的编程范式大家讨论了很多年,究竟是面向对象还是面向过程,通过老马的著书经历可见一二。

在OO刚问世的时候,桌面软件开发盛行,比如Microsoft Office套件。开发特点是软件运行的所有的数据都可以装入内存,对象之间的导航自由;而在J2EE诞生时,企业应用开发逐渐占据市场主导地位,应用数据日益庞大,数据库技术得到大力发展,每次只能取出很小一部分放入内存,对象之间是没法自由导航的,这也就造成了OO很难在企业应用中使用,贫血模型泛滥。

老马的《重构》也在二十多年后的第二版中将实现语言变为了工程化没那么强的JavaScript,由面向对象的原教旨主义者变成了编程范式的中立主义者。也就是说对于编程范式而言,不拘泥于面向过程、面向对象、面向方面等等,而是将他们结合起来——面向多种编程范式编程

这也是钟敬老师带来的启发,最后,给出钟敬老师的代码风格规则:

  • 领域对象不访问数据库
  • 领域服务只能读数据库
  • 应用服务可以读写数据库
  • 用 ID 表示对象之间的关联
  • 领域对象有自己的领域服务
  • 在以上前提下利用封装和继承

参考