likes
comments
collection
share

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(优化与重构)

作者站长头像
站长
· 阅读数 22

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

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

RESTful与API设计&管理

网关路由模块化支持与条件配置

DDD领域驱动设计与业务模块化(概念与理解)

DDD领域驱动设计与业务模块化(落地与实现)

DDD领域驱动设计与业务模块化(薛定谔模型)

DDD领域驱动设计与业务模块化(优化与重构)(本文)

RPC模块化设计与分布式事务

未完待续......

通过添加启动模块来任意组合和扩展功能模块

  • 示例1:通过启动模块juejin-appliaction-systemjuejin-user(用户)juejin-message(消息)合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin来单独提供juejin-pin(沸点)模块服务以支持大流量功能模块的精准扩容

  • 示例2:通过启动模块juejin-appliaction-singlejuejin-user(用户)juejin-message(消息)juejin-pin(沸点)直接打包成一个单体应用来运行,适合项目前期体量较小的情况

PS:示例基于IDEA + Spring Cloud

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(优化与重构)

为了能更好的理解本专栏中的模块化,建议读者先阅读 服务端模块化架构设计|项目结构与模块化构建思路

前情回顾

在上一篇 DDD领域驱动设计与业务模块化(薛定谔模型) 中,我们单独讲述了薛定谔模型的设计,但是还有很多地方可以优化,所以下面就对于这部分内容来讲述一下思路(没有看过 落地与实现薛定谔模型 的话建议先看 落地与实现薛定谔模型 哦)

校验器 DomainValidator

还记得我们的Builder是怎么校验的么

/**
 * 沸点
 */
public class PinImpl implements Pin {

    //省略属性
    
    public static class Builder {

        //省略属性和属性方法

        public PinImpl build() {
            if (!StringUtils.hasText(id)) {
                throw new IllegalArgumentException("Id required");
            }
            if (!StringUtils.hasText(content)) {
                throw new IllegalArgumentException("Content required");
            }
            if (user == null) {
                throw new IllegalArgumentException("User required");
            }
            if (comments == null) {
                throw new IllegalArgumentException("Comments required");
            }
            if (likes == null) {
                throw new IllegalArgumentException("Likes required");
            }
            if (createTime == null) {
                createTime = System.currentTimeMillis();
            }
            return new PinImpl(
                    id,
                    content,
                    club,
                    user,
                    comments,
                    likes,
                    createTime);
        }
    }
}

每个领域模型的Builder都需要我们手动校验每一个属性,这样写太麻烦了,平时在写Controller的时候我们可以用@Valid或者@Validated配合一些注解来做参数校验,所以我们也可以用同样的注解来实现这个功能

首先我们定义一个DomainValidator

/**
 * 领域校验器
 */
public interface DomainValidator {

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

然后在Builder中用DomainValidator来校验就行了

/**
 * 沸点
 */
public class PinImpl implements Pin {

    //省略属性

    public static class Builder {

        @NotEmpty
        protected String id;

        @NotEmpty
        protected String content;

        protected Club club;

        @NotNull
        protected User user;

        @NotNull
        protected Comments comments;

        @NotNull
        protected Likes likes;

        @NotNull
        protected Long createTime;
        
        /**
         * 需要传入一个校验器
         */
        protected DomainValidator validator

        //省略方法

        public PinImpl build() {
            if (createTime == null) {
                createTime = System.currentTimeMillis();
            }
            validator.validate(this);//校验属性
            return new PinImpl(
                    id,
                    content,
                    club,
                    user,
                    comments,
                    likes,
                    createTime);
        }
    }
}

我们在Builder中配置DomainValidator并且在属性上标记注解,然后只需要在build方法中调用validate就可以对属性进行校验了

至于DomainValidator的实现类我们可以根据我们的需求灵活实现,比如想要直接复用Spring的校验逻辑就可以实现一个对应的实现类

/**
 * 基于 Spring 的校验器
 */
@AllArgsConstructor
public class ApplicationDomainValidator implements DomainValidator {

