1. Builder建造者模式


=== 背景引入


下面这个类,如果我们需要限制条件,例如:

1.age < 10的name不能为Tom

2.age > 10的name不能为Jerry

package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 20:33
 */
public class User {

    private String name;

    private Integer age;

    public User() {
    }

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

}

于是我们需要新增一个方法用于检查判断:

public boolean check() {
    if (age < 10 && "Tom".equals(name)) {
        return false;
    }
    if (age > 10 && "Jerry".equals(name)) {
        return false;
    }
    return true;
}

public static void main(String[] args) {
    User user = new User();
    user.setName("wangcai");
    user.setAge(18);
    boolean check = user.check();
}

问题来了,那我如果在setAge之前调用呢?

public static void main(String[] args) {
    User user = new User();
    user.setName("wangcai");
    boolean check = user.check();
    user.setAge(18);
}

此时由于age还未设置值,是不是无法判断?那怎么可以解决这个问题呢?既能在对象创建的时候设置值又能设置值后就调用检查方法完成检查,由此引申出了建造者模式。


建造者模式是一种创建型设计模式,它将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。


并且我们可以在创建过程的任一步骤进行切入执行更多的业务逻辑。

建造者模式的核心是每个类的构造方法私有化不再提供构造方法(可选),将构造的权利交由一个与该类绑定的内部类 Builder 进行构造。

如下:

public static class Builder {
    public User build() {
        User user = new User();
        user.setName(name);
        user.setAge(age);
        return user;
    }
}

Builder中的build方法构造出User实例进行返回,但是User需要两个参数,因此Builder中也应该具有这两个参数。

public static class Builder {
    
    private String name;
    private Integer age;
    
    public User build() {
        User user = new User();
        user.setName(this.name);
        user.setAge(this.age);
        return user;
    }
    
}

那Builder中的字段值怎么来呢?所以我们需要提供set方法进行设置。

public static class Builder {

        private String name;
        private Integer age;

        public void name(String name) {
            this.name = name;
        }

        public void age(Integer age) {
            this.age = age;
        }

        public User build() {
            User user = new User();
            user.setName(this.name);
            user.setAge(this.age);
            return user;
        }

    }

既然build会进行User对象实例的创建,那我们就可以把校验过程放置在build中,不合法抛出异常即可,这样就把创建对象和校验过程捆绑在一起了。

public static class Builder {

        private String name;
        private Integer age;

        public void name(String name) {
            this.name = name;
        }

        public void age(Integer age) {
            this.age = age;
        }

        public User build() {
            User user = new User();
            user.setName(this.name);
            user.setAge(this.age);
            if (this.age < 10 && "Tom".equals(name)) {
                throw new IllegalArgumentException("姓名不合法");
            }
            if (this.age > 10 && "Jerry".equals(name)) {
                throw new IllegalArgumentException("姓名不合法");
            }
            return user;
        }

接下来即可测试使用:

Useage.java

package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 21:01
 */
public class Useage {
    public static void main(String[] args) {
        User.Builder builder = new User.Builder();
        builder.name("wangcai");
        builder.age(18);
        User user = builder.build();
        System.out.printf("name: %s, age: %d", user.getName(), user.getAge());
    }
}

/*
name: wangcai, age: 18
*/
package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 21:01
 */
public class Useage {
    public static void main(String[] args) {
        User.Builder builder = new User.Builder();
        builder.name("Tom");
        builder.age(9);
        User user = builder.build();
        System.out.printf("name: %s, age: %d", user.getName(), user.getAge());
    }
}

/*
Exception in thread "main" java.lang.IllegalArgumentException: 姓名不合法
	at builder.User$Builder.build(User.java:65)
	at builder.Useage.main(Useage.java:12)
*/

仔细观察会发现和经常用的建造者模式是不是有区别?

1.User.Builder类型显示过于复杂

2.builder.name()/builder.age()就像是调用Builder对象的set方法一样

所以我们可以接着改造User类

私有化Builder类构造方法,通过User提供方法创建

支持链式调用赋值属性(只需要调用赋值方法返回this对象即可,也就是将Builder实例通过方法接着返回,这样就达到能继续.的操作)

User.java

package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 20:33
 */
public class User {

    private String name;

    private Integer age;

