本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
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'
<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
参考DDD的CQRS架构,分别实现领域服务,应用服务以及搜索服务
领域服务
领域服务比较简单,只有领域对象的处理
//用户领域服务
@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;
}
以及还有UserCreateCommand,UserUpdateCommand,UserDeleteCommand,UserQuery等等
同时还需要定义一个接口来转换视图模型和领域模型
//领域模型转换适配器
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);
}
}
一般情况下我们查询的时候也是从数据库拿的
不过也有一些例外情况,比如说
可以实现ESUserSearcher从ES中查询数据
也可以实现RedisUserSearcher从Redis中查询数据
搜索服务能够很好的实现读写分离
外层接口
最后就是最外层的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只是接收数据然后转交给应用服务或查询服务即可
六边形架构
我们的整个架构如下图所示
以领域作为核心,输入输出通过边缘适配接入HTTP协议,数据库,消息队列等
总结
这种实现方式弱化了聚合根的概念,依旧保持MVC的增删改模式,降低了仓储的技术要求
将业务和技术分割开来,便于在替换技术方案的时候不影响或少影响业务代码
引入了延迟加载的机制,优化不必要的查询