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

3,035 阅读15分钟

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

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

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'
<dependency>
  <groupId>com.github.linyuzai</groupId>
  <artifactId>concept-domain-spring-boot-starter</artifactId>
  <version>2.0.2</version>
</dependency>

<!--使用 mybatisplus 才需要引入-->
<dependency>
  <groupId>com.github.linyuzai</groupId>
  <artifactId>concept-domain-mbp</artifactId>
  <version>2.0.2</version>
</dependency>

下面来讲讲里面的类是怎么设计的

领域对象

首先最重要的就是我们的领域对象的定义

//用于提供 id
public interface Identifiable {

    String getId();
}

//领域对象
public interface DomainObject extends Identifiable {

}

//领域实体
public interface DomainEntity extends DomainObject {

}

//值对象
public interface DomainValue extends DomainObject {

}

//领域模型集合
public interface DomainCollection<T extends DomainObject> extends DomainObject {

    @Override
    default String getId() {
        throw new UnsupportedOperationException();
    }

    //根据 id 查询领域模型
    T get(String id);

    //是否存在对应的 id
    boolean contains(String id);

    //列表查询
    List<T> list();

    //流式查询
    Stream<T> 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<User> {
}

这里和普遍的DDD领域对象实现不太一样,用的是接口,主要也是为了方便延时加载数据

生成领域对象

毕竟我们都是通过领域对象来进行业务处理的

肯定是要先生成领域对象

Builder模式

Builder模式的好处是能够在build的时候进行字段校验

这样的话只要是实例化出来的领域对象就一定是符合业务要求的

不需要进行无意义的判断,如是否为空等(如果业务本身允许为空那还是要进行业务判断的)

所以我们可以定义如下接口

//领域校验器
public interface DomainValidator {

    //校验
    void validate(Object target);
}

//领域模型 Builder
public interface DomainBuilder<T extends DomainObject> {

    T build(DomainValidator validator);
}

//领域模型 Builder 抽象类
public abstract class AbstractDomainBuilder<T extends DomainObject> implements DomainBuilder<T> {

    @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<UserImpl> {

        @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 extends DomainObject> T createObject(Class<T> cls, String id);
    
    //创建延迟调用的领域对象
    <T extends DomainObject> T createObject(Class<T> cls, Supplier<T> supplier);

    //创建在指定集合范围内的指定 id 的领域对象
    <T extends DomainObject> T createObject(Class<T> cls, DomainCollection<T> collection, String id);
    
    //创建指定 ids 的领域集合
    <C extends DomainCollection<?>> C createCollection(Class<C> cls, Collection<String> ids);
    
    //创建延迟调用的领域集合
    <T extends DomainObject, C extends DomainCollection<T>> C createCollection(Class<C> cls, Supplier<Collection<T>> supplier);

    //创建在指定集合范围内的指定 ids 的领域集合
    <T extends DomainObject, C extends DomainCollection<T>> C createCollection(Class<C> cls, C collection, Collection<String> ids);
    
    //省略了部分方法
}

我们可以通过这个工厂来生成不同模型的代理对象(内部用动态代理来实现)

比如当我们现在已经有了用户ID我们就可以通过DomainFactory来获得一个User对象

User user = domainFactory.createObject(User.class, userId);

在我们不需要用户信息的时候是不会触发查询的

只有当我们调用比如user.getUsername()时才会通过userId去查询用户信息

领域仓储

实现增删改查业务的时候就需要仓储层来落地存储

条件

基于我们查询的时候需要添加各种条件参数

先定义一个条件类来存储这些条件

//查询条件
@ToString
@Getter
@Setter(AccessLevel.PROTECTED)
public class Conditions {

    //= 条件
    private Collection<Equal> equals = new ArrayList<>();

    //is null 条件
    private Collection<Null> nulls = new ArrayList<>();

    //in 条件
    private Collection<In> ins = new ArrayList<>();

