注意:
最近在做一个事情,封装数据流程引擎,这个是官方的名字,实际上就是封装一个用于处理数据的框架。
当前的项目主要负责数据的处理,将业务方的数据通过清洗转换,然后存放到自己的数据库或者ES中。举个例子就是:有一张A表在业务方数据库中,需要将这张表的数据清洗处理之后存放到数据库的B表中。简单说就是读数据->处理数据->写数据的场景。
那么问题,或者说这个过程的难点在什么地方呢?
难点在于,清洗不是简单对数据的判断,而是需要在这个过程中根据数据的状态进行增删改查,或者根据C表判断A表的数据是否应该存放到B表中(也可能是修改和删除)。这样的一种场景还是只是最低的复杂度,复杂度高一点的需求,需要进行各种组装过滤判断装换。
而整个项目大部分的需求都是这样的,对于开发人员来说,感觉工作过于重复,因此就想要实现一个工具通过配置化的方式实现无代码或者少代码完成数据的处理。
以上是大致的背景,在这个框架的实现过程中有一个问题:如何确保配置项参数的正确和有效。
简单解释一下,要配置一个数据处理任务,最起码需要说清楚一些内容:从那张表读取数据,要放到哪张表中。哪个字段是主键,过滤条件是什么,多个处理流程的话,父处理器和子处理器是什么,串行还是并行,等等。
这些配置信息该如何封装到对象中便于框架在后续的代码中使用呢?直接new一个配置对象,然后将数据set进去吗?
当然不行,通过set方法会有几个问题不便于解决:
基于以上的问题,通过传统的new+set是解决不了的。因此需要一些特殊的代码方式,也就是建造者模式进行处理。
设计模式有一个常见的分类:创建型、结构型和行为型。建造者模式属于是创建型中的一种,也可以称之为Builder模式,创建者模式等。顾名思义,创建者模式就是用于创建对象的。
那么问题来了,为什么要使用建造者模式,或者说建造者模式有什么用?
就以刚才的需求为例,我要创建一个配置项对象,那么由于这个对象的参数多且复杂,如果采用new+set的方式,毫无疑问,需要有多个构造方法(不同参数重载多次),在set方法中也很处理的很复杂,进行很多的参数校验。势必让代码越来越乱,那么如何使用建造者模式会怎么处理这种情况呢?
接下来让我们通过一个demo来边实现边学习。
在建造者模式中,有两个核心的概念:建造者和被创建对象。(这个地方也可以分成四个核心概念,但是没有必要,概念太多没啥实际意义)。被创建对象就是我们要创建的配置项对象,那么什么是建造者呢?
建造者就是用于创建配置项对象的,对,你没有看错,我们不直接创建配置项对象,而是通过创建者来实现,如图所示。
image-20211223192224187
接下来通过代码来进行模拟:
首先创建一个 配置项类(省略几十个属性)
public class SynchronizerConfiguration {
/**
* 默认的主键字段名称(id)
*/
private String DEFAULT_ID_COLUMN;
/**
* 源数据 service class <br>
* 应为 SyncService、IService、ElasticsearchRepository 的子类
*/
private Class<?> sourceServiceClass;
/**
* 目标数据 service class
*/
private Class<?> targetServiceClass;
/**
* 源数据主键名称
*/
private String sourcePrimaryKeyField = DEFAULT_ID_COLUMN;
/**
* 目标数据主键名称
*/
private String targetPrimaryKeyField = DEFAULT_ID_COLUMN;
/**
* 同步任务名称
*/
private String name = "";
}
创建建造者类,在类中做这样几个事情
image-20211223192827744
public SynchronizerConfiguration build()
{
config = new SynchronizerConfiguration(id,name);
return config;
}
这样当我们要获取一个配置项对象的时候,就可以通过建造者对象类获取,相对应的使用代码如下:
SynchronizerConfiguration drugInstructionSyncConfig = new SynchronizerConfigurationBuilder()
.name("书主表数据同步任务")
.build();
通过以上的方式,**我们可以把校验逻辑放置到Builder类中,先创建建造者,并且通过set()方法设置建造者的变量值,然后在使用build()方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。**这就是建造者对象。说白了就是将参数的处理和对象的创建分离,在建造者对象中处理参数,该默认值默认值,该校验校验,全部处理好,再创建目标对象。
根据以上的描述,可以思考一下建造者模式的应用场景,首先建造者是为了处理参数,因此如果参数不复杂且很少的情况下是没有必要使用建造者的,因此当你创建对象的时候,感觉参数多且复杂的时候就可以考虑建造者了。
以上的案例是项目中真实的业务代码,但是由于业务的问题,并不能提供所有的代码,所以接下来会提供另一种常见实现—线程池创建。
线程池就不过多解释了,在使用线程池的时候,如果需要自定义线程池的话,参数的处理往往是非常复杂的,核心线程数,最大线程数,等待队列,拒绝策略等,不同的参数设置又会实现出来不同作用的线程池。因此完美的契合建造者模式的应用。代码如下。
注:代码参照hutool工具类中的线程池工具类实现,想要看更完整代码请看hutool工具类源码。
public class ExecutorBuilder {
/** 默认的等待队列容量 */
public static final int DEFAULT_QUEUE_CAPACITY = 1024;
/**
* 初始池大小
*/
private int corePoolSize;
/**
* 最大池大小(允许同时执行的最大线程数)
*/
private int maxPoolSize = Integer.MAX_VALUE;
/**
* 线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长
*/
private long keepAliveTime = TimeUnit.SECONDS.toNanos(60);
/**
* 队列,用于存放未执行的线程
*/
private BlockingQueue<Runnable> workQueue;
/**
* 线程工厂,用于自定义线程创建
*/
private ThreadFactory threadFactory;
/**
* 当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略
*/
private RejectedExecutionHandler handler;
/**
* 线程执行超时后是否回收线程
*/
private Boolean allowCoreThreadTimeOut;
/**
* 设置初始池大小,默认0
*
* @param corePoolSize 初始池大小
* @return this
*/
public ExecutorBuilder setCorePoolSize(int corePoolSize) {
this.corePoolSize = corePoolSize;
return this;
}
/**
* 设置最大池大小(允许同时执行的最大线程数)
*
* @param maxPoolSize 最大池大小(允许同时执行的最大线程数)
* @return this
*/
public ExecutorBuilder setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
return this;
}
/**
* 设置线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长
*
* @param keepAliveTime 线程存活时间
* @param unit 单位
* @return this
*/
public ExecutorBuilder setKeepAliveTime(long keepAliveTime, TimeUnit unit) {
return setKeepAliveTime(unit.toNanos(keepAliveTime));
}
/**
* 设置线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长,单位纳秒
*
* @param keepAliveTime 线程存活时间,单位纳秒
* @return this
*/
public ExecutorBuilder setKeepAliveTime(long keepAliveTime) {
this.keepAliveTime = keepAliveTime;
return this;
}
/**
* 设置队列,用于存在未执行的线程<br>
* 可选队列有:
*
* <pre>
* 1. {@link SynchronousQueue} 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
* 2. {@link LinkedBlockingQueue} 默认无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maxPoolSize无效。
* 当构造LinkedBlockingQueue对象时传入参数,变为有界队列,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
* 3. {@link ArrayBlockingQueue} 有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
* </pre>
*
* @param workQueue 队列
* @return this
*/
public ExecutorBuilder setWorkQueue(BlockingQueue<Runnable> workQueue) {
this.workQueue = workQueue;
return this;
}
/**
* 使用{@link ArrayBlockingQueue} 做为等待队列<br>
* 有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
*
* @param capacity 队列容量
* @return this
*/
public ExecutorBuilder useArrayBlockingQueue(int capacity) {
return setWorkQueue(new ArrayBlockingQueue<>(capacity));
}
/**
* 使用{@link SynchronousQueue} 做为等待队列(非公平策略)<br>
* 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
*
* @return this
*/
public ExecutorBuilder useSynchronousQueue() {
return useSynchronousQueue(false);
}
/**
* 使用{@link SynchronousQueue} 做为等待队列<br>
* 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
*
* @param fair 是否使用公平访问策略
* @return this
*/
public ExecutorBuilder useSynchronousQueue(boolean fair) {
return setWorkQueue(new SynchronousQueue<>(fair));
}
/**
* 设置线程工厂,用于自定义线程创建
*
* @param threadFactory 线程工厂
* @return this
* @see ThreadFactoryBuilder
*/
public ExecutorBuilder setThreadFactory(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
return this;
}
/**
* 设置当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略
* <p>
* 此处可以使用JDK预定义的几种策略,见{@link RejectPolicy}枚举
*
* @param handler {@link RejectedExecutionHandler}
* @return this
* @see RejectPolicy
*/
public ExecutorBuilder setHandler(RejectedExecutionHandler handler) {
this.handler = handler;
return this;
}
/**
* 设置线程执行超时后是否回收线程
*
* @param allowCoreThreadTimeOut 线程执行超时后是否回收线程
* @return this
*/
public ExecutorBuilder setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {
this.allowCoreThreadTimeOut = allowCoreThreadTimeOut;
return this;
}
/**
* 创建ExecutorBuilder,开始构建
*
* @return this
*/
public static ExecutorBuilder create() {
return new ExecutorBuilder();
}
public ThreadPoolExecutor build() {
return build(this);
}
/**
* 构建ThreadPoolExecutor
*
* @param builder this
* @return {@link ThreadPoolExecutor}
*/
private static ThreadPoolExecutor build(ExecutorBuilder builder) {
final int corePoolSize = builder.corePoolSize;
final int maxPoolSize = builder.maxPoolSize;
final long keepAliveTime = builder.keepAliveTime;
final BlockingQueue<Runnable> workQueue;
if (null != builder.workQueue) {
workQueue = builder.workQueue;
} else {
// corePoolSize为0则要使用SynchronousQueue避免无限阻塞
workQueue = (corePoolSize <= 0) ? new SynchronousQueue<>() : new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
}
final ThreadFactory threadFactory = (null != builder.threadFactory) ? builder.threadFactory : Executors.defaultThreadFactory();
RejectedExecutionHandler handler = ObjectUtils.defaultIfNull(builder.handler, new ThreadPoolExecutor.AbortPolicy());
final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime, TimeUnit.NANOSECONDS,
workQueue,
threadFactory,
handler
);
if (null != builder.allowCoreThreadTimeOut) {
threadPoolExecutor.allowCoreThreadTimeOut(builder.allowCoreThreadTimeOut);
}
return threadPoolExecutor;
}
}
总结,没什么总结的,下面一段话在我看来是最重要的。
通过以上的方式,我们可以把校验逻辑放置到Builder类中,先创建建造者,并且通过set()方法设置建造者的变量值,然后在使用build()方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。这就是建造者对象。说白了就是将参数的处理和对象的创建分离,在建造者对象中处理参数,该默认值默认值,该校验校验,全部处理好,再创建目标对象。
根据以上的描述,可以思考一下建造者模式的应用场景,首先建造者是为了处理参数,因此如果参数不复杂且很少的情况下是没有必要使用建造者的,因此当你创建对象的时候,感觉参数多且复杂的时候就可以考虑建造者了。