    /**
     * org.springframework.validation.Validator
     */
    private 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());
        }
    }
}

这样就相当于给我们的Builder加上了一个@Valid/@Validated注解来做校验了

上下文 DomainContext

在我们之前的薛定谔模型中,需要传入指定的Repository

/**
 * 薛定谔的圈子模型
 */
@Getter
public class SchrodingerClub extends ClubImpl implements Club {

    /**
     * 圈子存储
     */
    protected ClubRepository clubRepository;

    protected SchrodingerClub(String id, ClubRepository clubRepository) {
        this.id = id;
        this.clubRepository = clubRepository;
    }

    /**
     * 获得圈子名称
     */
    @Override
    public String getName() {
        //如果名称为 null 则先从存储读取
        if (this.name == null) {
            load();
        }
        return this.name;
    }

    /**
     * 获得圈子图标
     */
    @Override
    public String getLogo() {
        //如果图标为 null 则先从存储读取
        if (this.logo == null) {
            load();
        }
        return this.logo;
    }

    /**
     * 获得圈子描述
     */
    @Override
    public String getDescription() {
        //如果描述为 null 则先从存储读取
        if (this.description == null) {
            load();
        }
        return this.description;
    }

    /**
     * 根据 id 加载其他的数据
     */
    public void load() {
        Club club = getClubRepository().get(id);
        if (club == null) {
            throw new JuejinException("Club not found: " + id);
        }
        this.name = club.getName();
        this.tag = club.getTag();
        this.description = club.getDescription();
    }

    public static class Builder {

        protected String id;

        protected ClubRepository clubRepository;
        
        protected DomainValidator validator;

        //省略属性方法

        public SchrodingerClub build() {
            validator.validate(this);
            return new SchrodingerClub(id, clubRepository);
        }
    }
}

我们的SchrodingerClub需要传入ClubRepository

但是这样会有两个问题

  • 当需要再扩展一个PinRepository来获得圈子下的沸点数量的时候,需要之前所有用到的地方都添加代码来多设置一个PinRepository,如果这个模型用的地方很多,那么改起来也会比较麻烦
new SchrodingerClub.Builder()
    .id(id)
    .clubRepository(clubRepository)
    .pinRepository(pinRepository)//每个地方都要加
    .validator(validator)
    .build();
  • 容易出现循环依赖,如SchrodingerClub现在需要PinRepository来获得圈子下的沸点数量,而PinRepository中又需要ClubRepository来生成沸点对应的圈子SchrodingerClub,这样就出现了循环依赖

针对上面两个问题,我们可以用ApplicationContext#getBean来解决

定义一个DomainContext作为抽象

/**
 * 领域上下文
 */
public interface DomainContext {

    /**
     * 通过类获得实例
     */
    <T> T get(Class<T> type);
}

然后基于ApplicationContext实现一个ApplicationDomainContext

/**
 * 基于 {@link ApplicationContext} 实现领域上下文
 */
@AllArgsConstructor
public class ApplicationDomainContext implements DomainContext {

    private ApplicationContext context;

    @Override
    public <T> T get(Class<T> type) {
        return context.getBean(type);
    }
}

我们的薛定谔模型就可以改成这样

/**
 * 薛定谔的圈子模型
 */
@Getter
public class SchrodingerClub extends ClubImpl implements Club {

    protected DomainContext context;

    protected SchrodingerClub(String id, DomainContext context) {
        this.id = id;
        this.context = context;
    }

    /**
     * 获得圈子名称
     */
    @Override
    public String getName() {
        //如果名称为 null 则先从存储读取
        if (this.name == null) {
            load();
        }
        return this.name;
    }

    /**
     * 获得圈子图标
     */
    @Override
    public String getLogo() {
        //如果图标为 null 则先从存储读取
        if (this.logo == null) {
            load();
        }
        return this.logo;
    }