    //like 条件
    private Collection<Like> likes = new ArrayList<>();

    //order by 条件
    private Collection<OrderBy> orderBys = new ArrayList<>();

    //limit
    private Limit limit;

    //有需要可以继承添加其他条件

    public LambdaConditions lambda() {
        LambdaConditions conditions = new LambdaConditions();
        copy(conditions);
        return conditions;
    }

    //添加 =
    public Conditions equal(String key, Object value) {
        if (notIgnore(key)) {
            equals.add(new Equal(key, value));
        }
        return this;
    }

    //添加 in
    public Conditions in(String key, Collection<?> values) {
        if (notIgnore(key)) {
            ins.add(new In(key, values));
        }
        return this;
    }
    
    //省略了其他的条件配置方法
}

获得一个用户ID的条件的时候就可以这样写

Conditions conditions = new Conditions()
    .lambda()
    .equal(User::getId, userId);

仓储

基于领域对象和查询条件来定义仓储

//领域存储
public interface DomainRepository<T extends DomainObject, C extends DomainCollection<T>> {

    //单个新增
    void create(T object);

    //多个新增
    void create(C collection);

    //单个更新
    void update(T object);

    //多个更新
    void update(C collection);

    //单个删除
    void delete(T object);

    //多个删除
    void delete(C collection);

    //单个 id 查询
    T get(String id);

    //多个 id 查询
    C select(Collection<String> ids);

    //条件删除
    void delete(Conditions conditions);

    //单个条件查询
    T get(Conditions conditions);

    //列表条件查询
    C select(Conditions conditions);

    //数量条件查询
    Long count(Conditions conditions);