    public User() {
    }

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    // get/set方法省略

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {

        private String name;
        private Integer age;

        private Builder() {
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder age(Integer age) {
            this.age = age;
            return this;
        }

        public User build() {
            User user = new User();
            user.setName(this.name);
            user.setAge(this.age);
            if (this.age < 10 && "Tom".equals(name)) {
                throw new IllegalArgumentException("姓名不合法");
            }
            if (this.age > 10 && "Jerry".equals(name)) {
                throw new IllegalArgumentException("姓名不合法");
            }
            return user;
        }

    }

}

Useage.java

package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 21:01
 */
public class Useage {
    public static void main(String[] args) {
        User user = User.builder().name("wangcai").age(18).build();
        System.out.printf("name: %s, age: %d", user.getName(), user.getAge());
    }
}

/*
name: wangcai, age: 18
*/

但是此时User还是支持通过无参、有参构造方法进行创建,理论来说采用了建造者模式后应私有化掉无参构造方法,后续都将基于此演示。

我们会发现一个问题,就是User还暴露着set方法,意思就是哪怕我们通过Builder创建一个合法的User后,因为set方法能调用还是可能更改出不合法的对象,理论说我们通过Builder创建的对象都是一次性或者不可变的对象,例如Web请求中的DTO(不可变)、数据流转的时候的BO对象(一次性)、Http请求时负责发送的HttpClient对象(不可变)、HttpRequest对象(一次性)。

既然对象属性不需要进行更改,那我们直接将字段改为final修饰。

最终的改造结果:

User.java

package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 20:33
 */
public class User {

    private final String name;

    private final Integer age;

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {

        private String name;
        private Integer age;

        private Builder() {
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder age(Integer age) {
            this.age = age;
            return this;
        }

        public User build() {
            User user = new User(this);
            if (this.age < 10 && "Tom".equals(name)) {
                throw new IllegalArgumentException("姓名不合法");
            }
            if (this.age > 10 && "Jerry".equals(name)) {
                throw new IllegalArgumentException("姓名不合法");
            }
            return user;
        }

    }

}

改造完后创建对象一行代码即可搞定,简单又优雅,现在的实现在使用上已经和Lombok注解@Builder几乎一致。

我们可以思考下建造者模式的意义:

1.API更加清爽,这也是现代化编程的使用趋势。

2.我们可以在建造过程中定义一些有意义的方法进行切入操作。

package builder;

/**
 * @author: wangcai
 * @date: 2025/10/22 21:01
 */
public class Useage {
    public static void main(String[] args) {
        User.Builder builder = User.builder().name("wangcai");
        User.Builder builder1 = builder.age(18);
        User user = builder1.build();
        System.out.printf("name: %s, age: %d", user.getName(), user.getAge());
    }
}

在建造过程中,builder和builder1在这个代码中实际上是一个对象,但是非得如此吗?我们是不是可以操作让其有对象的交互过程返回一个不一样的对象。在CompletableFuture中就有这种高级玩法,也就是DSL。

package builder;

import java.util.concurrent.CompletableFuture;

/**
 * @author: wangcai
 * @date: 2025/10/22 21:01
 */
public class Useage {
    public static void main(String[] args) {
        User.Builder builder = User.builder().name("wangcai");
        User.Builder builder1 = builder.age(18);
        System.out.println(builder == builder1);
        User user = builder1.build();
        System.out.printf("name: %s, age: %d", user.getName(), user.getAge());
        System.out.println();
        CompletableFuture.supplyAsync(() -> "a").thenAccept(System.out::println);
    }
}
/*
true
name: wangcai, age: 18
a
*/

DSL(Domain-Specific Language): 是针对特定领域设计的计算机语言,与通用编程语言(GPL)相对,专门解决某个特定领域的问题。

例如:

// Java Stream API - 集合操作DSL
List<String> result = users.stream()
    .filter(user -> user.getAge() > 18)
    .map(User::getName)
    .sorted()
    .collect(Collectors.toList());

=== Java 中的内部 DSL 实现

1.方法链DSL

// 配置构建DSL
public class ConfigBuilder {
    public static void main(String[] args) {
        // 读取配置的DSL
        Config config = ConfigBuilder.create()
            .server(server -> server
                .port(8080)
                .host("localhost")
                .timeout(3000)
            )
            .database(db -> db
                .url("jdbc:mysql://localhost:3306/test")
                .username("admin")
                .password("secret")
            )
            .build();
    }
}

2.Builder模式DSL

// 建造者模式其实就是一种DSL
User user = User.builder()
    .name("张三")
    .age(25)
    .build();

3.函数式DSL

// 业务规则DSL
public class RuleEngine {
    public static void main(String[] args) {
        // 业务规则DSL
        Rule rule = Rule.when(person -> person.getAge() >= 18)
            .and(person -> person.hasDrivingLicense())
            .then(person -> System.out.println("可以租车"))
            .otherwise(person -> System.out.println("不符合租车条件"));
        
        rule.apply(new Person(20, true));
    }
}

=== 手动实现一个简单的数据库操作的DSL


SQL.java

package builder;

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

/**
 * @author: wangcai
 * @date: 2025/10/22 22:24
 */
public class SQL {