    /**
     * 获得圈子描述
     */
    @Override
    public String getDescription() {
        //如果描述为 null 则先从存储读取
        if (this.description == null) {
            load();
        }
        return this.description;
    }

    /**
     * 根据 id 加载其他的数据
     */
    public void load() {
        ClubRepository clubRepository = context.get(ClubRepository.class);
        Club club = clubRepository.get(id);
        if (club == null) {
            throw new JuejinException("Club not found: " + id);
        }
        this.name = club.getName();
        this.tag = club.getTag();
        this.description = club.getDescription();
    }

    public static class Builder {

        protected String id;

        protected DomainContext context;
        
        protected DomainValidator validator;

        //省略属性方法

        public SchrodingerClub build() {
            validator.validate(this);
            return new SchrodingerClub(id, context);
        }
    }
}

最后我们使用的时候就只要传入idDomainValidatorDomainContext就行了

new SchrodingerClub.Builder()
    .id(id)
    .context(context)
    .validator(validator)
    .build();

就算SchrodingerClub需要100个Repository都不需要添加参数,也不用担心会循环依赖了

薛定谔模型动态代理

我们的薛定谔模型需要重写除了getId外的所有方法,如果模型的字段很多,重写方法的时候也就会很费力,所以我就想能不能用动态代理在调用方法之前统一进行查询

改造之后的SchrodingerClub

/**
 * 薛定谔的圈子模型
 */
@Getter
public class SchrodingerClub implements InvocationHandler {

    /**
     * 圈子 id
     */
    protected String id;

    /**
     * 圈子 懒加载
     */
    protected Club club;

    protected DomainContext context;

    protected SchrodingerClub(String id, DomainContext context) {
        this.id = id;
        this.context = context;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //如果是获取 id 的话直接返回
        if ("getId".equals(method.getName())) {
            return id;
        }
        return method.invoke(getClub(), args);
    }

    protected Club getClub() {
        //如果为 null 则先查询一次数据
        if (this.club == null) {
            ClubRepository clubRepository = context.get(ClubRepository.class);
            Club club = clubRepository.get(id);
            if (club == null) {
                throw new JuejinException("Club not found: " + id);
            }
            this.club = club;
        }
        return this.club;
    }

    public static class Builder {

        @NotNull
        protected String id;

        @NotNull
        protected DomainContext context;
        
        protected DomainValidator validator;

        //省略属性方法

        public Club build() {
            validator.validate(this);
            //动态代理 Club 接口
            return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Club.class}, new SchrodingerClub(id, context));
        }
    }
}

这样我们就可以解放双手,不用一个一个重写方法啦

不过每生成一个对象都要动态代理一次的话,性能肯定是没有手动重写方法的方式好,所以我还写了个程序来对比创建对象的时间差距

public static void test() {
    int count = 10000000;
    List<IA> list = new ArrayList<>(count * 2);
    long start = System.currentTimeMillis();
    for (int i = 0; i < count; i++) {
        list.add(nativeNew());
    }
    long time = System.currentTimeMillis();
    System.out.println(time - start);
    for (int i = 0; i < count; i++) {
        list.add(proxyNew());
    }
    long end = System.currentTimeMillis();
    System.out.println(end - time);
}

public static IA nativeNew() {
    return new A();
}

public static IA proxyNew() {
    return (IA) Proxy.newProxyInstance(
            IA.class.getClassLoader(),
            new Class[]{IA.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    return null;
                }
            });
}

public interface IA {

}

public static class A implements IA {

}

记录一下实验数据

次数原生(运行3次平均ms)代理(运行3次平均ms)
1w2.3320.66
10w8.00190.33
100w35.001404.66

100w次需要1.4s我感觉其实就算每次生成都用动态代理体感也没差,如果真的很在意性能的话就自己手动重写方法就行了

数据模型缓存

我们可以使用一些缓存防止多次查询相同的数据,这样性能上应该也能说得过去

定义缓存接口Cache

/**
 * 缓存
 */