    //分页条件查询
    Pages<T> page(Conditions conditions, Pages.Args page);
}

//领域存储抽象类
//P 为数据模型
public abstract class AbstractDomainRepository<T extends DomainObject, C extends DomainCollection<T>,
        P extends Identifiable> implements DomainRepository<T, C> {

    //领域模型转数据模型
    public abstract P do2po(T object);

    //领域模型转数据模型
    public Collection<P> dos2pos(Collection<? extends T> objects) {
        return objects.stream().map(this::do2po).collect(Collectors.toList());
    }

    //数据模型转领域模型
    public abstract T po2do(P po);

    //数据模型转领域模型
    public Collection<T> pos2dos(Collection<? extends P> pos) {
        return pos.stream().map(this::po2do).collect(Collectors.toList());
    }

    //创建一个领域模型
    @Override
    public void create(T object) {
        doCreate(do2po(object));
    }

    //添加一条记录
    protected abstract void doCreate(P po);

    //创建多个领域模型
    @Override
    public void create(C collection) {
        doCreate(dos2pos(collection.list()));
    }

    //添加多条记录
    protected abstract void doCreate(Collection<? extends P> pos);

    //更新一个领域模型
    @Override
    public void update(T object) {
        doUpdate(do2po(object));
    }

    //更新一条记录
    protected abstract void doUpdate(P po);

    //更新多个领域模型
    @Override
    public void update(C collection) {
        doUpdate(dos2pos(collection.list()));
    }

    //更新多条记录
    protected abstract void doUpdate(Collection<? extends P> pos);

    //删除一个领域模型
    @Override
    public void delete(T object) {
        doDelete(do2po(object));
    }

    //删除一条记录
    protected abstract void doDelete(P po);

    //删除多个领域模型
    @Override
    public void delete(C collection) {
        doDelete(dos2pos(collection.list()));
    }

    //删除多条记录
    protected abstract void doDelete(Collection<? extends P> pos);

    //根据 id 获得一个领域模型
    @Override
    public T get(String id) {
        P po = doGet(id);
        if (po == null) {
            return null;
        }
        return po2do(po);
    }

    //根据 id 获得一条数据
    protected abstract P doGet(String id);

    //根据 ids 获得多个领域模型
    @Override
    public C select(Collection<String> ids) {
        return wrap(pos2dos(doSelect(ids)));
    }

    //根据 ids 获得多条数据
    protected abstract Collection<P> doSelect(Collection<String> ids);

    //条件删除
    @Override
    public void delete(Conditions conditions) {
        if (intercept(conditions)) {
            return;
        }
        doDelete(conditions);
    }

    //条件删除
    protected abstract void doDelete(Conditions conditions);

    //条件查询一个领域模型
    @Override
    public T get(Conditions conditions) {
        if (intercept(conditions)) {
            return null;
        }
        P po = doGet(conditions);
        if (po == null) {
            return null;
        }
        return po2do(po);
    }

    //条件查询一条数据
    protected abstract P doGet(Conditions conditions);

    //条件查询多个领域模型
    @Override
    public C select(Conditions conditions) {
        if (intercept(conditions)) {
            return wrap(Collections.emptyList());
        }
        return wrap(pos2dos(doSelect(conditions)));
    }

    //条件查询多条数据
    protected abstract Collection<P> doSelect(Conditions conditions);

    //数量查询
    @Override
    public Long count(Conditions conditions) {
        if (intercept(conditions)) {
            return 0L;
        }
        return doCount(conditions);
    }

    //数量查询
    protected abstract Long doCount(Conditions conditions);

    //条件查询分页
    @Override
    public Pages<T> page(Conditions conditions, Pages.Args page) {
        return doPage(conditions, page).mapAll(this::pos2dos);
    }

    //条件查询分页
    protected abstract Pages<P> doPage(Conditions conditions, Pages.Args page);

    //如果拼接出来 id in () 这样的条件直接返回空
    protected boolean intercept(Conditions conditions) {
        for (Conditions.In in : conditions.getIns()) {
            if (in.getValues().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    //将多条数据包装成领域集合
    protected C wrap(Collection<T> objects) {
        Class<C> genericType = getGenericType();
        DomainFactory factory = getFactory();
        if (factory == null) {
            ProxyListableDomainCollection<T> creator = new ProxyListableDomainCollection<>();
            creator.setType(genericType);
            creator.setContext(getContext());
            creator.setList(new ArrayList<>(objects));
            return creator.create(genericType);
        } else {
            return factory.wrapCollection(genericType, objects);
        }
    }

    protected Class<C> getGenericType() {
        return DomainLink.generic(getClass(), 1);
    }

    protected DomainContext getContext() {
        return null;
    }

    protected DomainFactory getFactory() {
        return null;
    }
}

DomainRepository就是对领域对象的增删改查

AbstractDomainRepository扩展了领域模型和数据模型的转换并提供数据模型的增删改查

MyBatisPlus扩展

因为我平时用MyBatisPlus比较多

所以基于MyBatisPlus扩展了一个MBPDomainRepository

//基于 MyBatis-Plus 的通用存储
public abstract class MBPDomainRepository<T extends DomainObject, C extends DomainCollection<T>, P extends Identifiable>
        extends AbstractDomainRepository<T, C, P> {

    //插入一条数据
    @Override
    protected void doCreate(P po) {
        getBaseMapper().insert(po);
    }

    @Transactional(rollbackFor = Throwable.class)
    @Override
    public void create(C collection) {
        super.create(collection);
    }

    //插入多条数据
    @Override
    protected void doCreate(Collection<? extends P> pos) {
        pos.forEach(this::doCreate);
    }

    //更新一条数据
    @Override
    protected void doUpdate(P po) {
        getBaseMapper().updateById(po);
    }

    @Transactional(rollbackFor = Throwable.class)
    @Override
    public void update(C collection) {
        super.update(collection);
    }

    //更新多条数据
    @Override
    protected void doUpdate(Collection<? extends P> pos) {
        pos.forEach(this::doUpdate);
    }

    //删除一条数据
    @Override
    protected void doDelete(P po) {
        getBaseMapper().deleteById(po.getId());
    }

    @Override
    protected void doDelete(Collection<? extends P> pos) {
        if (pos.isEmpty()) {
            return;
        }
        Set<String> ids = pos.stream()
                .map(Identifiable::getId)
                .collect(Collectors.toSet());
        getBaseMapper().deleteBatchIds(ids);
    }

    //根据 id 获得一条数据
    @Override
    protected P doGet(String id) {
        return getBaseMapper().selectById(id);
    }

    //根据 id 集合获得多条数据
    @Override
    protected Collection<P> doSelect(Collection<String> ids) {
        if (ids.isEmpty()) {
            return Collections.emptyList();
        }
        return getBaseMapper().selectBatchIds(ids);
    }

    @Transactional(rollbackFor = Throwable.class)
    @Override
    public void delete(Conditions conditions) {
        super.delete(conditions);
    }

    //条件删除数据
    @Override
    protected void doDelete(Conditions conditions) {
        getBaseMapper().delete(getWrapper(conditions));
    }

    //根据条件查询一条数据
    @Override
    protected P doGet(Conditions conditions) {
        return getBaseMapper().selectOne(getWrapper(conditions));
    }

    @Override
    protected Collection<P> doSelect(Conditions conditions) {
        return getBaseMapper().selectList(getWrapper(conditions));
    }

    //根据条件获得数量
    @Override
    protected Long doCount(Conditions conditions) {
        return getBaseMapper().selectCount(getWrapper(conditions));
    }

    //根据条件获得分页数据
    @Override
    protected Pages<P> doPage(Conditions conditions, Pages.Args page) {
        IPage<P> p = new Page<>(page.getCurrent(), page.getSize());
        return toPages(getBaseMapper().selectPage(p, getWrapper(conditions)));
    }

    //根据条件生成 Wrapper
    protected Wrapper<P> getWrapper(Conditions conditions) {
        QueryWrapper<P> wrapper = new QueryWrapper<>();
        conditions.getEquals().forEach(it ->
                wrapper.eq(fetchColumn(getFetchClass(), it.getKey()), it.getValue()));
        conditions.getNulls().forEach(it -> {
            if (it.isNot()) {
                wrapper.isNotNull(fetchColumn(getFetchClass(), it.getKey()));
            } else {
                wrapper.isNull(fetchColumn(getFetchClass(), it.getKey()));
            }
        });
        conditions.getIns().forEach(it ->
                wrapper.in(fetchColumn(getFetchClass(), it.getKey()), it.getValues()));
        conditions.getLikes().forEach(it ->
                wrapper.like(fetchColumn(getFetchClass(), it.getKey()), it.getValue()));
        conditions.getOrderBys().forEach(it -> {
            if (it.isDesc()) {
                wrapper.orderByDesc(fetchColumn(getFetchClass(), it.getKey()));
            } else {
                wrapper.orderByAsc(fetchColumn(getFetchClass(), it.getKey()));
            }
        });
        Conditions.Limit limit = conditions.getLimit();
        if (limit != null) {
            wrapper.last("limit " + limit.getStart() + ", " + limit.getSize());
        }
        return wrapper;
    }

    //匹配字段,借助 MyBatis-Plus 的工具获得实体字段与数据库字段的映射
    protected String fetchColumn(Class<P> clazz, String field) {
        Map<String, ColumnCache> columnMap = LambdaUtils.getColumnMap(clazz);
        ColumnCache columnCache = columnMap.get(LambdaUtils.formatKey(field));
        if (columnCache == null) {
            return field;
        }
        return columnCache.getColumn();
    }

    //将 mbp 的 page 转为我们的领域 pages
    protected Pages<P> toPages(IPage<P> p) {
        Pages<P> pages = new Pages<>();
        pages.setCurrent(p.getCurrent());
        pages.setSize(p.getSize());
        pages.setTotal(p.getTotal());
        pages.setPages(p.getPages());
        pages.setRecords(p.getRecords());
        return pages;
    }

    //字段匹配类
    public Class<P> getFetchClass() {
        return DomainLink.generic(getClass(), 2);
    }

    //获得 Mapper
    public abstract BaseMapper<P> getBaseMapper();
}

继承AbstractDomainRepository通过BaseMapper来进行增删改查

这里的P需要实现Identifiable用于获得id,这就是我之前说为什么要单独抽象Identifiable出来

用户仓储

如果我们现在用MyBatisPlus作为持久层

实现UserRepository就非常简单

//用户存储
public interface UserRepository extends DomainRepository<User, Users> {

}

//基于 MBP 的用户存储实现
//UserPO 为数据库表对应的数据模型
@Repository
public class MBPUserRepository extends MBPDomainRepository<User, Users, UserPO> implements UserRepository {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private DomainValidator validator;

    @Override
    public UserPO do2po(User user) {
        UserPO po = new UserPO();
        po.setId(user.getId());
        po.setUsername(user.getUsername());
        po.setPassword(user.getPassword());
        po.setNickname(user.getNickname());
        po.setCreateTime(user.getCreateTime());
        return po;
    }

    @Override
    public User po2do(UserPO po) {
        return new UserImpl.Builder()
                .id(po.getId())
                .username(po.getUsername())
                .password(po.getPassword())
                .nickname(po.getNickname())
                .createTime(po.getCreateTime())
                .build(validator);
    }

    @Override
    public BaseMapper<UserPO> getBaseMapper() {
        return userMapper;
    }
}

只需要实现一下领域模型和数据模型的转换方法就行了

整个存储不存在业务相关的内容,只有基本的增删改查

这样设计方便直接替换持久层框架或是在修改表设计结构时完全不影响业务层

CQRS

参考DDDCQRS架构,分别实现领域服务,应用服务以及搜索服务

领域服务

领域服务比较简单,只有领域对象的处理

//用户领域服务
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private DomainEventPublisher eventPublisher;

    //创建
    public void create(User user) {
        userRepository.create(user);
        eventPublisher.publish(new UserCreatedEvent(user));
    }

    //更新
    public void update(User newUser, User oldUser) {
        userRepository.update(newUser);
        eventPublisher.publish(new UserUpdatedEvent(newUser, oldUser));
    }

    //删除
    public void delete(User user) {
        userRepository.delete(user);
        eventPublisher.publish(new UserDeletedEvent(user));
    }
}

主要就是进行领域内的业务处理然后发布领域事件

视图模型

CQRS中的C就是Command表示增删改,Q就是Query表示查询

所以我们需要定义一些视图模型来接收请求参数和返回响应数据

@Data
@Schema(description = "用户视图")
public class UserVO {

    @Schema(description = "用户ID")
    private String id;

    @Schema(description = "用户名称")
    private String nickname;
}

以及还有UserCreateCommandUserUpdateCommandUserDeleteCommandUserQuery等等

同时还需要定义一个接口来转换视图模型和领域模型

//领域模型转换适配器
public interface UserFacadeAdapter {

    //创建视图转领域模型
    User from(UserCreateCommand create);

    //更新视图转领域模型
    User from(UserUpdateCommand update, User old);

    //领域模型转视图
    UserVO do2vo(User user);

    //查询转条件
    Conditions toConditions(UserQuery query);
}

类似于防腐层

外部模型需要转换为内部专用的领域模型

防止外部模型不断侵入污染领域内部

应用服务

//用户应用服务
@Service
public class UserApplicationService {

    @Autowired
    private UserService userService;

    @Autowired
    private UserFacadeAdapter userFacadeAdapter;

    @Autowired
    private UserRepository userRepository;

    //创建
    public void create(UserCreateCommand create) {
        User user = userFacadeAdapter.from(create);
        userService.create(user);
    }

    //更新
    public void update(UserUpdateCommand update) {
        User oldUser = userRepository.get(update.getId());
        if (oldUser == null) {
            throw new DomainNotFoundException(User.class, update.getId());
        }
        User newUser = userFacadeAdapter.from(update, oldUser);
        userService.update(newUser, oldUser);
    }

    //删除
    public void delete(UserDeleteCommand delete) {
        User user = userRepository.get(delete.getId());
        if (user == null) {
            throw new DomainNotFoundException(User.class, delete.getId());
        }
        userService.delete(user);
    }
}

应用服务在这里主要就是将视图转为领域对象然后交给领域服务处理

搜索服务

//搜索服务
public interface UserSearcher {

    //根据 id 获得
    UserVO get(String id);

    //列表查询
    List<UserVO> list(UserQuery query);

    //分页查询
    Pages<UserVO> page(UserQuery query, Pages.Args page);
}

//搜索服务实现
@Component
public class UserSearcherImpl implements UserSearcher {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserFacadeAdapter userFacadeAdapter;

    @Override
    public UserVO get(String id) {
        User user = userRepository.get(id);
        if (user == null) {
            return null;
        }
        return userFacadeAdapter.do2vo(user);
    }

    @Override
    public List<UserVO> list(UserQuery query) {
        return userRepository.select(userFacadeAdapter.toConditions(query))
                .list()
                .stream()
                .map(userFacadeAdapter::do2vo)
                .collect(Collectors.toList());
    }

    @Override
    public Pages<UserVO> page(UserQuery query, Pages.Args page) {
        return userRepository
                .page(userFacadeAdapter.toConditions(query), page)
                .map(userFacadeAdapter::do2vo);
    }
}

一般情况下我们查询的时候也是从数据库拿的

不过也有一些例外情况,比如说

可以实现ESUserSearcherES中查询数据

也可以实现RedisUserSearcherRedis中查询数据

搜索服务能够很好的实现读写分离

外层接口

最后就是最外层的Controller

@Tag(name = "用户")
@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserApplicationService userApplicationService;

    @Autowired
    private UserSearcher userSearcher;

    @Operation(summary = "创建")
    @PostMapping
    public void create(@RequestBody UserCreateCommand create) {
        userApplicationService.create(create);
    }

    @Operation(summary = "更新")
    @PutMapping
    public void update(@RequestBody UserUpdateCommand update) {
        userApplicationService.update(update);
    }

    @Operation(summary = "删除")
    @DeleteMapping
    public void delete(@RequestBody UserDeleteCommand delete) {
        userApplicationService.delete(delete);
    }

    @Operation(summary = "详情")
    @GetMapping("{id}")
    public UserVO get(@Parameter(description = "ID") @PathVariable String id) {
        return userSearcher.get(id);
    }

    @Operation(summary = "列表查询")
    @GetMapping("list")
    public List<UserVO> list(UserQuery query) {
        return userSearcher.list(query);
    }

    @Operation(summary = "分页查询")
    @GetMapping("page")
    public Pages<UserVO> page(UserQuery query, Pages.Args page) {
        return userSearcher.page(query, page);
    }
}

Controller只是接收数据然后转交给应用服务或查询服务即可

六边形架构

我们的整个架构如下图所示

DDD六边形架构.png

以领域作为核心,输入输出通过边缘适配接入HTTP协议,数据库,消息队列等

总结

这种实现方式弱化了聚合根的概念,依旧保持MVC的增删改模式,降低了仓储的技术要求

将业务和技术分割开来,便于在替换技术方案的时候不影响或少影响业务代码

引入了延迟加载的机制,优化不必要的查询

上一篇:v2.0:结合DDD与MVC的中庸之道(启发与思路)

下一篇:v2.0:结合DDD与MVC的中庸之道(优化与插件)