设计模式-建造者模式

设计模式-建造者模式

案例分析

思考这样一个场景,现在需要实现一个简单的线程池,但是有一些要求,如果不传队列长度字段,就必须传最大线程数,原因是这样的处理方式为来一个请求就创建一个新线程,如果不限制最大线程数可能会导致资源耗尽系统崩溃。

java 复制代码
package com.xsdl.builder;

public class ThreadPool {

    private Integer coreSize;
    private Integer maxSize;
    private Integer queueSize;

}

如果按照正常的开发,比如暴露所有属性的 set 方法,会有以下的问题:

  • 不传队列长度字段,就必须传最大线程数的校验该放到哪里去做
java 复制代码
package com.xsdl.builder;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ThreadPool {

    private Integer coreSize;
    private Integer maxSize;
    private Integer queueSize;

    public boolean validate() {
        // 传了队列长度
        if (queueSize != null && queueSize > 0) {
            return true;
        }
        // 没有传队列长度,那么必须传最大线程数
        return maxSize != null;
    }

}
  • 用户设置了部分值后就使用这个对象该怎么办,对象在设置完成之前应该是无效且无法使用的
java 复制代码
package com.xsdl.builder;

public class Main {

    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool();
        threadPool.setCoreSize(10);
        // 用户如果不做校验直接开始使用怎么办
        System.out.println(threadPool.validate());
    }

}

以上这些问题也不是不能解决,比如定义 n 个有参构造器,限制类只能通过有参构造器来进行创建对象,在构造器里对参数进行校验。但这样会导致拓展性极差,且在条件复杂时很难处理。

于是就引入了建造者模式,建造者模式一般单独定义一个 Builder 类或定义为主类里的内部类,主类不允许通过直接创建,而是先创建 Builder 类,并在设置完成所有属性后调用 build 方法来创建实例,如下所示:

java 复制代码
package com.xsdl.builder;

import lombok.Data;
import lombok.experimental.Accessors;

public class ThreadPool {

    private Integer coreSize;
    private Integer maxSize;
    private Integer queueSize;

    // 私有构造方法,防止外部实例化
    private ThreadPool(Builder builder) {
        this.coreSize = builder.coreSize;
        this.maxSize = builder.maxSize;
        this.queueSize = builder.queueSize;
    }

    @Data
    @Accessors(chain = true)
    public static class Builder {
        private Integer coreSize;
        private Integer maxSize;
        private Integer queueSize;

        public ThreadPool build() {
            if (!validate()) {
                throw new IllegalArgumentException("没有队列长度必须传入最大线程数");
            }
            return new ThreadPool(this);
        }

        private boolean validate() {
            // 传了队列长度
            if (queueSize != null && queueSize > 0) {
                return true;
            }
            // 没有传队列长度,那么必须传最大线程数
            return maxSize != null;
        }
    }

}

其中需要注意的地方:

  • 主类的构造器必须私有化,防止从外界直接创建
  • 中间的操作永远不会返回主类对象,只有在调用 build 方法后才能获得到真正的主类对象,校验的方法也在其中
java 复制代码
package com.xsdl.builder;

public class Main {

    public static void main(String[] args) {
        ThreadPool build = new ThreadPool.Builder()
                .setCoreSize(10)
                .setMaxSize(100)
                .build();
    }

}
拓展分析

如果你经常使用 Java 语言进行开发,那么一定用过 Stream 流,Stream 流在对于容器的遍历和处理上非常方便,并将操作分为中间操作和终端操作,调用完终端操作后就没办法再调用中间操作了。深究其原因,中间操作返回的依然是 Stream 而终端操作例如 count、collect 后返回的对象已经变了。

那我们能不能利用 stream 流的思想和建造者模式如何实现一个简单的 sql 生成器呢?

以 update操作为例,update table set columnA = 'A' and columnB = 'B' where ...

因为 update 是可以更新无数个字段的,可以使用一个 Map<> 来存储 column 和要更新为的 value,这个方法可以无限的调用,但是当调用 where 后就不能再向 Map<> 里追加值了,原因是 where 语句已经生成,如果后续再跟 columnA = 'A' 会变成查询条件而不是更新内容。

所以我们实现的功能其实是,如何限制在调用完一个方法后不能再调用同一个类的另一个方法,多态可以很好的满足我们的需求

java 复制代码
package com.xsdl.builder;

public class SqlGenerate {

    private SqlGenerate() {

    }

    public static Init createUpdateSql() {
        return new SqlBuilder();
    }

    public interface Init {
        Update table(String tableName);
    }

    public interface Update {
        Update setString(String column, String value);
        Update setRaw(String column, String value);
        Where where();
    }

    public interface Where {
        Where eqString(String column, String value);
        Where eqRaw(String column, String value);
        String build();
    }

}

定义上述的三个接口,SqlGenerate 类只有一个私有构造器,无法通过外界 new 创建,对外仅提供一个方法 createUpdateSql ,该方法返回一个 Init 接口对象,而该接口对象只能调用 table 方法返回 Update 接口对象,Update 接口对象可以进行多次的 set 值,也可以调用 where 方法返回 where 接口对象。如前图所示。

