建造者模式:复杂SQL查询构建器
建造者模式,说白了就是解决「对象太复杂,构造函数不够用」的问题。
我刚开始接触这个模式时,觉得它有点多余。直到我在项目中写了一个SQL构建器,参数从3个涨到15个,构造函数越来越长,调用方根本分不清哪个参数对应哪个位置。嗯,那时候我才意识到——建造者模式,真香。
模式核心:构建与表示分离
官方定义说:「将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。」
翻译成人话就是:你只管告诉我你要什么,怎么拼装是我的事。
举个例子。你去披萨店点餐:
- 「我要一个9寸的,厚底,加芝士、培根、蘑菇」
- 「我要一个12寸的,薄底,加海鲜、青椒、洋葱」
这两个披萨的构建过程是一样的——选尺寸、选底、选配料。但最终出来的披萨完全不同。这就是建造者模式。
核心三要素:
- 产品(Product):最终要构建的复杂对象
- 建造者(Builder):定义构建步骤的接口
- 指挥者(Director):控制构建流程,按顺序调用建造者的方法
业务场景一:复杂SQL查询构建器
我在做数据中台项目时,遇到过最头疼的事——动态SQL拼接。
用户在前端勾选各种条件:选哪些表、要不要Join、按什么排序、要不要分组。后台如果写死SQL,那代码就炸了。如果手动拼接字符串,又容易出SQL注入。
建造者模式正好解决这个问题。
先看产品类:SQLQuery
public class SQLQuery {
private String select;
private String from;
private List<String> joins;
private List<String> where;
private List<String> groupBy;
private List<String> orderBy;
private Integer limit;
private Integer offset;
// 只提供getter,不提供setter
// 构建过程全部交给Builder
public String build() {
StringBuilder sb = new StringBuilder();
sb.append("SELECT ").append(select);
sb.append(" FROM ").append(from);
joins.forEach(j -> sb.append(" ").append(j));
if (!where.isEmpty()) {
sb.append(" WHERE ").append(String.join(" AND ", where));
}
if (!groupBy.isEmpty()) {
sb.append(" GROUP BY ").append(String.join(", ", groupBy));
}
if (!orderBy.isEmpty()) {
sb.append(" ORDER BY ").append(String.join(", ", orderBy));
}
if (limit != null) sb.append(" LIMIT ").append(limit);
if (offset != null) sb.append(" OFFSET ").append(offset);
return sb.toString();
}
}
再看建造者:SQLQueryBuilder
public class SQLQueryBuilder {
private SQLQuery query = new SQLQuery();
public SQLQueryBuilder select(String columns) {
query.setSelect(columns);
return this;
}
public SQLQueryBuilder from(String table) {
query.setFrom(table);
return this;
}
public SQLQueryBuilder join(String joinClause) {
query.getJoins().add(joinClause);
return this;
}
public SQLQueryBuilder where(String condition) {
query.getWhere().add(condition);
return this;
}
public SQLQueryBuilder groupBy(String column) {
query.getGroupBy().add(column);
return this;
}
public SQLQueryBuilder orderBy(String column, boolean asc) {
query.getOrderBy().add(column + (asc ? " ASC" : " DESC"));
return this;
}
public SQLQueryBuilder limit(int limit) {
query.setLimit(limit);
return this;
}
public SQLQueryBuilder offset(int offset) {
query.setOffset(offset);
return this;
}
public SQLQuery build() {
return query;
}
}
注意看,每个方法都返回 this。这就是链式调用的关键。
调用方怎么用?
SQLQuery query = new SQLQueryBuilder()
.select("u.name, o.order_date, o.total")
.from("users u")
.join("JOIN orders o ON u.id = o.user_id")
.where("u.status = 'active'")
.where("o.total > 100")
.orderBy("o.order_date", false)
.limit(20)
.offset(0)
.build();
System.out.println(query.build());
// 输出:
// SELECT u.name, o.order_date, o.total
// FROM users u
// JOIN orders o ON u.id = o.user_id
// WHERE u.status = 'active' AND o.total > 100
// ORDER BY o.order_date DESC
// LIMIT 20 OFFSET 0
你看,调用方完全不用关心SQL是怎么拼装的。他只管告诉建造者「我要什么条件」,最后调用 build() 拿结果。
避坑指南:我曾经在项目中直接用字符串拼接SQL,结果被DBA骂了一顿——因为用户输入的条件里带了单引号,直接导致SQL语法错误。后来改用建造者模式,所有参数都经过预编译处理,再也没出过问题。
业务场景二:披萨对象构建
第二个场景更生活化——点披萨。
披萨的复杂度在于:尺寸、底饼、酱料、芝士、配料,每个维度都有多种选择。如果用构造函数,你得写多少个重载?
// 不用建造者模式,调用方得这样写:
Pizza pizza = new Pizza("large", "thick", "tomato", "mozzarella",
Arrays.asList("pepperoni", "mushroom", "onion"));
// 问题来了:如果用户不要芝士呢?如果用户要双倍芝士呢?
// 构造函数根本没法灵活处理
用建造者模式重构
public class Pizza {
private String size; // small, medium, large
private String crust; // thin, thick, stuffed
private String sauce; // tomato, bbq, pesto
private String cheese; // mozzarella, cheddar, vegan
private List<String> toppings;
private boolean extraCheese;
private boolean glutenFree;
private Pizza() {} // 私有构造,只能通过Builder创建
// 内部建造者类
public static class Builder {
private Pizza pizza = new Pizza();
public Builder size(String size) {
pizza.size = size;
return this;
}
public Builder crust(String crust) {
pizza.crust = crust;
return this;
}
public Builder sauce(String sauce) {
pizza.sauce = sauce;
return this;
}
public Builder cheese(String cheese) {
pizza.cheese = cheese;
return this;
}
public Builder addTopping(String topping) {
if (pizza.toppings == null) {
pizza.toppings = new ArrayList<>();
}
pizza.toppings.add(topping);
return this;
}
public Builder extraCheese(boolean extra) {
pizza.extraCheese = extra;
return this;
}
public Builder glutenFree(boolean gf) {
pizza.glutenFree = gf;
return this;
}
public Pizza build() {
// 可以在这里做校验
if (pizza.size == null) {
throw new IllegalStateException("尺寸不能为空");
}
return pizza;
}
}
}
调用方体验
Pizza pizza = new Pizza.Builder()
.size("large")
.crust("thick")
.sauce("tomato")
.cheese("mozzarella")
.addTopping("pepperoni")
.addTopping("mushroom")
.addTopping("onion")
.extraCheese(true)
.build();
// 如果用户想点一个简单的芝士披萨:
Pizza simple = new Pizza.Builder()
.size("medium")
.crust("thin")
.sauce("tomato")
.cheese("mozzarella")
.build();
你想想看,如果不用建造者模式,这种「有些参数可选、有些参数必填、有些参数可以重复添加」的场景,用构造函数怎么处理?要么写一堆重载,要么传null,代码可读性极差。
注意:建造者模式不是万能的。如果对象只有两三个属性,直接用构造函数或setter就行,别为了用模式而用模式。我个人习惯是:当构造参数超过4个,或者参数之间有复杂的依赖关系时,才考虑建造者模式。
建造者模式的核心流程图
建造者模式 vs 传统构造方式
| 对比维度 | 传统构造方式 | 建造者模式 |
|---|---|---|
| 参数可读性 | 差,需要记住参数顺序 | 好,方法名即说明 |
| 可选参数 | 需要写多个重载构造 | 天然支持,不调用即可 |
| 参数校验 | 在构造函数中校验 | 在 build() 中统一校验 |
| 不可变对象 | 需要额外处理 | 天然支持,产品类不暴露setter |
| 链式调用 | 不支持 | 天然支持 |
总结一下:建造者模式最适合的场景是——对象构造参数多、参数之间有依赖关系、或者构造过程需要分步骤进行。我个人在项目中用得最多的地方就是:SQL构建器、配置对象构建、以及各种DTO的组装。记住一句话:当构造函数开始变得臃肿时,就该考虑建造者模式了。
公众号:蓝海资料掘金营,微信deep3321