public interface Cache<T> {

    /**
     * 设置缓存
     */
    void set(String id, T cache);

    /**
     * 获得缓存
     */
    T get(String id);

    /**
     * 清除缓存
     */
    void remove(String id);
}

然后定义缓存提供者CacheProvider和缓存适配器CacheAdapter,我们可以通过缓存提供者去遍历所有的缓存适配器来适配适合的缓存

/**
 * 缓存提供者
 */
public interface CacheProvider {

    /**
     * 根据 key 获得对应的缓存
     */
    <T> Cache<T> get(Object key);
}

/**
 * 缓存适配器
 */
public interface CacheAdapter {

    /**
     * 是否支持 key
     */
    boolean support(Object key);

    /**
     * 获得缓存
     */
    <T> Cache<T> adapt(Object key);
}

提供一个CacheProvider的默认实现CacheProviderImpl

/**
 * 缓存提供者通过缓存适配器来返回各种缓存
 */
@Component
@AllArgsConstructor
public class CacheProviderImpl implements CacheProvider {

    /**
     * 所有的缓存适配器
     */
    private List<CacheAdapter> cacheAdapters;

    /**
     * 按顺序进行匹配
     */
    @Override
    public <T> Cache<T> get(Object key) {
        for (CacheAdapter adapter : cacheAdapters) {
            if (adapter.support(key)) {
                return adapter.adapt(key);
            }
        }
        return new NoCache<>();
    }

    /**
     * 当没有匹配到时返回无缓存
     */
    static class NoCache<T> implements Cache<T> {

        @Override
        public void set(String id, T cache) {

        }

        @Override
        public T get(String id) {
            return null;
        }

        @Override
        public void remove(String id) {

        }
    }
}

通过缓存适配器CacheAdapter我们可以非常灵活的控制缓存实现

如我们可以定义一个全局的内存缓存支持所有的情况

/**
 * 内存缓存适配器
 */
@Order
@Component
public class InMemoryCacheAdapter implements CacheAdapter {

    /**
     * 全部支持
     */
    @Override
    public boolean support(Object key) {
        return true;
    }

    /**
     * 返回内存缓存实现
     */
    @Override
    public <T> Cache<T> adapt(Object key) {
        return new InMemoryCache<>();
    }
}

现在我们希望把评论缓存到Redis,我们就可以针对CommentRepository实现一个单独的CommentCacheAdapter

/**
 * 评论缓存适配器
 */
@Order(0)
@Component
public class CommentCacheAdapter implements CacheAdapter {

    @Autowired
    private RedisTemplate<String, String> template;

    /**
     * 支持评论
     */
    @Override
    public boolean support(Object key) {
        return key instanceof CommentRepository;
    }

    /**
     * 返回Redis缓存实现
     */
    @Override
    public <T> Cache<T> adapt(Object key) {
        return new RedisCache<>(template);
    }
}

这样我们就可以通过自定义缓存适配器CacheAdapter来统一指定或是单独指定缓存实现

然后我们改造一下我们的AbstractDomainRepository就可以支持所有的Repository

/**
 * 领域存储抽象类
 *
 * @param <T> 领域模型
 * @param <P> 数据模型
 */
public abstract class AbstractDomainRepository<T extends DomainObject, P extends IdProvider> implements DomainRepository<T> {

    /**
     * 缓存提供者
     */
    @Autowired
    protected CacheProvider cacheProvider;

    /**
     * 缓存
     */
    protected volatile Cache<P> cache;

    protected Cache<P> getCache() {
        if (cache == null) {
            synchronized (this) {
                if (cache == null) {
                    //自身作为 key
                    cache = cacheProvider.get(this);
                }
            }
        }
        return cache;
    }

    @Override
    public void update(T object) {
        doUpdate(do2po(object));
        //更新的时候清除缓存
        getCache().remove(object.getId());
    }

    protected abstract void doUpdate(P po);

