服务端模块化架构设计|DDD 领域驱动设计与业务模块化(优化与重构)
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
DDD领域驱动设计与业务模块化(优化与重构)(本文)
未完待续......
在之前的文章 服务端模块化架构设计|项目结构与模块化构建思路 中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin
,其中包含三个模块:juejin-user(用户)
,juejin-pin(沸点)
,juejin-message(消息)
通过添加启动模块来任意组合和扩展功能模块
-
示例1:通过启动模块
juejin-appliaction-system
将juejin-user(用户)
和juejin-message(消息)
合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin
来单独提供juejin-pin(沸点)
模块服务以支持大流量功能模块的精准扩容 -
示例2:通过启动模块
juejin-appliaction-single
将juejin-user(用户)
,juejin-message(消息)
,juejin-pin(沸点)
直接打包成一个单体应用来运行,适合项目前期体量较小的情况
PS:示例基于IDEA + Spring Cloud
为了能更好的理解本专栏中的模块化,建议读者先阅读 服务端模块化架构设计|项目结构与模块化构建思路
前情回顾
在上一篇 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);
}
}
}
最后我们使用的时候就只要传入id
,DomainValidator
,DomainContext
就行了
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) |
---|---|---|
1w | 2.33 | 20.66 |
10w | 8.00 | 190.33 |
100w | 35.00 | 1404.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
来注入Controller
,Service
,Repository
等组件,同时指定扫描的包路径只包含单独添加的配置类,这样我们就可以非常方便的进行扩展了
首先我们先添加配置类
/**
* 沸点领域相关配置
*/
@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()
会导致我们扩展了Pin2
和PinImpl2.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
,作为之后的项目依赖
这样我们就不需要再花费大量的时间去实现几乎相同的功能需求,可以把更多的时间放到其他的技术上再应用到我们的项目中,形成一个良性的循环
转载自:https://juejin.cn/post/7163930169525141511