    private SQL() {
    }

    public static SQLBuilder builder() {
        return new SQLBuilder();
    }

    public static class SQLBuilder {

        private SQLType sqlType;
        private String[] columns;
        private String table;
        private String where;
        private final Map<String, Object> setMap = new LinkedHashMap<>();

        private SQLBuilder() {
        }

        public SQLBuilder sqlType(SQLType sqlType) {
            this.sqlType = sqlType;
            return this;
        }

        public SQLBuilder columns(String ...columns) {
            this.columns = columns;
            return this;
        }

        public SQLBuilder table(String table) {
            this.table = table;
            return this;
        }

        public SQLBuilder where(String where) {
            this.where = where;
            return this;
        }

        public SQLBuilder set(String key, Object value) {
            this.setMap.put(key, value);
            return this;
        }

        public String build() {
            StringBuilder sql = new StringBuilder();
            switch (sqlType) {
                case SELECT -> {
                    sql.append("SELECT ").append(String.join(", ", columns))
                            .append(" FROM ")
                            .append(table);
                    if (where != null) {
                        sql.append(" WHERE ")
                                .append(where);
                    }
                }
                case UPDATE -> {
                    sql.append("UPDATE ")
                            .append(table)
                            .append(" SET ");
                    String setSql = setMap.entrySet().stream()
                            .map(entry -> {
                                return entry.getKey() + " = " + entry.getValue();
                            })
                            .collect(Collectors.joining(", "));
                    sql.append(setSql);
                    if (where != null) {
                        sql.append(" WHERE ")
                                .append(where);
                    }
                }
                case DELETE -> {

                }
                case INSERT -> {

                }
                default -> {
                    throw new IllegalArgumentException("sqlType is not valid");
                }
            }
            return sql.toString();
        }

    }

    public enum SQLType {
        SELECT,
        UPDATE,
        DELETE,
        INSERT
    }
}

Useage.java

package builder;


/**
 * @author: wangcai
 * @date: 2025/10/22 21:01
 */
public class Useage {
    public static void main(String[] args) {
        String sql = SQL.builder()
                .sqlType(SQL.SQLType.SELECT)
                .table("user")
                .columns("name", "age")
                .where("id = 1")
                .build();
        System.out.println(sql);
        String sql1 = SQL.builder()
                .sqlType(SQL.SQLType.UPDATE)
                .table("user")
                .set("name", "wangcai2")
                .set("age", 99)
                .where("id = 1")
                .build();
        System.out.println(sql1);
    }
}
/*
SELECT name, age FROM user WHERE id = 1
UPDATE user SET name = wangcai2, age = 99 WHERE id = 1
*/

现在我们已经可以成功的构建SELECT和UPDATE语句了,但是还有一个问题,我们的where重复使用的话只会以最后一次增加的为准,这其实不合理,而且我们可以发现在SELECT中是可以使用set方法的,这个API设计也不合理,所以这就是建造者模式的高级用法,我们是可以限制这种行为的。

① 怎么不让语法混杂在一起

前面说过,建造者模式的核心就是提供一个Builder对象负责实例的创建,现在问题就是不管是SELECT还是UPDATE都是由一个Builder对象去构建的,既然如此,那我们就将不同的语句构建隔离即可。

改造后

SQL.java

package builder;

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

public class SQL {

    private SQL() {
    }

    public static SelectBuilder select(String ...columns) {
        return new SelectBuilder(columns);
    }

    public static class SelectBuilder {

        private String[] columns;
        private String table;
        private String where;

        private SelectBuilder() {}

        private SelectBuilder(String[] columns) {
            this.columns = columns;
        }

        public SelectBuilder from(String table) {
            this.table = table;
            return this;
        }

        public SelectBuilder where(String where) {
            this.where = where;
            return this;
        }

        public String build() {
            StringBuilder sql = new StringBuilder();
            sql.append("SELECT ").append(String.join(", ", columns))
                    .append(" FROM ")
                    .append(table);
            if (where != null) {
                sql.append(" WHERE ")
                        .append(where);
            }
            return sql.toString();
        }

    }

    public static UpdateBuilder update() {
        return new UpdateBuilder();
    }

    public static class UpdateBuilder {

        private String table;
        private String where;
        private final Map<String, Object> setMap = new LinkedHashMap<>();

        private UpdateBuilder() {}

