设计模式-建造者模式
案例分析
思考这样一个场景,现在需要实现一个简单的线程池,但是有一些要求,如果不传队列长度字段,就必须传最大线程数,原因是这样的处理方式为来一个请求就创建一个新线程,如果不限制最大线程数可能会导致资源耗尽系统崩溃。
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 语句就失去意义了。
总结
通过以上的例子也可以总结出建造者模式要解决什么样的问题:
- 需要在生成最终的实例前进行参数的合法性校验
- 避免出现无效的中间态对象。