服务端模块化架构设计 2.0|结合DDD与MVC的中庸之道(标准与实现)

2023年 8月 22日 56.4k 0

本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务

项目结构与模块化构建思路

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:项目结构优化升级 中,我们将项目的结构进行了优化,优化之后的项目结构如下

项目结构v2.png

简单说明

考虑到可能有读者是第一次看这个专栏,所以我还是先简单介绍一下,详细的内容大家可以看我之前的专栏文章

这里模块化的设想其实就是可插拔的功能模块,如果需要这个功能,就把这个模块用GradleMaven的方式编译进来,如果不需要,去掉对应的依赖就行了,避免改动代码,因为一旦涉及到代码改动的话,就会变得“改不断,理还乱”

当然了,需要达到每个模块都能够任意拆卸的程度其实并不简单,所以我借鉴了DDD的思想来达成这个目的,专栏中也有这一块的内容,今天这篇文章就是对DDD的进一步优化

当我们把所有模块都编译进来的时候,那么就是一个单体应用,如果把多个模块分开编译,那么就变成了微服务

以我们juejin项目的三个模块(用户,沸点,通知)为例:

在最开始的时候,我们可以将这三个模块打包成一个服务,作为单体应用来运行,适合项目前期体量较小的情况

之后,当我们发现沸点的流量越来越大,就可以将沸点模块拆分出来作为单独的一个服务方便扩容,用户模块和通知模块打包在一起作为另一个服务,这样就从单体应用变成了微服务

注:从单体应用到微服务的切换是不需要修改代码的

标准

基于之前对于DDD的研究和铺垫

我决定将DDD的实现进行标准化

也就是编写一些类来规范DDD的结构和用法

在这个基础上我们不需要再从0开始实现概念

而是会像其他的框架一样通过实现具体的接口来落地

不过这里需要提前说明:

整个规范的落地是以我自己的思路来实现的

所以可能会存在对于概念上的理解差异

以及我其实算是结合了DDDMVC来对项目进行优化

而非纯粹的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 的领域集合

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论