前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CRUDBoy成长计划(2)—项目中如何落地建造者模式

CRUDBoy成长计划(2)—项目中如何落地建造者模式

作者头像
鹿老师的Java笔记
发布2022-01-20 13:21:20
3040
发布2022-01-20 13:21:20
举报

CRUDBoy成长计划(2)—项目中如何落地建造者模式

注意:

  1. 本次计划针对有一定工作经验,想要提升技术和工作能力的程序员。
  2. 关于本计划的详情见:CRUD Boy 成长计划
  3. 宗旨 首先不做理论知识普及,这样的东西太多了,其次,不做自我感动的事情,看上去学了很久,实际上工作用不到,面试问不到。 最后功利的一点说就是为了提高薪资。为了能够在工作中有更好的表现,将代码写的更好,更易用,更好维护,更优雅。也为了,在下一次面试的时候,能够从一群八股文科举选手中脱颖而出。 归根结底,技术岗位最重要的还是解决问题的能力,而不是背书的能力。 由于每个人的经验都是不可复制的,因此,我们需要互相学习互相探索出一条程序员自我成长的路线。整体方向围绕两个核心 职业素养 和 技术能力开展。既为了现在能够把代码写好,把需求做好,也为了日后能够把牛逼吹好,把薪资谈好。

需求背景

最近在做一个事情,封装数据流程引擎,这个是官方的名字,实际上就是封装一个用于处理数据的框架。

当前的项目主要负责数据的处理,将业务方的数据通过清洗转换,然后存放到自己的数据库或者ES中。举个例子就是:有一张A表在业务方数据库中,需要将这张表的数据清洗处理之后存放到数据库的B表中。简单说就是读数据->处理数据->写数据的场景

那么问题,或者说这个过程的难点在什么地方呢?

难点在于,清洗不是简单对数据的判断,而是需要在这个过程中根据数据的状态进行增删改查,或者根据C表判断A表的数据是否应该存放到B表中(也可能是修改和删除)。这样的一种场景还是只是最低的复杂度,复杂度高一点的需求,需要进行各种组装过滤判断装换。

而整个项目大部分的需求都是这样的,对于开发人员来说,感觉工作过于重复,因此就想要实现一个工具通过配置化的方式实现无代码或者少代码完成数据的处理。

以上是大致的背景,在这个框架的实现过程中有一个问题:如何确保配置项参数的正确和有效

简单解释一下,要配置一个数据处理任务,最起码需要说清楚一些内容:从那张表读取数据,要放到哪张表中。哪个字段是主键,过滤条件是什么,多个处理流程的话,父处理器和子处理器是什么,串行还是并行,等等。

这些配置信息该如何封装到对象中便于框架在后续的代码中使用呢?直接new一个配置对象,然后将数据set进去吗?

当然不行,通过set方法会有几个问题不便于解决:

  1. 核心参数有十来个,有些参数有默认值,有些没有,个别属性的默认值需要根据数据源的变化而变化(目前是mysql和es,后续还可能有jms),默认值不好处理,另外必填参数是不是还好放在构造方法中?构造函数会变的很长,且可能需要重载多次。
  2. 由于业务需要,部分参数之间有关联关系,需要做一些参数的校验。例如C属性赋值之前需要保证A B两个属性不能为空。
  3. 配置项在任务运行起来之后是不应该被修改的,所以需要保证配置项对象创建出来之后不能改变。

基于以上的问题,通过传统的new+set是解决不了的。因此需要一些特殊的代码方式,也就是建造者模式进行处理。

什么是建造者模式

设计模式有一个常见的分类:创建型、结构型和行为型。建造者模式属于是创建型中的一种,也可以称之为Builder模式,创建者模式等。顾名思义,创建者模式就是用于创建对象的。

那么问题来了,为什么要使用建造者模式,或者说建造者模式有什么用?

就以刚才的需求为例,我要创建一个配置项对象,那么由于这个对象的参数多且复杂,如果采用new+set的方式,毫无疑问,需要有多个构造方法(不同参数重载多次),在set方法中也很处理的很复杂,进行很多的参数校验。势必让代码越来越乱,那么如何使用建造者模式会怎么处理这种情况呢?

接下来让我们通过一个demo来边实现边学习。

建造者模式demo

在建造者模式中,有两个核心的概念:建造者和被创建对象。(这个地方也可以分成四个核心概念,但是没有必要,概念太多没啥实际意义)。被创建对象就是我们要创建的配置项对象,那么什么是建造者呢?

建造者就是用于创建配置项对象的,对,你没有看错,我们不直接创建配置项对象,而是通过创建者来实现,如图所示。

image-20211223192224187

接下来通过代码来进行模拟:

首先创建一个 配置项类(省略几十个属性)

代码语言:javascript
复制
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 = "";
}

创建建造者类,在类中做这样几个事情

  • 设置给必填属性,或者默认属性的默认值,以DEFAULT_ID_COLUMN为例。

image-20211223192827744

  • 提供一个builder方法,使用建造者类中的默认值创建对象。 public SynchronizerConfiguration build() { config = new SynchronizerConfiguration(id,name); return config; }

这样当我们要获取一个配置项对象的时候,就可以通过建造者对象类获取,相对应的使用代码如下:

代码语言:javascript
复制
SynchronizerConfiguration drugInstructionSyncConfig = new SynchronizerConfigurationBuilder()
        .name("书主表数据同步任务")
        .build();

通过以上的方式,**我们可以把校验逻辑放置到Builder类中,先创建建造者,并且通过set()方法设置建造者的变量值,然后在使用build()方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。**这就是建造者对象。说白了就是将参数的处理和对象的创建分离,在建造者对象中处理参数,该默认值默认值,该校验校验,全部处理好,再创建目标对象。

根据以上的描述,可以思考一下建造者模式的应用场景,首先建造者是为了处理参数,因此如果参数不复杂且很少的情况下是没有必要使用建造者的,因此当你创建对象的时候,感觉参数多且复杂的时候就可以考虑建造者了。

项目应用—线程池工具类

以上的案例是项目中真实的业务代码,但是由于业务的问题,并不能提供所有的代码,所以接下来会提供另一种常见实现—线程池创建。

线程池就不过多解释了,在使用线程池的时候,如果需要自定义线程池的话,参数的处理往往是非常复杂的,核心线程数,最大线程数,等待队列,拒绝策略等,不同的参数设置又会实现出来不同作用的线程池。因此完美的契合建造者模式的应用。代码如下。

注:代码参照hutool工具类中的线程池工具类实现,想要看更完整代码请看hutool工具类源码

代码语言:javascript
复制
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()方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。这就是建造者对象。说白了就是将参数的处理和对象的创建分离,在建造者对象中处理参数,该默认值默认值,该校验校验,全部处理好,再创建目标对象。

根据以上的描述,可以思考一下建造者模式的应用场景,首先建造者是为了处理参数,因此如果参数不复杂且很少的情况下是没有必要使用建造者的,因此当你创建对象的时候,感觉参数多且复杂的时候就可以考虑建造者了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 鹿小洋的Java笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CRUDBoy成长计划(2)—项目中如何落地建造者模式
    • 需求背景
      • 什么是建造者模式
        • 建造者模式demo
          • 项目应用—线程池工具类
            • 总结
            相关产品与服务
            数据库
            云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档