95、STL与建造者模式:复杂对象构建、分步构建、产品表示
建造者模式,说白了就是「分步造东西」。
我刚开始接触设计模式时,觉得这玩意儿不就是写一堆 setter 吗?后来在项目中遇到一个配置对象,构造函数有十几个参数,每次调用都得翻文档看参数顺序……嗯,从那以后我再也不敢小看建造者模式了。
为什么需要建造者模式?
你想想看,如果一个对象有 8 个可选参数,你打算怎么构造它?
- 写 2^8 = 256 个构造函数重载?不现实。
- 全用默认参数?调用方根本分不清哪个参数对应哪个位置。
- 用 setter 一个个设?对象可能处于「半成品」状态,线程不安全。
建造者模式就是来解决这个问题的。它把对象的构造过程拆成多个步骤,每一步只设置一个或一组属性,最后一步才「产出」最终对象。
核心三要素:
- 产品(Product):最终要构建的复杂对象
- 建造者(Builder):定义分步构建的接口
- 指挥者(Director):控制构建步骤的顺序(可选)
STL 里有没有建造者模式?
严格来说,STL 没有直接提供 Builder 类。但 STL 的设计哲学和建造者模式高度契合——尤其是 std::stringstream 和 std::locale 的 facet 构建过程。
我个人习惯把 std::stringstream 看作一个「文本建造者」:
#include <sstream>
#include <iomanip>
std::stringstream builder;
builder << "Name: " << std::setw(10) << "Alice";
builder << ", Age: " << 30;
builder << ", Score: " << std::fixed << std::setprecision(2) << 95.5;
std::string result = builder.str();
// result = "Name: Alice, Age: 30, Score: 95.50"
看到了吗?每一步只追加一部分内容,最后调用 .str() 拿到完整产品。这就是建造者模式的精髓。
实战:用 STL 容器实现建造者模式
我在项目中遇到过这样一个场景:需要构建一个复杂的查询条件对象,包含多个字段的过滤、排序、分页信息。用建造者模式来搞,代码清晰多了。
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <sstream>
// 产品:查询条件
class Query {
public:
std::string table;
std::vector<std::string> fields;
std::map<std::string, std::string> filters;
std::string orderBy;
bool ascending;
int limit;
int offset;
std::string toSQL() const {
std::stringstream ss;
ss << "SELECT ";
if (fields.empty()) ss << "*";
else {
for (size_t i = 0; i < fields.size(); ++i) {
if (i > 0) ss << ", ";
ss << fields[i];
}
}
ss << " FROM " << table;
if (!filters.empty()) {
ss << " WHERE ";
bool first = true;
for (auto& [k, v] : filters) {
if (!first) ss << " AND ";
ss << k << " = '" << v << "'";
first = false;
}
}
if (!orderBy.empty()) {
ss << " ORDER BY " << orderBy;
ss << (ascending ? " ASC" : " DESC");
}
if (limit > 0) ss << " LIMIT " << limit;
if (offset > 0) ss << " OFFSET " << offset;
return ss.str();
}
};
// 建造者
class QueryBuilder {
private:
Query query;
public:
QueryBuilder& from(const std::string& table) {
query.table = table;
return *this;
}
QueryBuilder& select(const std::vector<std::string>& fields) {
query.fields = fields;
return *this;
}
QueryBuilder& where(const std::string& key, const std::string& value) {
query.filters[key] = value;
return *this;
}
QueryBuilder& orderBy(const std::string& field, bool asc = true) {
query.orderBy = field;
query.ascending = asc;
return *this;
}
QueryBuilder& limit(int n) {
query.limit = n;
return *this;
}
QueryBuilder& offset(int n) {
query.offset = n;
return *this;
}
Query build() {
return query;
}
};
int main() {
Query q = QueryBuilder()
.from("users")
.select({"id", "name", "email"})
.where("status", "active")
.where("age", "25")
.orderBy("name", true)
.limit(10)
.offset(0)
.build();
std::cout << q.toSQL() << std::endl;
// SELECT id, name, email FROM users WHERE status = 'active' AND age = '25' ORDER BY name ASC LIMIT 10 OFFSET 0
return 0;
}
链式调用的关键:每个 setter 方法都返回 *this 的引用。这样就能一路点下去,代码读起来像在念句子。
建造者模式的核心逻辑
下面这张图帮你理清建造者模式中各个角色的关系:
分步构建的三种常见形式
| 形式 | 特点 | 适用场景 | STL 对应 |
|---|---|---|---|
| 链式调用 | 每个方法返回 *this |
配置对象、查询构建 | std::stringstream << |
| 分步填充 | 按固定顺序调用多个方法 | 文档生成、协议打包 | std::back_inserter |
| 参数对象 | 用临时对象收集参数 | 复杂函数调用 | std::locale::facet |
避坑指南:建造者模式常见问题
我曾经踩过的一个坑:建造者内部直接暴露了产品对象的指针,结果调用方在 build() 之后还能通过指针修改产品内部状态。这破坏了不可变性。
正确的做法是:build() 返回产品对象的副本,或者把产品类的构造函数设为私有,只让建造者能调用。
另外,建造者模式不是万能的。如果你的对象只有两三个参数,直接用构造函数或聚合初始化就行,别为了用模式而用模式。
用 STL 容器做「参数收集器」
我个人很喜欢用 std::vector<std::pair<std::string, std::string>> 来做参数收集器。它比 map 更灵活,能保留插入顺序,也允许重复键。
#include <vector>
#include <string>
#include <utility>
class HttpRequestBuilder {
private:
std::string method_;
std::string url_;
std::vector<std::pair<std::string, std::string>> headers_;
std::string body_;
public:
HttpRequestBuilder& method(const std::string& m) { method_ = m; return *this; }
HttpRequestBuilder& url(const std::string& u) { url_ = u; return *this; }
HttpRequestBuilder& header(const std::string& k, const std::string& v) {
headers_.emplace_back(k, v);
return *this;
}
HttpRequestBuilder& body(const std::string& b) { body_ = b; return *this; }
std::string build() const {
std::stringstream ss;
ss << method_ << " " << url_ << " HTTP/1.1\r\n";
for (auto& [k, v] : headers_) {
ss << k << ": " << v << "\r\n";
}
if (!body_.empty()) {
ss << "Content-Length: " << body_.size() << "\r\n\r\n";
ss << body_;
} else {
ss << "\r\n";
}
return ss.str();
}
};
你看,用 vector<pair> 来收集 header,既保留了添加顺序,又支持重复的 header 名(比如多个 Set-Cookie)。这就是 STL 容器和建造者模式结合的魅力。
小结
建造者模式的核心思想就一句话:把复杂对象的构造过程拆成多个简单步骤。
STL 虽然没有直接提供 Builder 类,但它的流式操作、容器适配器、迭代器模式,都和建造者模式的精神一脉相承。你在写代码时,如果发现某个对象的构造函数越来越臃肿,不妨停下来想想——是不是该引入一个建造者了?
我的建议:不要一开始就上建造者模式。先写简单的构造函数,等参数膨胀到 5 个以上,或者调用方经常搞错参数顺序时,再重构为建造者模式。时机很重要。