Skip to content

建造者模式 (Builder) —— 乐高积木怎么搭

前言

在深入探讨建造者模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 建造者模式是什么?

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

通俗解释

  • 组装电脑:你需要选 CPU、显卡、内存、硬盘、电源、机箱。而且安装顺序有讲究(先把 CPU 装主板上,再装风扇)。
  • 肯德基点餐:汉堡 + 薯条 + 可乐 = 套餐 A。虽然都是这几样东西,但组合方式不同,结果就不同。

1.1 为什么我们需要它?

核心痛点构造函数参数过多(伸缩构造函数反模式)

假设我们要创建一个 Computer 对象,它有几十个参数。 烂代码示例

java
public class Computer {
    public Computer(String cpu, String gpu, int ram, int storage, boolean hasBluetooth, boolean hasWifi, String os, ...) {
        // ...
    }
}

// 客户端调用
Computer pc = new Computer("Intel i9", "RTX 4090", 32, 1024, true, true, "Windows 11", ...);

问题

  1. 参数太难记:第 5 个参数是蓝牙还是 WiFi?传错了编译器又不报错(都是 boolean),运行时才发现 bug。
  2. 可选参数处理麻烦:如果我不需要蓝牙,我不仅要传 false,甚至可能要写多个重载的构造函数。
  3. 可读性极差:这一行代码写出来,没人知道那一堆 true, false, null 到底代表什么。

2. Java 实战演练:组装高配电脑

我们要创建一个 Computer 类,核心配置(CPU, RAM)是必填的,其他配置(蓝牙, WiFi)是选填的。

2.1 目标类 (Computer)

注意:我们将构造函数私有化,强制通过 Builder 来创建。

java
public class Computer {
    private String cpu; // 必填
    private String ram; // 必填
    private int usbCount; // 选填
    private String keyboard; // 选填
    private String display; // 选填

    // 私有构造函数,参数是 Builder
    private Computer(Builder builder) {
        this.cpu = builder.cpu;
        this.ram = builder.ram;
        this.usbCount = builder.usbCount;
        this.keyboard = builder.keyboard;
        this.display = builder.display;
    }

    @Override
    public String toString() {
        return "Computer{" +
                "cpu='" + cpu + '\'' +
                ", ram='" + ram + '\'' +
                ", usbCount=" + usbCount +
                ", keyboard='" + keyboard + '\'' +
                ", display='" + display + '\'' +
                '}';
    }

    // 静态内部类 Builder
    public static class Builder {
        private String cpu; // 必填
        private String ram; // 必填
        private int usbCount = 0; // 选填,给默认值
        private String keyboard; // 选填
        private String display; // 选填

        // 构造函数仅包含必填参数
        public Builder(String cpu, String ram) {
            this.cpu = cpu;
            this.ram = ram;
        }

        // 链式调用方法:设置完属性后返回 this
        public Builder setUsbCount(int usbCount) {
            this.usbCount = usbCount;
            return this;
        }

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

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

        // 核心方法:构建对象
        public Computer build() {
            return new Computer(this);
        }
    }
}

2.2 客户端调用

代码变得非常清晰,像读文章一样。

java
public class Client {
    public static void main(String[] args) {
        Computer pc = new Computer.Builder("Intel i9", "32GB")
                .setUsbCount(4)
                .setKeyboard("Cherry MX")
                .setDisplay("4K Monitor")
                .build();

        System.out.println(pc);
    }
}

3. 现代 Java 开发:Lombok 的 @Builder

在实际工作中,我们很少手写 Builder 模式,因为代码太冗余了。Lombok 插件通过注解帮我们生成所有代码。

java
import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class User {
    private String name;
    private int age;
    private String email;
}

// 客户端调用
public class Client {
    public static void main(String[] args) {
        User user = User.builder()
                .name("Zhang San")
                .age(25)
                .email("zhangsan@example.com")
                .build();
        
        System.out.println(user);
    }
}

注意:Lombok 默认生成的 Builder 不会强制区分“必填”和“选填”参数。如果需要强制必填,还是需要手动写一点代码或者配合 @NonNull 注解。

4. 源码中的建造者模式

JDK 和常用框架中大量使用了 Builder 模式:

  1. java.lang.StringBuilder: 这是最著名的 Builder。

    java
    StringBuilder sb = new StringBuilder();
    sb.append("Hello").append(" ").append("World"); // 链式调用
    String result = sb.toString(); // build()

    虽然它没有叫 Builder,但 append() 方法返回 this,最后 toString() 返回结果,完全符合 Builder 模式的思想。

  2. java.util.stream.Stream.Builder: 用于手动构建流。

    java
    Stream<String> stream = Stream.<String>builder()
        .add("a")
        .add("b")
        .add("c")
        .build();
  3. Spring 的 BeanDefinitionBuilder: 用于编程式地定义 Spring Bean。

    java
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyService.class);
    builder.addPropertyReference("dao", "myDao");
    BeanDefinition beanDef = builder.getBeanDefinition();

5. 优缺点与适用场景总结

优点

  • 链式调用,代码可读性高:不再是一堆看不懂的参数列表。
  • 参数校验:可以在 build() 方法中集中进行参数校验,确保构建出的对象是合法的。
  • 不可变性:通常 Computer 类的属性都是 final 的(虽然示例中没加),Builder 模式非常适合构建不可变对象 (Immutable Object)。

缺点

  • 代码冗余:为了创建一个对象,必须多创建一个 Builder 对象。对于只有两三个参数的简单类,没必要用 Builder。
  • 属性重复:Builder 类必须复制目标类的所有属性。

适用场景

  1. 构造函数参数超过 4 个,且很多参数是可选的。
  2. 创建的对象是复杂的,需要分步骤构建。
  3. 需要创建不可变对象。

6. 结语

建造者模式是解决“长参数列表”的良药。在 Effective Java 中,Josh Bloch 也极力推荐:当构造方法参数过多时,考虑用 Builder 模式。配合 Lombok,它几乎成为了现代 Java Bean 的标配。

7. 实验实操

design-patterns-web 项目中,你可以体验建造者模式构建复杂对象的过程——制作汉堡。 点击“Beef Patty”、“Cheese”、“Lettuce”等按钮,就像调用 Builder 的各个步骤方法(如 .addPatty(), .addCheese())。右侧的汉堡会实时根据你的选择一层层堆叠起来。最后点击“Complete Order”,Builder 会自动检查并加上顶层的面包(Top Bun),完成最终产品的构建。这展示了如何分步骤构建一个复杂的对象。 截图建议:截取构建过程中包含多层食材的汉堡,以及点击完成后的最终形态。