        public UpdateBuilder table(String table) {
            this.table = table;
            return this;
        }

        public UpdateBuilder set(String key, Object value) {
            this.setMap.put(key, value);
            return this;
        }

        public UpdateBuilder where(String where) {
            this.where = where;
            return this;
        }

        public String build() {
            StringBuilder sql = new StringBuilder();
            sql.append("UPDATE ")
                    .append(table)
                    .append(" SET ");
            String setSql = setMap.entrySet().stream()
                    .map(entry -> {
                        return entry.getKey() + " = " + entry.getValue();
                    })
                    .collect(Collectors.joining(", "));
            sql.append(setSql);
            if (where != null) {
                sql.append(" WHERE ")
                        .append(where);
            }
            return sql.toString();
        }

    }

}

Useage.java

package builder;

public class Useage {
    public static void main(String[] args) {
        String selectSQL = SQL.select("name", "age")
                .from("user")
                .where("id = 1")
                .build();
        System.out.println(selectSQL);
        String updateSQL = SQL.update()
                .table("user")
                .set("name", "wangcai2")
                .set("age", 99)
                .where("id = 1")
                .build();
                System.out.println(updateSQL);
    }
}
/*
SELECT name, age FROM user WHERE id = 1
UPDATE user SET name = wangcai2, age = 99 WHERE id = 1
*/

现在还有某些API只允许调用一次或者要求按顺序调用的限制还没有实现,现在在SELECT中不能调用set,在UPDATE也不能调用SELECT中的API了,但是比如在SELECT中还是能重复调用from或者调用顺序可以随便写的,显然这样的API的调用是不合理的。

所以我们需要将API分离,每个阶段构建完成返回下一阶段的构建对象,这样我们按顺序去返回就能做到按顺序的调用API而且不能重复的调用,需要重复调用的我们只需要在下一阶段也有对应的方法就行或者将能重复调用的接口中提供最后的build方法即可。

② 隔离API


=== 以Update为例


-定义三个接口

interface TableStage {
    WhereStage table(String table);
}

interface WhereStage {
    SetStage where(String where);
}

interface SetStage {
    SetStage set(String key, Object value);
    String build();
}

让其有固定顺序表->where条件->set条件->build

SQL.java

public static TableStage update() {
    return new UpdateBuilder();
}

public static class UpdateBuilder implements TableStage, SetStage, WhereStage {

    private String table;
    private String where;
    private final Map<String, Object> setMap = new LinkedHashMap<>();

    private UpdateBuilder() {}

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

    @Override
    public UpdateBuilder set(String key, Object value) {
        this.setMap.put(key, value);
        return this;
    }

    @Override
    public UpdateBuilder where(String where) {
        this.where = where;
        return this;
    }

    @Override
    public String build() {
        StringBuilder sql = new StringBuilder();
        sql.append("UPDATE ")
                .append(table)
                .append(" SET ");
        String setSql = setMap.entrySet().stream()
                .map(entry -> {
                    return entry.getKey() + " = " + entry.getValue();
                })
                .collect(Collectors.joining(", "));
        sql.append(setSql);
        if (where != null) {
            sql.append(" WHERE ")
                    .append(where);
        }
        return sql.toString();
    }

}

同理改造SELECT

接口:

interface FromStage {
    WhereStageBySelectAndDelete from(String table);
}

interface WhereStageBySelectAndDelete {
    BuildStage where(String where);
}

interface BuildStage {
    String build();
}

SQL.java

public static FromStage select(String... columns) {
        return new SelectBuilder(columns);
    }

    private static class SelectBuilder implements FromStage, WhereStageBySelectAndDelete, BuildStage {

        private String[] columns;
        private String table;
        private String where;

        private SelectBuilder() {
        }

        private SelectBuilder(String[] columns) {
            this.columns = columns;
        }

        @Override
        public SelectBuilder from(String table) {
            this.table = table;
            return this;
        }

        @Override
        public SelectBuilder where(String where) {
            this.where = where;
            return this;
        }

        @Override
        public String build() {
            StringBuilder sql = new StringBuilder();
            sql.append("SELECT ").append(String.join(", ", columns))
                    .append(" FROM ")
                    .append(table);
            if (where != null) {
                sql.append(" WHERE ")
                        .append(where);
            }
            return sql.toString();
        }

    }

Useage.java

String built1 = SQL.select("name", "age")
        .from("user1")
        .where("id = 111")
        .build();
System.out.println(built1);
/*
SELECT name, age FROM user1 WHERE id = 111
*/

DELETE和UPDATE可自行动手实践一下,特别是INSERT方法,应该会让你在解决过程中印象更深。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