95、STL与建造者模式:复杂对象构建、分步构建、产品表示

建造者模式,说白了就是「分步造东西」。

我刚开始接触设计模式时,觉得这玩意儿不就是写一堆 setter 吗?后来在项目中遇到一个配置对象,构造函数有十几个参数,每次调用都得翻文档看参数顺序……嗯,从那以后我再也不敢小看建造者模式了。

为什么需要建造者模式?

你想想看,如果一个对象有 8 个可选参数,你打算怎么构造它?

  • 写 2^8 = 256 个构造函数重载?不现实。
  • 全用默认参数?调用方根本分不清哪个参数对应哪个位置。
  • 用 setter 一个个设?对象可能处于「半成品」状态,线程不安全。

建造者模式就是来解决这个问题的。它把对象的构造过程拆成多个步骤,每一步只设置一个或一组属性,最后一步才「产出」最终对象。

核心三要素:

  • 产品(Product):最终要构建的复杂对象
  • 建造者(Builder):定义分步构建的接口
  • 指挥者(Director):控制构建步骤的顺序(可选)

STL 里有没有建造者模式?

严格来说,STL 没有直接提供 Builder 类。但 STL 的设计哲学和建造者模式高度契合——尤其是 std::stringstreamstd::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 的引用。这样就能一路点下去,代码读起来像在念句子。

建造者模式的核心逻辑

下面这张图帮你理清建造者模式中各个角色的关系:

建造者模式核心结构 客户端 (Client) 指挥者 (Director) 建造者接口 (Builder) 具体建造者 (ConcreteBuilder) 产品 (Product) 定义分步构建接口 stepA(), stepB(), getResult() 实现具体构建步骤 持有产品实例 最终复杂对象 包含多个部件

分步构建的三种常见形式

形式 特点 适用场景 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 个以上,或者调用方经常搞错参数顺序时,再重构为建造者模式。时机很重要。


公众号:蓝海资料掘金营,微信deep3321