建造者模式:模式动机与定义、角色与结构、C++代码实现、与工厂模式的区别
说实话,建造者模式是我在项目中用得最「舒服」的一个创建型模式。为什么?因为它解决了一个很实在的问题:当一个对象有太多配置项,或者构造过程需要分几步走的时候,代码就容易变得又臭又长。我早期写过一个渲染引擎,初始化一个场景对象要传十几个参数,每次调用构造函数都像在填高考志愿——顺序不能错,缺了还不行。后来重构时用了建造者模式,整个世界清净了。
模式动机与定义
先聊聊动机。你想想看,如果一个类的构造函数有七八个参数,甚至有些参数还是可选的,你会怎么写?大部分人可能会写多个重载构造函数,或者用默认参数。但这样做的后果是:构造函数列表越来越长,调用方根本分不清哪个参数对应哪个含义。更糟糕的是,如果参数之间有依赖关系(比如必须先设置A才能设置B),那构造函数根本没法表达这种约束。
建造者模式的动机就是:将一个复杂对象的构建过程与它的表示分离。说白了,就是让同样的构建过程可以创建出不同的对象。比如你做一个电脑配置系统,同样的组装流程(选CPU、选内存、选硬盘),最后可以组装出游戏本、办公本、工作站——这就是建造者模式的精髓。
角色与结构
建造者模式里,有四个核心角色。我习惯把它们记成「导演、建造者、具体建造者、产品」——嗯,就像拍电影一样。
- 产品(Product):最终要创建的复杂对象。它通常包含多个部件。
- 抽象建造者(Builder):定义创建产品各个部件的抽象接口。一般会声明几个构建方法,比如
buildPartA()、buildPartB()。 - 具体建造者(ConcreteBuilder):实现抽象建造者接口,负责实际组装产品的部件。它还会提供一个
getResult()方法,返回最终产品。 - 导演(Director):控制构建过程的顺序。它不关心具体建造者是谁,只负责调用建造者的构建方法。
下面这张图可以帮你快速理清关系:
C++代码实现
咱们直接上代码。假设我们要构建一台电脑,电脑有CPU、内存、硬盘三个部件。不同的配置组合可以生成不同定位的电脑。
// 产品类
class Computer {
public:
void setCPU(const std::string& cpu) { cpu_ = cpu; }
void setMemory(const std::string& mem) { memory_ = mem; }
void setStorage(const std::string& storage) { storage_ = storage; }
void showSpecs() const {
std::cout << "CPU: " << cpu_ << "\n"
<< "Memory: " << memory_ << "\n"
<< "Storage: " << storage_ << std::endl;
}
private:
std::string cpu_;
std::string memory_;
std::string storage_;
};
// 抽象建造者
class ComputerBuilder {
public:
virtual ~ComputerBuilder() = default;
virtual void buildCPU() = 0;
virtual void buildMemory() = 0;
virtual void buildStorage() = 0;
virtual Computer getResult() = 0;
};
// 具体建造者:游戏本
class GamingComputerBuilder : public ComputerBuilder {
public:
void buildCPU() override { computer_.setCPU("Intel i9-13900K"); }
void buildMemory() override { computer_.setMemory("32GB DDR5"); }
void buildStorage() override { computer_.setStorage("1TB NVMe SSD"); }
Computer getResult() override { return computer_; }
private:
Computer computer_;
};
// 具体建造者:办公本
class OfficeComputerBuilder : public ComputerBuilder {
public:
void buildCPU() override { computer_.setCPU("Intel i5-13400"); }
void buildMemory() override { computer_.setMemory("16GB DDR4"); }
void buildStorage() override { computer_.setStorage("512GB SSD"); }
Computer getResult() override { return computer_; }
private:
Computer computer_;
};
// 导演类
class Director {
public:
void setBuilder(ComputerBuilder* builder) { builder_ = builder; }
Computer construct() {
builder_->buildCPU();
builder_->buildMemory();
builder_->buildStorage();
return builder_->getResult();
}
private:
ComputerBuilder* builder_;
};
// 客户端使用
int main() {
Director director;
GamingComputerBuilder gamingBuilder;
director.setBuilder(&gamingBuilder);
Computer gamingPC = director.construct();
gamingPC.showSpecs();
OfficeComputerBuilder officeBuilder;
director.setBuilder(&officeBuilder);
Computer officePC = director.construct();
officePC.showSpecs();
return 0;
}
Director 的 construct() 方法设计成可以接受多个建造者参数,或者支持链式调用。这样在测试不同配置时特别方便。
与工厂模式的区别
这个问题我面试时经常被问到。很多人觉得建造者和工厂模式很像,都是创建对象嘛。但它们的关注点完全不同。
| 对比维度 | 工厂模式 | 建造者模式 |
|---|---|---|
| 核心关注点 | 创建哪个产品(产品族/产品等级) | 如何一步步构建产品 |
| 产品复杂度 | 通常创建单一对象,部件较少 | 创建复杂对象,包含多个部件 |
| 构建过程 | 一步到位,通常一个方法返回产品 | 分步骤,由导演控制顺序 |
| 客户端控制 | 客户端只需指定类型 | 客户端可以自定义构建步骤 |
| 典型场景 | 日志记录器、数据库连接 | 复杂文档、游戏角色、配置对象 |
说白了,工厂模式是「我要一个XX产品」,建造者模式是「我要按照这个流程做一个产品」。我记得有一次做报表系统,需要生成不同格式的报表(PDF、Excel、HTML)。如果用工厂模式,你得为每种格式写一个工厂类。但用建造者模式,你只需要写一个报表建造者接口,然后让导演控制「添加标题→添加表格→添加图表」这个流程,不同建造者输出不同格式——代码复用率直接拉满。
另外,还有一个容易混淆的点:建造者模式与工厂模式可以组合使用。比如,你可以用抽象工厂来创建不同系列的建造者,然后再用导演去构建产品。这种组合在大型框架中很常见,比如GUI库中创建不同风格(Windows、Mac、Linux)的对话框。
嗯,总结一下我的经验:当你发现一个对象的构造过程需要「分步执行」且「步骤可能变化」时,就该考虑建造者模式了。它让代码的可读性和可维护性都上了一个台阶。但别滥用,简单场景就用简单方案——这是架构师的基本素养。