建造者模式:复杂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个,或者参数之间有复杂的依赖关系时,才考虑建造者模式。

建造者模式的核心流程图

建造者模式核心流程 客户端 指挥者(Director) 建造者接口 Builder SQLQueryBuilder 具体建造者A PizzaBuilder 具体建造者B 产品 SQLQuery / Pizza 调用 使用 同样的构建过程(指挥者控制),可以创建不同的产品(具体建造者实现)

建造者模式 vs 传统构造方式

对比维度 传统构造方式 建造者模式
参数可读性 差,需要记住参数顺序 好,方法名即说明
可选参数 需要写多个重载构造 天然支持,不调用即可
参数校验 在构造函数中校验 在 build() 中统一校验
不可变对象 需要额外处理 天然支持,产品类不暴露setter
链式调用 不支持 天然支持

总结一下:建造者模式最适合的场景是——对象构造参数多、参数之间有依赖关系、或者构造过程需要分步骤进行。我个人在项目中用得最多的地方就是:SQL构建器、配置对象构建、以及各种DTO的组装。记住一句话:当构造函数开始变得臃肿时,就该考虑建造者模式了。


公众号:蓝海资料掘金营,微信deep3321