建造者模式
建造者模式
Rif设计模式笔记:建造者模式 (Builder Pattern)
一、一句话概括
将一个复杂对象的构建过程与其最终表示相分离,使得同样的构建过程可以创建出不同配置的对象。通俗地说,就是 “把组装电脑的活儿交给专业的装机师傅,你只需要告诉他要什么配置就行了”。
二、为什么需要它 (Why - The Pain Point)
想象一下,当你要创建一个具有很多属性的对象时,会遇到哪些困境:
**伸缩构造函数 (Telescoping Constructor Problem)**:为了应对不同的属性组合,你可能会创建一堆重载的构造函数。
1
2
3
4
5
6// 痛点:构造函数爆炸,参数列表又长又乱
public Pizza(String dough, String sauce) { ... }
public Pizza(String dough, String sauce, String cheese) { ... }
public Pizza(String dough, String sauce, String cheese, String topping1) { ... }
// 调用时像噩梦:
Pizza myPizza = new Pizza("Thin Crust", "Tomato", "Mozzarella", null, "Pepperoni", null);这种代码可读性极差,且非常容易传错参数。
JavaBeans 模式的缺陷:另一种方式是提供一个无参构造函数,然后通过一系列
setter方法来设置属性。1
2
3
4
5
6// 痛点:对象在构建过程中状态不一致,且无法保证不可变
Pizza myPizza = new Pizza();
myPizza.setDough("Thin Crust");
myPizza.setSauce("Tomato");
// ... 在调用 build() 之前,myPizza 是一个不完整的“半成品”
myPizza.setCheese("Mozzarella");这种方式有两个主要问题:
- 状态不一致:在所有
setter被调用完毕之前,这个Pizza对象处于一个不完整、不可用的状态。 - 无法实现不可变性:由于
setter方法是公开的,对象在创建后可以被随意修改,这在多线程环境下或需要稳定状态的场景中是危险的。
- 状态不一致:在所有
核心问题:如何创建一个拥有众多参数(尤其是可选参数)的复杂对象,同时保证过程清晰、对象状态一致且最终不可变?
三、它是什么 (What - The Solution)
建造者模式优雅地解决了上述问题。它引入了一个独立的 Builder 对象,专门负责接收参数和构建最终的产品(Product)。
核心思想:
- **产品 (Product)**:这是我们想要创建的复杂对象(如
Computer,Pizza)。它的构造函数是私有的,防止外部直接用new创建。 - 建造者 (Builder):这是一个帮助类,通常作为产品的静态内部类。
- 它拥有与产品完全相同的属性。
- 它提供一系列流畅的(fluent)设置方法(如
cpu("i9")、ram("32G")),每个方法都返回Builder自身,以支持链式调用。 - 它有一个最终的
build()方法,负责用当前收集到的所有属性来创建并返回一个完整的产品实例。
- **客户端 (Client)**:客户端通过
Builder来配置和构建对象,代码清晰明了,且产品一旦通过build()创建,就是完整的、不可变的。
C++ 背景对比 & Java 关键点:
- 与C++的联系:C++ 也可以通过方法链实现类似效果,但 Java 的静态内部类是实现该模式的“黄金搭档”。它将
Builder和Product紧密地组织在一起,逻辑上清晰,访问权限也易于控制(Builder可以访问Product的私有构造函数)。 - Java 的实现精髓:
- 静态内部类 (
public static class Builder): 使得你可以直接通过Product.Builder来创建建造者,而不需要先有一个Product实例。 - 链式调用 (Method Chaining):
builder.foo().bar().baz(),代码如行云流水。 - 不可变性 (Immutability):
Product的所有字段都设为final,并且只在私有构造函数中赋值一次,创建后无法更改。
- 静态内部类 (
四、怎么做 (How - The Blueprint)
基本步骤(以构建一台电脑为例):
- 创建最终产品类
Computer:- 将其所有字段声明为
final。 - 创建一个私有的构造函数,它接受一个
Builder对象作为参数,并用Builder的字段来初始化自己的final字段。
- 将其所有字段声明为
- 在
Computer内部,创建public static class Builder:- 在
Builder中,定义与Computer完全相同的字段(但非final)。 Builder的构造函数只接受必选参数。- 为所有可选参数创建设置方法(如
gpu(...),motherboard(...))。这些方法必须返回Builder自身 (return this;)。
- 在
- 在
Builder中,创建build()方法:- 这个方法调用
Computer的私有构造函数new Computer(this),将自己作为参数传入,从而创建并返回一个Computer实例。 - 可以在
build()方法中添加校验逻辑,确保对象状态的合法性。
- 这个方法调用
Java 示例代码 (经典实现)
1 | // 1. 最终产品类 (Product) |
五、UML 类图
下面是上述例子的 PlantUML 代码。
1 | @startuml |
图示解读:
Computer.Builder是Computer的一个内部类。BuilderPatternDemo(客户端) 依赖于Computer.Builder来配置和创建对象。Computer.Builder的build()方法最终会创建出一个Computer实例。
六、优缺点分析
优点 (Pros):
- 极高的可读性:通过描述性的方法名进行配置,代码意图清晰。
new Computer.Builder("i9", "32G").gpu("4090").build()比new Computer("i9", "32G", null, "4090")好一万倍。 - 强制对象不可变性:可以轻松地创建出不可变对象,这对于并发编程和系统稳定性至关重要。
- 封装构建细节:客户端无需知道对象内部的构建细节和约束。复杂的校验逻辑可以封装在
build()方法中。 - 灵活性和可扩展性:添加新的可选参数非常容易,只需在
Builder中增加一个字段和一个方法即可,不会影响到现有的客户端代码。
缺点 (Cons):
- 代码量增加:需要为每个
Product类额外编写一个Builder类,这导致了字段的重复定义和更多的代码。对于简单的对象(属性少于4个),这有点“杀鸡用牛刀”。 - 创建开销:在创建最终产品之前,需要先创建一个
Builder对象。虽然这点开销在大多数情况下可以忽略不计,但也是一个成本。
七、在 Java 中的应用
建造者模式在现代 Java 开发中非常流行,尤其是在构建配置类、请求对象等方面。
- **
StringBuilder和StringBuffer**:这是最经典的例子。你通过append()方法(相当于 builder 的配置方法)一步步构建字符串,最后通过toString()方法(相当于build())得到最终结果。 - **
java.util.stream.Stream.Builder**:用于手动构建一个Stream。 - Lombok 框架的
@Builder注解:这是一个“代码生成器”,你只需在一个类上添加@Builder注解,Lombok 就会在编译时自动为你生成完整的建造者模式代码,极大地减少了样板代码。 - 各大API客户端:例如 AWS SDK, Google Cloud Client Libraries 等,它们的请求对象(
S3PutObjectRequest等)大量使用建造者模式来配置复杂的API请求参数。