    @Override
    public void update(Collection<? extends T> objects) {
        doUpdate(objects.stream().map(this::do2po).collect(Collectors.toList()));
        //更新的时候清除缓存
        objects.stream().map(DomainObject::getId).forEach(getCache()::remove);
    }

    protected abstract void doUpdate(Collection<? extends P> pos);

    @Override
    public void delete(T object) {
        doDelete(do2po(object));
        //删除的时候清除缓存
        getCache().remove(object.getId());
    }

    protected abstract void doDelete(P po);

    @Override
    public void delete(String id) {
        doDelete(id);
        //删除的时候清除缓存
        getCache().remove(id);
    }

    protected abstract void doDelete(String id);

    @Override
    public void delete(Collection<String> ids) {
        doDelete(ids);
        //删除的时候清除缓存
        ids.forEach(getCache()::remove);
    }

    protected abstract void doDelete(Collection<String> ids);

    @Override
    public T get(String id) {
        //读取的时候先从缓存读
        //如果没有缓存则查询后放入缓存
        P cache = getCache().get(id);
        if (cache == null) {
            P po = doGet(id);
            getCache().set(id, po);
            if (po == null) {
                return null;
            }
            return po2do(po);
        }
        return po2do(cache);
    }

    protected abstract P doGet(String id);

    @Override
    public Collection<T> select(Collection<String> ids) {
        //读取的时候先从缓存读
        Collection<P> select = new ArrayList<>();
        Collection<String> unCachedIds = new ArrayList<>();
        for (String id : ids) {
            P cache = getCache().get(id);
            if (cache == null) {
                //没有缓存的先保存 id
                unCachedIds.add(id);
            } else {
                //有缓存的直接用
                select.add(cache);
            }
        }
        //一次性查询没有缓存的ids
        Collection<P> pos = doSelect(unCachedIds);
        //把这些放到缓存中
        pos.forEach(it -> getCache().set(it.getId(), it));
        select.addAll(pos);
        return select
                .stream()
                .map(this::po2do)
                .collect(Collectors.toList());
    }

    protected abstract Collection<P> doSelect(Collection<String> ids);
    
    @Override
    public void delete(Conditions conditions) {
        doDelete(conditions);
        stream(conditions).map(IdProvider::getId).forEach(getCache()::remove);
    }

    protected abstract void doDelete(Conditions conditions);

   //省略其他方法

通过AbstractDomainRepository的扩展,将缓存模版化,对于通用的增删改查直接生效,不会影响到具体的上层业务代码,不过如果是一些特殊的场景还是需要单独实现对应的缓存

这里还需要注意缓存存储的应该是数据模型,因为领域模型是不太好做序列化和反序列化的

Conditions Lambda支持

我们之前实现的Conditions需要硬编码属性字段

Conditions conditions = new Conditions();
conditions.equal("pinId", getPinId());

这肯定是很容易出问题的,所以我们看看能不能用Lambda来优化

Conditions conditions = new Conditions();
conditions.equal(Pin::getId, getPinId());

如果我们能够实现上面这样的代码就能解决这个问题

那么怎么把Pin::getId对应到pinId

答案就是SerializedLambda

我们先定义一个接收Pin::getId的接口

@FunctionalInterface
public interface LambdaFunction<T, R> extends Serializable {

    R get(T t);
}

接着给Condition添加对应的方法

public <T, R> Conditions equal(LambdaFunction<T, R> lf, Object value) {
    Method method = lf.getClass().getDeclaredMethod("writeReplace");
    method.setAccessible(true);
    SerializedLambda sl = (SerializedLambda) method.invoke(lf);
    String key = handleKey(sl);
    equal(key, value);
    return this;
}

我们可以通过反射,获得LambdaFunction中的SerializedLambda对象,从SerializedLambda中我们就可以获得"Pin""getId"这两个字符串,然后只要根据我们的需求处理字符串就行了

自定义扩展与条件配置

当项目需要扩展功能的时候,比如现在发布沸点需要显示地理位置,一般情况下我们需要修改我们的各种模型添加这个字段,但是如果这是某一个项目定制化的功能,只有这个项目需要而其他项目不需要话,有没有其他的方式能够满足只扩展这一个项目呢

我这里给大家提供一种思路,我们可以单独添加配置类,用@Bean@ConditionalOnMissingBean来注入ControllerServiceRepository等组件,同时指定扫描的包路径只包含单独添加的配置类,这样我们就可以非常方便的进行扩展了

首先我们先添加配置类

/**
 * 沸点领域相关配置
 */
@Configuration
public class DomainPinConfiguration {

