本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
项目结构与模块化构建思路
RESTful与API设计&管理
网关路由模块化支持与条件配置
DDD领域驱动设计与业务模块化(概念与理解)
DDD领域驱动设计与业务模块化(落地与实现)
DDD领域驱动设计与业务模块化(薛定谔模型)
DDD领域驱动设计与业务模块化(优化与重构)
RPC模块化设计与分布式事务
v2.0:项目结构优化升级
v2.0:项目构建+代码生成「插件篇」
v2.0:扩展模块实现技术解耦
v2.0:结合DDD与MVC的中庸之道(启发与思路)
v2.0:结合DDD与MVC的中庸之道(标准与实现)(本文)
v2.0:结合DDD与MVC的中庸之道(优化与插件)
未完待续......
在之前的文章 v2.0:项目结构优化升级 中,我们将项目的结构进行了优化,优化之后的项目结构如下
简单说明
考虑到可能有读者是第一次看这个专栏,所以我还是先简单介绍一下,详细的内容大家可以看我之前的专栏文章
这里模块化的设想其实就是可插拔的功能模块,如果需要这个功能,就把这个模块用Gradle
或Maven
的方式编译进来,如果不需要,去掉对应的依赖就行了,避免改动代码,因为一旦涉及到代码改动的话,就会变得“改不断,理还乱”
当然了,需要达到每个模块都能够任意拆卸的程度其实并不简单,所以我借鉴了DDD
的思想来达成这个目的,专栏中也有这一块的内容,今天这篇文章就是对DDD
的进一步优化
当我们把所有模块都编译进来的时候,那么就是一个单体应用,如果把多个模块分开编译,那么就变成了微服务
以我们juejin
项目的三个模块(用户,沸点,通知)为例:
在最开始的时候,我们可以将这三个模块打包成一个服务,作为单体应用来运行,适合项目前期体量较小的情况
之后,当我们发现沸点的流量越来越大,就可以将沸点模块拆分出来作为单独的一个服务方便扩容,用户模块和通知模块打包在一起作为另一个服务,这样就从单体应用变成了微服务
注:从单体应用到微服务的切换是不需要修改代码的
标准
基于之前对于DDD
的研究和铺垫
我决定将DDD
的实现进行标准化
也就是编写一些类来规范DDD
的结构和用法
在这个基础上我们不需要再从0开始实现概念
而是会像其他的框架一样通过实现具体的接口来落地
不过这里需要提前说明:
整个规范的落地是以我自己的思路来实现的
所以可能会存在对于概念上的理解差异
以及我其实算是结合了DDD
和MVC
来对项目进行优化
而非纯粹的DDD
实践
使用
只需要引用这个库就可以基于规范开发了
implementation 'com.github.linyuzai:concept-domain-spring-boot-starter:2.0.2'
//使用 mybatisplus 才需要引入
implementation 'com.github.linyuzai:concept-domain-mbp:2.0.2'
com.github.linyuzai
concept-domain-spring-boot-starter
2.0.2
com.github.linyuzai
concept-domain-mbp
2.0.2
下面来讲讲里面的类是怎么设计的
领域对象
首先最重要的就是我们的领域对象的定义
//用于提供 id
public interface Identifiable {
String getId();
}
//领域对象
public interface DomainObject extends Identifiable {
}
//领域实体
public interface DomainEntity extends DomainObject {
}
//值对象
public interface DomainValue extends DomainObject {
}
//领域模型集合
public interface DomainCollection extends DomainObject {
@Override
default String getId() {
throw new UnsupportedOperationException();
}
//根据 id 查询领域模型
T get(String id);
//是否存在对应的 id
boolean contains(String id);
//列表查询
List list();
//流式查询
Stream stream();
//数量
Long count();
}
Identifiable
为什么要单独抽象出来呢
主要是我们的领域对象需要和数据对象通过id
来关联,后面马上就会遇到了
然后就是领域集合DomainCollection
表示多个领域对象
这里不用List
而另外定义接口也是方便我们通过动态代理来进行懒加载
当我们需要这部分数据的时候才会查询数据库,减少不必要的性能消耗
其实动态代理List
也是可以的,但是自定义的接口方便扩展其他的方法
用户模型
我们可以这样实现用户模型
//用户
public interface User extends DomainEntity {
//用户名
String getUsername();
//密码
String getPassword();
//昵称
String getNickname();
//创建时间
Date getCreateTime();
}
//用户集合
public interface Users extends DomainCollection {
}
这里和普遍的DDD
领域对象实现不太一样,用的是接口,主要也是为了方便延时加载数据
生成领域对象
毕竟我们都是通过领域对象来进行业务处理的
肯定是要先生成领域对象
Builder模式
Builder
模式的好处是能够在build
的时候进行字段校验
这样的话只要是实例化出来的领域对象就一定是符合业务要求的
不需要进行无意义的判断,如是否为空等(如果业务本身允许为空那还是要进行业务判断的)
所以我们可以定义如下接口
//领域校验器
public interface DomainValidator {
//校验
void validate(Object target);
}
//领域模型 Builder
public interface DomainBuilder {
T build(DomainValidator validator);
}
//领域模型 Builder 抽象类
public abstract class AbstractDomainBuilder implements DomainBuilder {
@Override
public T build(DomainValidator validator) {
init();
validate(validator);
return build();
}
//校验
protected void validate(DomainValidator validator) {
if (validator == null) {
throw new DomainException("DomainValidator is null");
}
validator.validate(this);
}
//初始化一些默认值
protected void init() {
}
//需要子类实现的 build 流程
protected abstract T build();
}
DomainValidator
用于自定义校验逻辑
我们可以直接用Spring
自带的校验来实现
//基于 Spring 的校验器
@Getter
@RequiredArgsConstructor
public class ApplicationDomainValidator implements DomainValidator {
//org.springframework.validation.Validator
private final Validator validator;
@Override
public void validate(Object target) {
BindingResult bindingResult = createBindingResult(target);
validator.validate(target, bindingResult);
onBindingResult(target, bindingResult);
}
//创建一个绑定结果容器
protected BindingResult createBindingResult(Object target) {
return new DirectFieldBindingResult(target, target.getClass().getSimpleName());
}
//处理绑定结果
protected void onBindingResult(Object target, BindingResult bindingResult) {
if (bindingResult.hasFieldErrors()) {
FieldError error = Objects.requireNonNull(bindingResult.getFieldError());
String s = target.getClass().getName() + "#" + error.getField();
throw new IllegalArgumentException(s + ", " + error.getDefaultMessage());
}
}
}
这样就可以和Spring
的校验无缝衔接了
接下来是User
模型的实现
//用户实现
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class UserImpl implements User {
//用户ID
protected String id;
//用户名
protected String username;
//密码
protected String password;
//昵称
protected String nickname;
//创建时间
protected Date createTime;
public static class Builder extends AbstractDomainBuilder {
@NotEmpty
protected String id;
@NotEmpty
protected String username;
@NotNull
protected String password;
@NotEmpty
protected String nickname;
@NotNull
protected Date createTime;
public Builder id(String id) {
this.id = id;
return this;
}
public Builder username(String username) {
this.username = username;
return this;
}
public Builder password(String password) {
this.password = password;
return this;
}
public Builder nickname(String nickname) {
this.nickname = nickname;
return this;
}
public Builder createTime(Date createTime) {
this.createTime = createTime;
return this;
}
@Override
protected void init() {
if (createTime == null) {
createTime = new Date();
}
}
@Override
protected UserImpl build() {
return new UserImpl(
id,
username,
password,
nickname,
createTime);
}
}
}
现在我们就可以通过下面的代码来实例化一个用户模型了
User user = new UserImpl.Builder()
.id(id)
.username(username)
.password(password)
.nickname(nickname)
.build(validator);
Proxy模式
当我们需要让数据延迟加载的情况下
动态代理就是一个很好的选择
定义一个领域工厂
//领域工厂
public interface DomainFactory {
//创建指定 id 的领域对象
T createObject(Class cls, String id);
//创建延迟调用的领域对象
T createObject(Class cls, Supplier supplier);
//创建在指定集合范围内的指定 id 的领域对象
T createObject(Class cls, DomainCollection collection, String id);
//创建指定 ids 的领域集合