建造者模式(Builder):产品类、抽象建造者、具体建造者、指挥者,以及链式调用实现与不可变对象构建
建造者模式,说白了就是帮你把「复杂对象的构造过程」和「它的最终表示」拆开。我刚开始接触这个模式时,总觉得它跟工厂模式差不多,后来在项目中踩过坑才明白——工厂模式是「我要一个产品,你给我造出来」,而建造者模式是「我要一个产品,但它的组装步骤很复杂,每一步我可能还想自定义」。
你想想看,如果一个对象有十几个字段,其中一半还是可选的,你怎么办?写十几个重载的构造函数?那代码简直没法看。用 setter?又破坏了不可变性。嗯,这时候建造者模式就派上用场了。
模式的核心角色
建造者模式有四个核心角色,我习惯把它们记成「一个产品、一个抽象蓝图、一个具体施工队、一个项目经理」:
- 产品类(Product):最终要构建的那个复杂对象。它通常包含多个部件,而且这些部件可能有不同的组合方式。
- 抽象建造者(Builder):定义构建产品各个部件的抽象接口。说白了就是「你要造这个产品,必须实现这些步骤」。
- 具体建造者(ConcreteBuilder):实现抽象建造者的接口,负责具体部件的构建,并提供获取最终产品的方法。
- 指挥者(Director):控制构建过程的顺序和逻辑。它不关心具体怎么造,只关心「先做什么,后做什么」。
核心要点:建造者模式最大的价值在于,它将「复杂对象的构建算法」与「部件的具体装配」解耦了。同样的构建过程,换不同的建造者,就能得到不同的产品表示。
SVG 结构图:建造者模式的核心逻辑
经典实现:以「计算机配置」为例
我记得有一次帮朋友组装电脑,发现这简直就是建造者模式的现实版——CPU、内存、硬盘、显卡,每个部件都可以选不同的品牌和型号,但组装流程是固定的。咱们就用这个例子来写代码。
1. 产品类:Computer
// Java 版本
public class Computer {
private final String cpu;
private final String memory;
private final String hardDisk;
private final String gpu;
private final String powerSupply;
// 构造函数私有,只能通过 Builder 创建
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.memory = builder.memory;
this.hardDisk = builder.hardDisk;
this.gpu = builder.gpu;
this.powerSupply = builder.powerSupply;
}
// 只提供 getter,不提供 setter —— 不可变对象
public String getCpu() { return cpu; }
public String getMemory() { return memory; }
public String getHardDisk() { return hardDisk; }
public String getGpu() { return gpu; }
public String getPowerSupply() { return powerSupply; }
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", memory='" + memory + '\'' +
", hardDisk='" + hardDisk + '\'' +
", gpu='" + gpu + '\'' +
", powerSupply='" + powerSupply + '\'' +
'}';
}
// 静态内部 Builder 类
public static class Builder {
private String cpu;
private String memory;
private String hardDisk;
private String gpu;
private String powerSupply;
public Builder cpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder memory(String memory) {
this.memory = memory;
return this;
}
public Builder hardDisk(String hardDisk) {
this.hardDisk = hardDisk;
return this;
}
public Builder gpu(String gpu) {
this.gpu = gpu;
return this;
}
public Builder powerSupply(String powerSupply) {
this.powerSupply = powerSupply;
return this;
}
public Computer build() {
// 可以在这里做参数校验
if (cpu == null || memory == null) {
throw new IllegalStateException("CPU 和内存是必选部件");
}
return new Computer(this);
}
}
}
个人习惯:我一般把 Builder 作为产品类的静态内部类。这样 Builder 可以直接访问产品的私有构造函数,而且从语义上看,Builder 和 Product 本来就是强关联的。
2. 抽象建造者与具体建造者
如果我们需要不同的「配置套餐」,比如「办公版」和「游戏版」,就可以用抽象建造者来规范流程:
// Java 版本:抽象建造者
public interface ComputerBuilder {
ComputerBuilder buildCpu();
ComputerBuilder buildMemory();
ComputerBuilder buildHardDisk();
ComputerBuilder buildGpu();
ComputerBuilder buildPowerSupply();
Computer build();
}
// 具体建造者:办公版
public class OfficeComputerBuilder implements ComputerBuilder {
private Computer.Builder builder = new Computer.Builder();
@Override
public ComputerBuilder buildCpu() {
builder.cpu("Intel i5");
return this;
}
@Override
public ComputerBuilder buildMemory() {
builder.memory("16GB DDR4");
return this;
}
@Override
public ComputerBuilder buildHardDisk() {
builder.hardDisk("512GB SSD");
return this;
}
@Override
public ComputerBuilder buildGpu() {
builder.gpu("集成显卡");
return this;
}
@Override
public ComputerBuilder buildPowerSupply() {
builder.powerSupply("400W");
return this;
}
@Override
public Computer build() {
return builder.build();
}
}
// 具体建造者:游戏版
public class GamingComputerBuilder implements ComputerBuilder {
private Computer.Builder builder = new Computer.Builder();
@Override
public ComputerBuilder buildCpu() {
builder.cpu("Intel i9");
return this;
}
@Override
public ComputerBuilder buildMemory() {
builder.memory("32GB DDR5");
return this;
}
@Override
public ComputerBuilder buildHardDisk() {
builder.hardDisk("1TB NVMe SSD");
return this;
}
@Override
public ComputerBuilder buildGpu() {
builder.gpu("NVIDIA RTX 4080");
return this;
}
@Override
public ComputerBuilder buildPowerSupply() {
builder.powerSupply("850W 金牌");
return this;
}
@Override
public Computer build() {
return builder.build();
}
}
3. 指挥者:Director
// Java 版本
public class ComputerDirector {
private ComputerBuilder builder;
public ComputerDirector(ComputerBuilder builder) {
this.builder = builder;
}
// 控制构建顺序
public Computer construct() {
return builder
.buildCpu()
.buildMemory()
.buildHardDisk()
.buildGpu()
.buildPowerSupply()
.build();
}
}
4. 客户端使用
// Java 版本
public class Client {
public static void main(String[] args) {
// 方式一:直接使用 Builder(链式调用)
Computer computer1 = new Computer.Builder()
.cpu("AMD Ryzen 7")
.memory("32GB DDR5")
.hardDisk("1TB SSD")
.gpu("RTX 4070")
.powerSupply("750W")
.build();
System.out.println(computer1);
// 方式二:通过指挥者 + 具体建造者
ComputerBuilder officeBuilder = new OfficeComputerBuilder();
ComputerDirector director = new ComputerDirector(officeBuilder);
Computer officeComputer = director.construct();
System.out.println(officeComputer);
// 换一个建造者,得到不同配置
ComputerBuilder gamingBuilder = new GamingComputerBuilder();
director = new ComputerDirector(gamingBuilder);
Computer gamingComputer = director.construct();
System.out.println(gamingComputer);
}
}
C++ 版本实现
C++ 的实现思路完全一样,只是语法上有些差异。我个人在 C++ 项目中更喜欢用智能指针来管理产品对象的生命周期:
// C++ 版本
#include <iostream>
#include <string>
#include <memory>
class Computer {
public:
// 前向声明 Builder
class Builder;
std::string getCpu() const { return cpu_; }
std::string getMemory() const { return memory_; }
std::string getHardDisk() const { return hardDisk_; }
std::string getGpu() const { return gpu_; }
std::string getPowerSupply() const { return powerSupply_; }
void print() const {
std::cout << "Computer: " << cpu_ << ", "
<< memory_ << ", " << hardDisk_ << ", "
<< gpu_ << ", " << powerSupply_ << std::endl;
}
private:
Computer() = default;
std::string cpu_;
std::string memory_;
std::string hardDisk_;
std::string gpu_;
std::string powerSupply_;
friend class Builder;
};
class Computer::Builder {
public:
Builder& cpu(const std::string& cpu) {
computer_.cpu_ = cpu;
return *this;
}
Builder& memory(const std::string& memory) {
computer_.memory_ = memory;
return *this;
}
Builder& hardDisk(const std::string& hardDisk) {
computer_.hardDisk_ = hardDisk;
return *this;
}
Builder& gpu(const std::string& gpu) {
computer_.gpu_ = gpu;
return *this;
}
Builder& powerSupply(const std::string& powerSupply) {
computer_.powerSupply_ = powerSupply;
return *this;
}
Computer build() {
// 参数校验
if (computer_.cpu_.empty() || computer_.memory_.empty()) {
throw std::runtime_error("CPU and memory are required");
}
return computer_;
}
private:
Computer computer_;
};
// 使用示例
int main() {
Computer computer = Computer::Builder()
.cpu("Intel i7")
.memory("16GB")
.hardDisk("512GB SSD")
.gpu("RTX 3060")
.powerSupply("650W")
.build();
computer.print();
return 0;
}
链式调用的实现技巧
链式调用说白了就是每个 setter 方法都返回 this(或者返回 Builder 本身)。这里有个小细节:
- 返回类型必须是 Builder 本身,不能是 void 或其它类型。
- 方法名建议用名词,比如
.cpu()、.memory(),而不是.setCpu()。这样读起来更像是在描述产品,而不是在调用方法。 - build() 方法做最终校验。我曾经在项目中发现有人漏掉了必填字段,结果运行时才报错。所以我在 build() 里加了参数校验,提前暴露问题。
不可变对象的构建
为什么建造者模式适合构建不可变对象?因为产品类的构造函数是私有的,所有字段都在构造时通过 Builder 传入,并且只提供 getter 不提供 setter。这样一旦对象创建完成,它的状态就固定了。
我曾经踩过的坑:在 Builder 中直接暴露了产品对象的引用,导致客户端可以在 build() 之后还能修改产品状态。后来我强制在 build() 方法中做一次深拷贝,或者像上面那样,让产品类的构造函数接收 Builder 并复制所有字段,彻底切断引用。
建造者模式 vs 工厂模式
| 对比维度 | 建造者模式 | 工厂模式 |
|---|---|---|
| 关注点 | 对象的构建过程(步骤、顺序) | 对象的创建结果(返回什么类型) |
| 产品复杂度 | 适合复杂对象(多个部件) | 适合简单对象或同一接口的不同实现 |
| 构建步骤 | 可以精细控制每一步 | 通常一步完成 |
| 链式调用 | 天然支持 | 通常不支持 |
| 典型场景 | 配置对象、文档生成、套餐组装 | 日志记录器、数据库连接、对象池 |
适用场景总结
什么时候该用建造者模式?我总结了三条:
- 对象的构造参数很多(超过4个),而且大部分是可选的。用建造者模式可以避免「 telescoping constructor 反模式」。
- 需要构建不可变对象。Builder 模式是构建不可变对象的标准手法,Java 标准库中的
StringBuilder、Stream.Builder都是例子。 - 同样的构建过程可以产生不同的表示。比如同一个文档导出为 PDF、HTML、Markdown 三种格式,只需要换三个不同的建造者。
一句话总结:建造者模式把「怎么做」和「做什么」分开了。指挥者决定怎么做(构建顺序),具体建造者决定做什么(每个部件用什么料)。而链式调用和不可变对象,是这个模式最实用的两个衍生能力。
好了,这一章的内容就到这里。建造者模式在实际项目中用得非常多,尤其是配置类和 DTO 的构建。下次你写一个构造函数参数超过三个的类时,不妨试试用 Builder 来重构——你会发现代码瞬间清爽很多。