    /**
     * 沸点 Controller
     */
    @Bean
    @ConditionalOnMissingBean
    public PinController pinController() {
        return new PinController();
    }

    /**
     * 沸点 Service
     */
    @Bean
    @ConditionalOnMissingBean
    public PinService pinService() {
        return new PinService();
    }

    /**
     * 沸点模型与视图的转换适配器
     */
    @Bean
    @ConditionalOnMissingBean
    public PinFacadeAdapter pinFacadeAdapter() {
        return new PinFacadeAdapterImpl();
    }

    /**
     * 沸点搜索器
     */
    @Bean
    @ConditionalOnMissingBean
    public PinSearcher pinSearcher() {
        return new PinSearcherImpl();
    }
}

其他模块也是一样添加一个配置类,可以都放一起,也可以分开,我这边是分开的

然后指定扫描的包

@ComponentScan(basePackages = "com.bytedance.juejin.*.config")

这里要注意一下,@ComponentScan是只有一个会生效,其他都会被覆盖,我这里只需要扫描这一个路径即可,如果大家有其他需要扫描的包路径的话,要在一个@ComponentScan中配置所有的包路径

假设我们的juejin-application-single就是需要单独扩展地理位置的项目,我们可以直接在juejin-application-single中进行扩展

首先给我们的模型都加上一个字段,这里只列举其中几个模型,其他的模型也都一样,继承之前的模型然后加一个字段即可

/**
 * 沸点 v2
 */
public interface Pin2 extends Pin {

    /**
     * 获得地理位置
     */
    String getLocation();
}

/**
 * 沸点视图 v2
 */
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class PinVO2 extends PinVO {

    @Schema(description = "地理位置")
    private String location;
}

/**
 * 沸点数据模型 v2
 */
@TableName("t_pin")
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
class PinPO2 extends PinPO {

    /**
     * 地理位置
     */
    private String location;
}

接着扩展我们的PinController

/**
 * 沸点 Controller v2
 */
public class PinController2 extends PinController {

    @Operation(summary = "发布沸点v2")
    @PostMapping("/v2")
    public void create(@RequestBody PinCreateCommand2 create, @Login User user) {
        super.create(create, user);
    }
}

添加一个新的接口,参数PinCreateCommand2也是继承PinCreateCommand之后添加了一个location字段

然后是扩展我们的PinFacadeAdapter

/**
 * 沸点领域模型和视图的转换适配器 v2
 */
public class PinFacadeAdapterImpl2 extends PinFacadeAdapterImpl {

    /**
     * 在 build 之前设置 location
     */
    @Override
    protected void beforeBuild(PinImpl.Builder builder, PinCreateCommand create) {
        ((PinImpl2.Builder) builder).location(((PinCreateCommand2) create).getLocation());
    }

    /**
     * 给 vo 设置 location
     */
    @Override
    public PinVO do2vo(Pin pin) {
        PinVO vo = super.do2vo(pin);
        ((PinVO2) vo).setLocation(((Pin2) pin).getLocation());
        return vo;
    }
}

这里就是重写了一些方法,然后设置了一下location的值

还有我们的MBPPinRepository也需要扩展

/**
 * 基于 MyBatis-Plus 的沸点存储实现 v2
 */
public class MBPPinRepository2 extends MBPPinRepository<PinPO2> {

    @Autowired
    private PinMapper2 pinMapper2;

