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,所以关联关系是单向的,应用层依赖领域层。
另一方面,主动适配器的仓库实现也有虚线箭头指向仓库接口,代表着仓库实现关系,适配器也依赖于领域层。从而顺利解决层间依赖关系。
面向过程的三层架构直接是在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 表示对象之间的关联
- 领域对象有自己的领域服务
- 在以上前提下利用封装和继承