建造者模式(Builder):复杂对象的构建,链式调用,与工厂模式的区别,C++实现示例
说实话,很多人在学设计模式时,容易把建造者模式和工厂模式搞混。我当年刚入行时也踩过这个坑。今天咱们就把建造者模式彻底讲透,顺便聊聊它和工厂模式到底差在哪。
什么是建造者模式?
建造者模式,说白了就是把复杂对象的构建过程拆解成多个步骤。你想想看,如果一个对象有十几个参数要设置,有些还是可选的,有些还有依赖关系——这时候直接写构造函数,那代码简直没法看。
我习惯把建造者模式理解成「组装电脑」。你买零件(CPU、内存、硬盘),然后一步步装起来。每一步都很清晰,而且你可以选择装什么不装什么。建造者模式就是这个思路。
核心思想:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
什么时候该用建造者模式?
我个人总结了几条判断标准:
- 对象构造参数太多——超过4、5个参数,尤其是很多可选参数时
- 构造过程有步骤依赖——比如必须先设置A,才能设置B
- 需要生成不同变体——同样的构建流程,产出不同配置的对象
- 想避免「望远镜构造函数」——就是那种参数列表越来越长的构造函数
我在项目中遇到过最典型的场景是配置对象的构建。一个网络请求配置,可能有URL、超时时间、重试次数、认证信息、代理设置……如果用构造函数,你得写多少个重载版本?
链式调用:让代码读起来像散文
链式调用是建造者模式的一个「杀手级应用」。每个设置方法都返回this指针,这样就能连续调用。代码读起来就像在描述一个过程:
HttpRequest request = HttpRequest::Builder()
.setUrl("https://api.example.com")
.setTimeout(30)
.setRetryCount(3)
.addHeader("Authorization", "Bearer xxx")
.build();
你看,这代码是不是很直观?不用翻来翻去找参数位置,也不用数括号。我特别喜欢这种写法,因为它把「做什么」和「怎么做」分开了。
小技巧:链式调用中,每个方法返回Builder&而不是Builder,可以避免不必要的拷贝。这是C++特有的优化点。
建造者模式 vs 工厂模式
嗯,这里要注意。很多人问这两者有什么区别。我直接给你画个对比表:
| 对比维度 | 建造者模式 | 工厂模式 |
|---|---|---|
| 关注点 | 构建过程(步骤、顺序) | 创建结果(返回什么对象) |
| 复杂度 | 适合复杂对象,多步骤 | 适合简单对象,一步到位 |
| 灵活性 | 可以控制每一步的细节 | 封装了创建逻辑,细节隐藏 |
| 典型场景 | 配置对象、文档生成、UI布局 | 对象池、单例、多态创建 |
| 链式调用 | 常见(返回Builder自身) | 不常见(通常返回产品对象) |
简单来说:工厂模式关心「造什么」,建造者模式关心「怎么造」。如果你只需要一行代码就能创建对象,用工厂;如果创建过程需要好几步,而且步骤可以变化,用建造者。
C++实现示例:一个完整的建造者模式
咱们写一个实际的例子。假设我们要构建一个DatabaseConfig对象,它有很多配置项:
#include <iostream>
#include <string>
#include <optional>
class DatabaseConfig {
public:
// 产品类 - 最终要构建的对象
std::string host;
int port;
std::string username;
std::string password;
std::string database;
int maxConnections;
bool useSSL;
int timeoutSeconds;
// 建造者类 - 嵌套在内部
class Builder {
private:
std::string m_host = "localhost";
int m_port = 3306;
std::string m_username = "root";
std::string m_password;
std::string m_database = "test";
int m_maxConnections = 10;
bool m_useSSL = false;
int m_timeoutSeconds = 30;
public:
Builder& setHost(const std::string& host) {
m_host = host;
return *this;
}
Builder& setPort(int port) {
m_port = port;
return *this;
}
Builder& setCredentials(const std::string& user,
const std::string& pass) {
m_username = user;
m_password = pass;
return *this;
}
Builder& setDatabase(const std::string& db) {
m_database = db;
return *this;
}
Builder& setMaxConnections(int max) {
m_maxConnections = max;
return *this;
}
Builder& enableSSL(bool enable = true) {
m_useSSL = enable;
return *this;
}
Builder& setTimeout(int seconds) {
m_timeoutSeconds = seconds;
return *this;
}
// 最终构建方法
DatabaseConfig build() {
// 这里可以做校验逻辑
if (m_password.empty()) {
throw std::runtime_error("密码不能为空!");
}
if (m_port < 1 || m_port > 65535) {
throw std::runtime_error("端口号无效!");
}
DatabaseConfig config;
config.host = m_host;
config.port = m_port;
config.username = m_username;
config.password = m_password;
config.database = m_database;
config.maxConnections = m_maxConnections;
config.useSSL = m_useSSL;
config.timeoutSeconds = m_timeoutSeconds;
return config;
}
};
// 提供一个静态方法获取Builder
static Builder create() {
return Builder();
}
void print() const {
std::cout << "数据库配置:\n"
<< " 主机: " << host << "\n"
<< " 端口: " << port << "\n"
<< " 用户: " << username << "\n"
<< " 数据库: " << database << "\n"
<< " 最大连接: " << maxConnections << "\n"
<< " SSL: " << (useSSL ? "启用" : "禁用") << "\n"
<< " 超时: " << timeoutSeconds << "秒\n";
}
};
// 使用示例
int main() {
try {
// 链式调用,只设置需要的参数
DatabaseConfig config = DatabaseConfig::create()
.setHost("db.example.com")
.setPort(5432)
.setCredentials("admin", "secure_pass_123")
.setDatabase("orders")
.setMaxConnections(50)
.enableSSL(true)
.setTimeout(60)
.build();
config.print();
// 也可以只设置部分参数,其余用默认值
DatabaseConfig simpleConfig = DatabaseConfig::create()
.setCredentials("guest", "guest_pass")
.setDatabase("analytics")
.build();
simpleConfig.print();
} catch (const std::exception& e) {
std::cerr << "构建失败: " << e.what() << std::endl;
}
return 0;
}
避坑指南:我曾经在项目里犯过一个错误——把建造者的build()方法设计成移动语义,结果用户忘记接收返回值,对象就析构了。后来我改成返回std::optional<Config>或者直接抛异常,强迫调用方处理结果。
建造者模式的核心流程图
下面这张图展示了建造者模式的完整工作流程:
建造者模式的变体:Director(导演)
有时候,我们会引入一个Director角色。它的作用是封装一套固定的构建流程。比如,你有一个「生产环境配置」的固定模板,每次都用同样的步骤:
class ConfigDirector {
public:
static DatabaseConfig createProductionConfig() {
return DatabaseConfig::create()
.setHost("prod-db.internal")
.setPort(3306)
.setMaxConnections(200)
.enableSSL(true)
.setTimeout(120)
.build();
}
static DatabaseConfig createDevConfig() {
return DatabaseConfig::create()
.setHost("localhost")
.setPort(3306)
.setMaxConnections(5)
.enableSSL(false)
.setTimeout(30)
.build();
}
};
这样,团队成员不用每次都手动写一长串链式调用,直接调用ConfigDirector::createProductionConfig()就行。Director 把「构建过程」也封装起来了。
总结一下
建造者模式的核心价值在于:把复杂对象的构建过程从使用代码中解耦出来。它让代码更可读、更可维护,也更容易扩展。
我个人建议,当你发现一个类的构造函数参数超过3个,或者构造逻辑中有「如果A设置了,B才能设置」这样的依赖关系时,就考虑用建造者模式。别等到代码写成一团乱麻再重构,那时候改起来就痛苦了。
记住一句话:好的代码不是写出来的,是设计出来的。建造者模式就是帮你「设计」出优雅代码的一个好工具。