    /**
     * 给 po 设置 location
     */
    @Override
    public PinPO2 do2po(Pin pin) {
        PinPO2 po = super.do2po(pin);
        po.setLocation(((Pin2) pin).getLocation());
        return po;
    }

    /**
     * 创建 PinPO2
     */
    @Override
    protected PinPO2 newPO() {
        return new PinPO2();
    }

    /**
     * 在 build 之前设置 location
     */
    @Override
    protected void beforeBuild(PinImpl.Builder builder, PinPO2 po) {
        ((PinImpl2.Builder) builder).location(po.getLocation());
    }

    /**
     * 指定数据模型为 PinPO2
     */
    @Override
    public Class<PinPO2> getFetchClass() {
        return PinPO2.class;
    }

    /**
     * 返回 BaseMapper 为 PinMapper2
     */
    @Override
    public BaseMapper<PinPO2> getBaseMapper() {
        return pinMapper2;
    }
}

主要也是重写方法然后设置location,最后只要把这些扩展的组件注入到Spring就行了

虽然看起来要添加好几个类,还要重写好几个类,但是这些都是一次性的,每个功能实现一遍就可以了,如果在当前扩展的基础上再新加字段就没有那么麻烦了

同时这样扩展完全不会影响到之前的业务逻辑,整个过程都是扩展添加,没有修改,完全不用担心会不会影响其他的项目

实例化器 Instantiator

在这个扩展的过程中我又发现了一个问题,那就是我们在手动实例化所有的模型的时候都是直接new

/**
 * 沸点领域模型和视图的转换适配器
 */
@Component
public class PinFacadeAdapterImpl implements PinFacadeAdapter {

    //省略其他代码

    @Override
    public Pin from(PinCreateCommand create, User user) {
        return new PinImpl.Builder()
                .id(generateId())
                .content(create.getContent())
                .club(getClub(create.getClubId()))
                .user(user)
                .build();
    }
}

这里我们直接用new Pin.Builder()会导致我们扩展了Pin2PinImpl2.Builder之后不好扩展

所以我们可以抽象出来一个沸点实例化器PinInstantiator

/**
 * 沸点实例化器
 */
public interface PinInstantiator {

    /**
     * 实例化普通的 Builder
     */
    PinImpl.Builder newBuilder();

    /**
     * 实例化薛定谔的 Builder
     */
    SchrodingerPin.Builder newSchrodingerBuilder();

    /**
     * 实例化视图
     */
    PinVO newView();
}

提供一个默认实现

/**
 * 沸点实例化器实现
 */
@Component
public class PinInstantiatorImpl implements PinInstantiator {

    @Override
    public PinImpl.Builder newBuilder() {
        return new PinImpl.Builder();
    }

    @Override
    public SchrodingerPin.Builder newSchrodingerBuilder() {
        return new SchrodingerPin.Builder();
    }

    @Override
    public PinVO newView() {
        return new PinVO();
    }
}

现在当我们要扩展的时候,只要继承PinInstantiatorImpl重写里面的方法就行了

/**
 * 沸点实例化器 v2
 */
public class PinInstantiatorImpl2 extends PinInstantiatorImpl {

    @Override
    public PinImpl.Builder newBuilder() {
        return new PinImpl2.Builder();
    }

    @Override
    public SchrodingerPin.Builder newSchrodingerBuilder() {
        return new SchrodingerPin2.Builder();
    }

    @Override
    public PinVO newView() {
        return new PinVO2();
    }
}

这样我们就能不修改原来的代码,直接通过扩展来支持新的模型实例化

持久层模块化

因为在我的设想中持久层应该是可以任意替换的,所以我们应该可以通过配置文件进行指定持久层的启用

juejin:
  repository:
    mybatis-plus:
      enabled: true
#   jpa:
#     enabled: true

这里我没有实现jpa的方式,但是大概就是这样的想法,可以有多套持久层实现,然后根据要求支持任意切换

添加一个@ConditionalOnMyBatisPlus

/**
 * 启用 MyBatis-Plus 时才注入
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ConditionalOnProperty(name = "juejin.repository.mybatis-plus.enabled", havingValue = "true")
public @interface ConditionalOnMyBatisPlus {
}

单独创建一个注解方便条件配置,不用每次都写里面的配置文件属性

然后我们的DomainPinConfiguration就变成了这样

/**
 * 沸点领域相关配置
 */
@Configuration
public class DomainPinConfiguration {