实现
java 复制代码
package com.xsdl.builder;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public final class SqlGenerate {

    private SqlGenerate() {

    }

    public static Init createUpdateSql() {
        return new SqlBuilder();
    }

    public interface Init {
        Update table(String tableName);
    }

    public interface Update {
        Update setString(String column, String value);
        Update setRaw(String column, String value);
        Where where();
    }

    public interface Where {
        Where eqString(String column, String value);
        Where eqRaw(String column, String value);
        String build();
    }


    private static final class SqlBuilder implements Init, Update, Where {
        private String tableName;
        private final Map<String, String> setStringMap = new LinkedHashMap<>();
        private final Map<String, String> setRawMap = new LinkedHashMap<>();
        private final Map<String, String> whereStringMap = new LinkedHashMap<>();
        private final Map<String, String> whereRawMap = new LinkedHashMap<>();

        @Override
        public Update table(String tableName) {
            this.tableName = tableName;
            return this;
        }

        @Override
        public Update setString(String column, String value) {
            setStringMap.put(column, value);
            return this;
        }

        @Override
        public Update setRaw(String column, String value) {
            setRawMap.put(column, value);
            return this;
        }

        @Override
        public Where where() {
            return this;
        }

        @Override
        public Where eqString(String column, String value) {
            whereStringMap.put(column, value);
            return this;
        }

        @Override
        public Where eqRaw(String column, String value) {
            whereRawMap.put(column, value);
            return this;
        }

        @Override
        public String build() {
            return "UPDATE " + tableName +
                    " SET " + concatAssignments(setStringMap, setRawMap) +
                    " WHERE " + concatConditions(whereStringMap, whereRawMap);
        }


        private static String concatAssignments(Map<String, String> str, Map<String, String> raw) {
            return concatMap(str, true, ", ") + (str.isEmpty() || raw.isEmpty() ? "" : ", ")
                    + concatMap(raw, false, ", ");
        }

        private static String concatConditions(Map<String, String> str, Map<String, String> raw) {
            return concatMap(str, true, " AND ") + (str.isEmpty() || raw.isEmpty() ? "" : " AND ")
                    + concatMap(raw, false, " AND ");
        }

        private static String concatMap(Map<String, String> map, boolean quoted, String delimiter) {
            return map.entrySet()
                    .stream()
                    .map(e -> e.getKey() + " = " + (quoted ? "'" + e.getValue() + "'" : e.getValue()))
                    .collect(Collectors.joining(delimiter));
        }

    }
}

程序中使用以下四个对象保存 set、eq 方法传入的参数值:

java 复制代码
        private final Map<String, String> setStringMap = new LinkedHashMap<>();
        private final Map<String, String> setRawMap = new LinkedHashMap<>();
        private final Map<String, String> whereStringMap = new LinkedHashMap<>();
        private final Map<String, String> whereRawMap = new LinkedHashMap<>();

测试如下:

java 复制代码
package com.xsdl.builder;

public class Main {
    public static void main(String[] args) {
        String sql = SqlGenerate.createUpdateSql()
                .table("user")
                .setString("name", "zhangsan")
                .setRaw("age", "3")
                .where()
                .eqString("1", "1")
                .eqRaw("sex", "1")
                .build();
        System.out.println(sql);
    }
}

输出结果:UPDATE user SET name = 'zhangsan', age = 3 WHERE 1 = '1' AND sex = 1

与预期的设计相符,用类似 stream 的方法实现生成 update sql 语句的功能,虽然也有一些不足,例如数据库中的字符串类型需要加 '' ,因此程序把字符串参数和其他参数分为两个 map 存储,如果先调用 setString 再 setRaw 再 setString,最后生成的语句会把所有 String 的条件生成在前面,丢失了传入的绝对顺序;同时目前的 where 条件只支持 = 的条件,不支持 <= >= != 等判断。 同时在某些严谨性上没有做校验,例如 setStringMap 和 setRawMap 是不能同时为空的,否则 update 语句就失去意义了。

总结

通过以上的例子也可以总结出建造者模式要解决什么样的问题:

  • 需要在生成最终的实例前进行参数的合法性校验
  • 避免出现无效的中间态对象。
相关推荐
SimonKing2 小时前
SpringBoot多模板引擎整合难题?一篇搞定JSP、Freemarker与Thymeleaf!
java·后端·程序员
charlie1145141912 小时前
精读C++20设计模式——创造型设计模式:单例模式
c++·学习·单例模式·设计模式·c++20
Craaaayon3 小时前
【数据结构】二叉树-图解深度优先搜索(递归法、迭代法)
java·数据结构·后端·算法·leetcode·深度优先
梦之翼6187203 小时前
eclipse复制项目后原项目名依然伴随值所复制的项目名
java·eclipse
缘的猿3 小时前
Kubernetes 安全管理:认证、授权与准入控制全面解析
java·安全·kubernetes
ChinaRainbowSea3 小时前
5. Prompt 提示词
java·人工智能·后端·spring·prompt·ai编程
合作小小程序员小小店3 小时前
web开发,在线%车辆管理%系统,基于Idea,html,css,vue,java,springboot,mysql
java·spring boot·vscode·html5·web app
龙茶清欢3 小时前
在 Spring Cloud Gateway 中实现跨域(CORS)的两种主要方式
java·spring boot·spring cloud·微服务·gateway