建造者模式(Builder Pattern) 主要用于解决"复杂对象的构建"问题。当一个类的构造函数参数过多(特别是很多可选参数)时,直接实例化会变得非常混乱。
为了直观对比,我们来看看在没有模式和使用模式下的代码差异。
1. 不使用建造者模式(重载构造函数)
这种方式通常被称为"伸缩式构造函数(Telescoping Constructor)"。当参数增加时,代码的可读性和维护性会迅速下降。
#include <iostream>
#include <string>
class Computer {
public:
// 必须提供多个构造函数来处理不同的配置组合
Computer(std::string cpu, std::string ram) : cpu(cpu), ram(ram) {}
Computer(std::string cpu, std::string ram, std::string gpu)
: cpu(cpu), ram(ram), gpu(gpu) {}
void show() {
std::cout << "配置: CPU=" << cpu << ", RAM=" << ram
<< ", GPU=" << (gpu.empty() ? "集成显卡" : gpu) << std::endl;
}
private:
std::string cpu;
std::string ram;
std::string gpu; // 可选参数
};
int main() {
// 缺点:参数含义不直观,容易传错顺序,且参数组合多时构造函数爆炸
Computer myPC("Intel i9", "32GB", "RTX 4090");
myPC.show();
return 0;
}
2. 使用建造者模式
建造者模式将构建过程 与表示分离。它允许你通过链式调用一步步配置对象。
#include <iostream>
#include <string>
// 产品类
class Computer {
public:
void setCPU(std::string c) { cpu = c; }
void setRAM(std::string r) { ram = r; }
void setGPU(std::string g) { gpu = g; }
void show() {
std::cout << "[Builder模式] 配置: CPU=" << cpu
<< ", RAM=" << ram
<< ", GPU=" << (gpu.empty() ? "集成显卡" : gpu) << std::endl;
}
private:
std::string cpu, ram, gpu;
};
// 抽象建造者
class ComputerBuilder {
public:
virtual ~ComputerBuilder() {}
virtual void buildCPU() = 0;
virtual void buildRAM() = 0;
virtual void buildGPU() = 0;
virtual Computer* getResult() = 0;
};
// 具体建造者:游戏电脑
class GamingPCBuilder : public ComputerBuilder {
private:
Computer* computer;
public:
GamingPCBuilder() { computer = new Computer(); }
void buildCPU() override { computer->setCPU("AMD Ryzen 9"); }
void buildRAM() override { computer->setRAM("64GB"); }
void buildGPU() override { computer->setGPU("RTX 5090"); }
Computer* getResult() override { return computer; }
};
// 指挥者(Director):负责构建步骤的顺序
class Director {
public:
void construct(ComputerBuilder* builder) {
builder->buildCPU();
builder->buildRAM();
builder->buildGPU();
}
};
int main() {
Director director;
GamingPCBuilder builder;
director.construct(&builder);
Computer* myPC = builder.getResult();
myPC->show();
delete myPC;
return 0;
}
核心区别对比
| 特性 | 不使用模式 (直接构造) | 使用建造者模式 |
|---|---|---|
| 可读性 | 参数多时非常糟糕(不知道第3个参数是什么) | 非常清晰,通过方法名(如 buildGPU)明确含义 |
| 灵活性 | 构造函数固定,难以处理复杂的逻辑分支 | 可以根据需要灵活组合不同的构建步骤 |
| 解耦 | 客户端必须了解对象的所有属性 | 客户端只需知道 Builder,实现了配置与表现的分离 |
| 适用场景 | 简单对象,参数少且固定 | 复杂对象,参数多,或同样的构建过程产生不同表现 |
什么时候用?
如果你发现你的构造函数长得像 Computer("i7", "16G", "SSD", "RTX", "750W", "LiquidCooling"),而且有时候你还想省略其中一两个参数,那就是该换成建造者模式的时候了。
需求背景:
-
加配件: 增加"显示器(Monitor)"。
-
加套餐: 增加一个"程序员专用配置"。
方案一:不使用模式(重载构造函数)
在不使用模式的情况下,你的类会变得非常臃肿,且逻辑散落在各处。
1. 更改 Computer 类
你必须修改核心类,增加成员变量和新的构造函数。
class Computer {
public:
// 原有的 3 参数构造函数(为了兼容老代码,不能删)
Computer(string c, string r, string g) : cpu(c), ram(r), gpu(g) {}
// 【修改点 1】:新增 4 参数构造函数,处理显示器需求
Computer(string c, string r, string g, string m)
: cpu(c), ram(r), gpu(g), monitor(m) {}
private:
string cpu, ram, gpu;
string monitor; // 【修改点 2】:新增属性
};
2. 更改调用处(套餐需求)
因为没有 Director,每个需要"程序员套餐"的地方,你都要手动写一遍那一长串参数。
// 【修改点 3】:在 main 或业务逻辑中手动拼装新套餐
Computer* devPC = new Computer("i9", "64G", "集成显卡", "4K显示器");
- 痛点: 如果全公司有 50 个地方要用这个"程序员套餐",你就得写 50 遍这行代码。一旦以后要改成"32G内存",你要改 50 处。
方案二:使用建造者模式(Builder + Director)
这种方案下,我们遵循"对扩展开放,对修改关闭"的原则。
1. 更改 Builder(增加零件)
你只需要给工人增加一个"装显示器"的技能。
// 在 ComputerBuilder 基类和具体类中增加
class ComputerBuilder {
public:
// ...
virtual void buildMonitor() = 0; // 新增接口
};
class GamingPCBuilder : public ComputerBuilder {
public:
void buildMonitor() override { computer->setMonitor("电竞屏"); } // 实现它
};
2. 更改 Director(增加套餐模板)
这是最关键的改进!你把"程序员套餐"定义成了一个标准脚本。
class Director {
public:
// 原有的游戏电脑套餐不动...
// 【修改点】:新增一个指挥方法,定义程序员套餐的步骤
void constructProgrammerPC(ComputerBuilder* b) {
b->buildCPU(); // 假设内部设置 i9
b->buildRAM(); // 假设内部设置 64G
// 注意:这里我不调用 buildGPU(),因为程序员不需要独立显卡
b->buildMonitor(); // 调用新零件:护眼 4K 屏
}
};
深度对比总结
| 维度 | 不用模式(构造函数) | 用模式(Builder + Director) |
|---|---|---|
| 增加配件(显示器) | 你得不断重载构造函数,参数列表越来越长,像"伸缩望远镜"一样可怕。 | 只需在 Builder 里多写一个 build 方法。 |
| 增加套餐(程序员版) | 散乱: 逻辑写在 main 里。如果要改配置,得全局搜索替换。 |
集中: 逻辑写在 Director 里的一个方法里。改配置只需改这一个地方。 |
| 老代码兼容性 | 虽然保留旧构造函数不报错,但类定义里会出现大量重复的赋值逻辑。 | 老代码调用的还是老的 Director 方法,完全无感,代码结构依然清晰。 |
| 构建逻辑 | 无法体现顺序。比如必须先买主板再买 CPU,构造函数管不了。 | Director 可以在方法内严格控制 build 的先后顺序。 |
最终对比结论:
-
如果不加 Director :你确实省了一个类,但你(调用者)就得亲自去记"程序员电脑"需要哪些
.set()。 -
如果加了 Director :你作为调用者,只需要说"给我来份程序员套餐",剩下的复杂度全部被
Director挡住了。
这就是为什么"加需求"时,建造者模式能让你的代码从"一堆乱麻"变成"标准模具"。