    /**
     * 沸点 Controller
     */
    @Bean
    @ConditionalOnMissingBean
    public PinController pinController() {
        return new PinController();
    }

    /**
     * 沸点 Service
     */
    @Bean
    @ConditionalOnMissingBean
    public PinService pinService() {
        return new PinService();
    }

    /**
     * 沸点模型与视图的转换适配器
     */
    @Bean
    @ConditionalOnMissingBean
    public PinFacadeAdapter pinFacadeAdapter() {
        return new PinFacadeAdapterImpl();
    }

    /**
     * 沸点实例化器
     */
    @Bean
    @ConditionalOnMissingBean
    public PinInstantiator pinInstantiator() {
        return new PinInstantiatorImpl();
    }

    /**
     * 沸点搜索器
     */
    @Bean
    @ConditionalOnMissingBean
    public PinSearcher pinSearcher() {
        return new PinSearcherImpl();
    }

    /**
     * 沸点 MyBatis-Plus 配置
     */
    @Configuration
    @ConditionalOnMyBatisPlus
    public static class MyBatisPlusConfiguration {

        /**
         * id 生成器
         */
        @Bean
        @ConditionalOnMissingBean
        public PinIdGenerator pinIdGenerator() {
            return new MBPPinIdGenerator();
        }

        /**
         * 基于 MyBatis-Plus 的沸点存储
         */
        @Bean
        @ConditionalOnMissingBean
        public PinRepository pinRepository() {
            return new MBPPinRepository<>();
        }
    }
}

然后我们的扩展配置PinConfiguration2是这样的

/**
 * 沸点扩展配置 v2
 */
@Configuration
public class PinConfiguration2 {

    @Bean
    public PinController pinController() {
        return new PinController2();
    }

    @Bean
    public PinFacadeAdapter pinFacadeAdapter() {
        return new PinFacadeAdapterImpl2();
    }

    @Bean
    public PinInstantiator pinInstantiator() {
        return new PinInstantiatorImpl2();
    }

    @Configuration
    @ConditionalOnMyBatisPlus
    public static class MyBatisPlusConfiguration2 {

        @Bean
        public PinRepository pinRepository() {
            return new MBPPinRepository2();
        }
    }
}

这里我的PinConfiguration2是在扫描的路径中的,所以能够直接扫到,同时由于我们原来的配置使用了@ConditionalOnMissingBean,所以只会注入我们扩展的组件而不会再注入我们之前的组件了,这样就完成了我们的功能扩展

总结

在重构优化的过程中我发现有很多模版化的方法于是就把他们单独提取出来作为一个基础模块,而且在实现各个组件的时候我发现很多都是把领域模型换一下,其他都一个样,所以我还在考虑可以写一个代码生成的插件,这样就更方便了

另外我们其实可以把实现的所有的业务模块都发布成一个一个的jar,当我们开发一个新项目的时候,可以把已经实现过的功能模块直接依赖进来,需要扩展的就扩展一下,如果是没有实现过的功能就实现一遍,然后继续发布成一个jar,作为之后的项目依赖

这样我们就不需要再花费大量的时间去实现几乎相同的功能需求,可以把更多的时间放到其他的技术上再应用到我们的项目中,形成一个良性的循环

源码

上一篇:DDD领域驱动设计与业务模块化(薛定谔模型)

下一篇:RPC模块化设计与分布式事务

转载自:https://juejin.cn/post/7163930169525141511
评论
请登录