深入浅出设计模式【四、建造者模式】
一、建造者模式介绍
建造者模式旨在将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
当创建一个对象需要很多步骤,或者需要大量参数(其中许多是可选的或需要复杂初始化)时,直接使用构造方法或 setter 方法会导致代码难以编写、阅读和维护。建造者模式通过提供一个专用的“导演”(Director)和“建造者”(Builder)来一步步指导对象的构造,最终返回一个完整的产品。
二、核心概念与意图
-
核心概念:
- 产品 (Product): 最终要构建的复杂对象。
- 抽象建造者 (Builder): 声明了创建产品各个部件的抽象方法/接口,通常还包括一个返回最终产品的方法(如
getProduct())。 - 具体建造者 (Concrete Builder): 实现抽象建造者接口,定义具体的构建步骤和逻辑。它持有当前正在构建的产品实例并负责组装它。
- 导演 (Director): 负责安排建造的步骤和顺序。它接收一个建造者对象,并按照特定的逻辑调用其构建方法。客户端通常与导演交互。
- 客户端 (Client): 创建导演和建造者对象,并将建造者对象传递给导演以开始构建过程。
-
意图:
- 将复杂对象的构建过程分离,使得构建过程可以独立于产品的组成部分。
- 通过一步步的构建过程,可以精细控制最终产品的内部结构和表示。
- 避免“伸缩构造函数” (Telescoping Constructor) 和过多的 setter 方法,提高代码的可读性和可维护性。
三、适用场景剖析
建造者模式在以下场景中非常有效:
- 创建复杂对象: 当对象的创建过程非常复杂,包含多个步骤或部件,并且这些步骤需要按照特定顺序执行时。
- 构造过程需要允许有不同的表示: 同样的构建过程(由导演控制)可以通过不同的具体建造者来创建不同内部表示的产品。
- 避免过多构造函数参数: 当对象的构造函数参数过多(特别是许多可选参数)时,使用建造者模式可以避免令人困惑的构造函数重载(伸缩构造函数模式)。
- 需要创建不可变对象: 建造者模式非常适合创建不可变对象。可以在建造过程中设置所有属性,并在最终
build()方法中一次性构造一个完整且不可变的对象。
四、类图解析
以下Mermaid类图清晰地展示了建造者模式的结构和角色间的关系:
Director: 持有对Builder的引用。它的construct()方法定义了构建产品的固定步骤和顺序。它只知道要调用哪些构建方法,但不知道具体如何构建。Builder: 抽象接口,声明了构建产品各个部件(buildPartX)和获取最终产品(getResult)的方法。ConcreteBuilder: 实现Builder接口。它负责:- 创建和组装产品的各个部件。
- 提供一个返回最终构建好的产品的方法。
- 可以定义并跟踪当前产品的构建状态。
Product: 最终被构建的复杂对象。它包含多个部件。
调用流程: 客户端创建一个 ConcreteBuilder 并将其传递给 Director。Director 指导构建过程。最后,客户端从 ConcreteBuilder 中获取构建好的 Product。
五、各种实现方式及其优缺点
1. 经典实现(GoF风格)
即上述UML所描述的方式,包含Director和抽象的Builder接口。
- 优点:
- 分离度高: 将构建过程(Director)、构建实现(ConcreteBuilder)和最终产品(Product)完全分离。
- 精细控制: 可以对构建过程进行非常精细的控制。
- 支持不同的表示: 通过替换ConcreteBuilder,可以用相同的构建过程创建不同的产品。
- 缺点:
- 结构复杂: 需要定义多个类(Director, Builder, ConcreteBuilder),如果产品不复杂,显得过于繁重。
- 产品必须可分步构建: 要求产品的组成部分必须允许分步设置。
2. 流式接口(Fluent Interface)建造者(现代Java常用)
这种变体将建造者内嵌到产品类中,并省略了Director角色,通过链式方法调用(Method Chaining)来指导构建过程。这是目前最流行、最直观的实现方式。
public class Computer {
// 必需参数
private final String cpu;
private final String ram;
// 可选参数
private final int usbCount;
private final String keyboard;
// 私有构造函数,只能通过Builder构建
private Computer(ComputerBuilder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.usbCount = builder.usbCount;
this.keyboard = builder.keyboard;
}
// 静态内部类:建造者
public static class ComputerBuilder {
// 必需参数
private final String cpu;
private final String ram;
// 可选参数 - 有默认值
private int usbCount = 2;
private String keyboard = "标准键盘";
// 构造必需参数
public ComputerBuilder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
// 设置可选参数的方法,返回this以支持链式调用
public ComputerBuilder setUsbCount(int usbCount) {
this.usbCount = usbCount;
return this;
}
public ComputerBuilder setKeyboard(String keyboard) {
this.keyboard = keyboard;
return this;
}
// 最终的build方法,创建产品实例
public Computer build() {
// 可以在此处进行校验
if (usbCount < 0) {
throw new IllegalArgumentException("USB数量不能为负");
}
return new Computer(this);
}
}
}
// 客户端使用
Computer computer = new Computer.ComputerBuilder("Intel i7", "16GB")
.setUsbCount(3)
.setKeyboard("机械键盘")
.build(); // 一次性构建不可变对象
- 优点:
- 代码可读性极高: 链式调用非常清晰,就像在配置对象一样。
- 模拟命名可选参数: 解决了Java没有命名可选参数的问题。
- 可以构建不可变对象: 所有参数在构造函数中一次性设置,对象创建后不可变,是线程安全的。
- 可进行参数校验: 可以在
build()方法中进行集中校验,保证对象构建的合法性。
- 缺点:
- 冗长: 需要为每个产品类编写一个对应的建造者类,代码量会增加。
- 通常省略Director: 构建逻辑分散在客户端代码中,如果构建顺序非常重要,可能不如经典实现安全。
六、最佳实践
- 优先使用流式接口建造者: 对于大多数需要大量可选参数的场景,流式接口建造者是首选,因为它极大地提高了客户端代码的可读性和可维护性。
- 将建造者作为静态内部类: 这将建造者与产品紧密关联,并且可以访问产品的私有构造函数,从而强制客户端必须通过建造者来创建产品。
- 在
build()方法中进行校验: 确保在构建对象之前,所有参数组合是有效和一致的。如果无效,抛出IllegalStateException或IllegalArgumentException。 - 用于创建不可变对象: 这是创建不可变对象的最佳方式之一。将所有属性设置为
final,并通过建造者一次性初始化。 - 与工厂模式区分:
- 工厂模式: 关心的是实例化什么对象(what),通常是立即返回创建好的对象。
- 建造者模式: 关心的是如何实例化对象(how),通过一步步的配置,最后才创建对象。它更适合构建复杂对象。
七、在开发中的演变和应用
建造者模式的思想在现代开发中得到了广泛应用和演变:
-
Lombok的
@Builder注解: Java库Lombok通过一个简单的注解自动生成流式接口建造者类的样板代码,极大地减少了编码工作量,使建造者模式的应用变得极其普遍。import lombok.Builder; import lombok.ToString; @Builder @ToString public class User { private final String name; private final int age; private final String email; } // 自动生成的建造者,客户端可直接使用 User user = User.builder() .name("Alice") .age(30) .email("alice@example.com") .build(); -
Spring Framework:
BeanDefinitionBuilder: 在Spring底层,用于以编程方式、分步地构建复杂的BeanDefinition对象。UriComponentsBuilder: 用于分步构建URI,是流式接口建造者的完美例子。
UriComponents uriComponents = UriComponentsBuilder .fromPath("/api/users/{id}") .queryParam("format", "json") .buildAndExpand(123); -
Java 8 Stream API: 虽然形式不同,但
Stream的构建和收集过程体现了建造者模式的思想。你通过中间操作(filter,map)一步步构建一个处理管道,最后通过终端操作(collect)得到最终结果。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
StringBuilder&StringBuffer: 这是建造者模式的经典案例,尽管它们没有完全遵循GoF的定义。它们用于分步构建一个复杂的字符串(产品),最后通过toString()方法返回最终产品。 -
java.nio.IntBuffer/CharBuffer等: 这些缓冲区类的创建和配置也使用了建造者模式的思想。 -
Google Guava: 这个库大量使用了建造者模式。例如:
ImmutableList.builder(),ImmutableSet.builder()CacheBuilder: 用于构建一个复杂的缓存实例,是流式接口的典范。
Cache<String, User> cache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .concurrencyLevel(4) .build(); -
Jackson库: 用于JSON反序列化的
JsonParserFactory和用于序列化的JsonGeneratorFactory都可以看作是建造者,用于配置和构建解析器/生成器实例。 -
测试框架(如Mockito): 创建和配置Mock对象的过程也常常采用建造者模式,使得配置非常清晰。
User mockUser = Mockito.mock(User.class, Mockito.withSettings() .name("testUser") .defaultAnswer(Mockito.RETURNS_SMART_NULLS));
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 创建型设计模式 |
| 核心意图 | 将复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 |
| 关键角色 | 产品 (Product)、建造者 (Builder)、具体建造者 (ConcreteBuilder)、导演 (Director) |
| 主要优点 | 1. 良好的封装性:隐藏产品构建的复杂细节。 2. 良好的灵活性:通过不同的建造者可以实现不同的产品表示。 3. 便于控制构建步骤:导演可以精细控制逐步的构建过程。 4. 解耦:将构建过程与产品本身解耦。 5. 提高可读性(流式):链式调用清晰易懂。 |
| 主要缺点 | 1. 增加代码量:需要创建多个新的类。 2. 产品需具备共性:产品之间的差异不能太大,否则需要定义过多的建造者。 3. Director缺失(流式):流式实现中,构建逻辑可能分散在客户端。 |
| 适用场景 | 1. 对象有很多可选参数或需要多个初始化步骤。 2. 希望创建不可变对象。 3. 同样的构建过程需要产生不同的结果产品。 4. 对象创建过程复杂。 |
| 关系与对比 | vs. 工厂模式: 工厂关注立即创建产品,建造者关注分步创建复杂产品。 vs. 抽象工厂: 抽象工厂创建产品家族,建造者创建一种复杂产品。 |
| 现代应用 | Lombok @Builder 使其成为Java中处理多参数构造的事实标准。Guava 和 Spring 广泛用于构建复杂组件。 |
建造者模式,特别是其流式接口变体,是解决“伸缩构造函数”反模式和创建不可变复杂对象的利器。它在现代Java开发中无处不在,极大地提升了代码的可读性、安全性和可维护性,是每一位工程师必须掌握的核心模式。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120856

浙公网安备 33010602011771号