把一个复杂对象的构建过程与它的内部表示分离,让同样的构建流程能造出不同的产品。—— 创建型模式第四站,前三天依次学完单例、工厂方法、抽象工厂,今天解决的是"对象太复杂、构造函数参数爆炸"的问题。
建造者模式(Builder Pattern):将一个复杂对象的构建过程与它的表示相分离,使得同样的构建过程可以创建不同的表示。
翻译成大白话:把"怎么组装"(流程)和"组装成什么"(内容)拆开。流程是稳定不变的,内容是可以替换的。它是 GoF 23 种设计模式中第 4 个登场的创建型模式,紧跟在抽象工厂之后——因为两者都关心"如何创建对象",但思路截然不同(第⑪节会重点对比)。
一句话定位:当对象的构造函数参数多到"看一眼就头大"、或者创建对象需要按固定顺序做一系列步骤时,Builder 就是你的解药。
先看一段"没有使用设计模式"的真实坏味道代码。假设我们在开发一个电脑配置系统,一个 Computer 对象有 CPU、GPU、内存、硬盘、操作系统、是否带 WiFi、是否带蓝牙、重量等属性。最常见的写法是"伸缩构造函数"(Telescoping Constructor):
// 坏味道示例:构造函数参数爆炸
#include <string>
class Computer {
public:
// 参数越来越多,每个可选特性都要加一个默认参数
Computer(const std::string& cpu, const std::string& gpu, int ramGb,
const std::string& storage, const std::string& os,
bool hasWifi = true, bool hasBluetooth = true,
double weightKg = 2.5);
};
// 调用方写出来的代码长这样:
// Computer pc("Intel i9-14900K", "NVIDIA RTX 4090", 64,
// "2TB NVMe SSD", "Windows 11 Pro", true, true, 2.5);
这段代码的问题,随着需求增长会越来越严重:
true 是什么?第 7 个呢?没人能一眼看懂。写的人过两周自己都忘了。std::string,编译通过,运行时整台"电脑"的配置全是错的。Builder 的核心思想一句话:把"组装"从"产品"中拆出去,让"流程"与"部件"各自独立演化。
拆完之后,各角色各管一件事:
生活类比:电脑城装机。销售员拿着装机单按固定流程走:选 CPU → 选显卡 → 选内存 → 选硬盘 → 装系统(这是 Director 的 construct())。但"具体选哪个型号"由顾客决定,由装机师傅执行(这是 ConcreteBuilder)。同样的流程,可以装出游戏主机、办公主机、家庭影音主机——流程一模一样,只是每个步骤选择了不同的配件。顾客完全不需要知道螺丝怎么拧、线怎么走(构建细节被封装在师傅手里)。
回到软件工程:变化的永远是"部件"(表示),稳定的是"流程"(构建过程)。设计模式永恒的主题就是——把变化的和稳定的分开,让稳定的驱动流程,让变化的易于替换。Builder 是这一思想的教科书级体现:Director 只依赖 Builder 抽象接口(依赖倒置 DIP),新增一种产品 = 新增一个 ConcreteBuilder,Director、Client、Product 一个都不用改。
注意:如果对象很简单,Builder 是小题大做;它的用武之地是"构造过程复杂到需要被单独管理"的场景。判断标准在⑩节详述。
结构图(文字版):
┌──────────────────┐
┌──────────►│ Director │
│ │ construct() │ <- 只编排步骤顺序
│ └────────┬─────────┘
│ 使用 │ 调用(依赖抽象接口)
│ ┌────────▼─────────┐
┌────┴─────┐ │ Builder │ <- 抽象建造者接口
│ Client │ │ buildPartA() │
│ │ │ buildPartB() │
└────┬─────┘ │ getResult() │
│ 创建 └────────┬─────────┘
└──────────► ┌───────▼────────┐
│ ConcreteBuilder│──► 装配并持有 Product
│ buildPartA()… │
└────────────────┘
| 角色 | 职责 | 依赖关系 |
|---|---|---|
| Client(客户) | 选择具体的 ConcreteBuilder,创建 Director,触发 construct(),最后取走产品 | 依赖 Builder 抽象接口和 Director |
| Director(导演) | 编排构建步骤的调用顺序(construct()),封装"流程" | 只依赖 Builder 抽象接口(DIP 体现) |
| Builder(抽象建造者) | 声明每个部件的构建方法 + getResult() | 纯抽象层,不知道产品内部细节 |
| ConcreteBuilder(具体建造者) | 实现各部件的构建逻辑,持有并逐步装配 Product | 实现 Builder,创建 Product |
| Product(产品) | 最终产物,通常只有数据成员和查询接口 | 被动对象,被 ConcreteBuilder 装配 |
谁依赖谁?Director 依赖 Builder 接口(而不是具体类)——这是依赖倒置;Client 创建 ConcreteBuilder 但只以 Builder 接口的身份交给 Director——这是面向接口编程。谁创建谁?Client 创建 Builder,Client 创建 Director,ConcreteBuilder 创建 Product。哪部分可以扩展?ConcreteBuilder 是唯一的扩展点:加一种产品配置,就加一个 ConcreteBuilder,其余角色纹丝不动(OCP)。
延续"电脑配置"场景,用 Builder 重构:同样造游戏主机和办公主机,但这次没有 8 个参数的构造函数,只有清晰的分步构建。完整代码,可直接编译运行(g++ -std=c++17 builder_demo.cpp -o builder_demo):
#include <iostream>
#include <string>
// ========== 产品:计算机配置(只负责保存和展示数据) ==========
class Computer {
public:
void setCpu(const std::string& cpu) { cpu_ = cpu; }
void setGpu(const std::string& gpu) { gpu_ = gpu; }
void setRamGb(int gb) { ramGb_ = gb; }
void setStorage(const std::string& s) { storage_ = s; }
void setOs(const std::string& os) { os_ = os; }
void describe() const {
std::cout << " CPU : " << cpu_
<< "\n GPU : " << gpu_
<< "\n RAM : " << ramGb_ << " GB"
<< "\n Storage: " << storage_
<< "\n OS : " << os_ << "\n";
}
private:
std::string cpu_, gpu_, storage_, os_;
int ramGb_ = 0;
};
// ========== 抽象建造者:定义"每个部件怎么造"的接口 ==========
class ComputerBuilder {
public:
virtual ~ComputerBuilder() = default;
virtual void buildCpu() = 0;
virtual void buildGpu() = 0;
virtual void buildRam() = 0;
virtual void buildStorage() = 0;
virtual void buildOs() = 0;
virtual Computer getResult() = 0; // 返回装配完成的产品
};
// ========== 具体建造者 A:游戏主机 ==========
class GamingComputerBuilder : public ComputerBuilder {
public:
void buildCpu() override { computer_.setCpu("Intel i9-14900K"); }
void buildGpu() override { computer_.setGpu("NVIDIA RTX 4090"); }
void buildRam() override { computer_.setRamGb(64); }
void buildStorage() override { computer_.setStorage("2TB NVMe SSD"); }
void buildOs() override { computer_.setOs("Windows 11 Pro"); }
Computer getResult() override { return computer_; } // 值拷贝返回(对象很轻量)
private:
Computer computer_; // 建造者持有半成品,逐步装配
};
// ========== 具体建造者 B:办公主机 ==========
class OfficeComputerBuilder : public ComputerBuilder {
public:
void buildCpu() override { computer_.setCpu("Intel i5-13400"); }
void buildGpu() override { computer_.setGpu("集成显卡(UHD 730)"); }
void buildRam() override { computer_.setRamGb(16); }
void buildStorage() override { computer_.setStorage("512GB SSD"); }
void buildOs() override { computer_.setOs("Ubuntu 24.04 LTS"); }
Computer getResult() override { return computer_; }
private:
Computer computer_;
};
// ========== 导演:只编排"组装顺序",不关心具体配件 ==========
class ComputerDirector {
public:
// 依赖抽象接口 ComputerBuilder(依赖倒置),不依赖具体建造者
explicit ComputerDirector(ComputerBuilder* builder) : builder_(builder) {}
void construct() {
builder_->buildCpu(); // 顺序是固定的:CPU → 显卡 → 内存 → 硬盘 → 系统
builder_->buildGpu();
builder_->buildRam();
builder_->buildStorage();
builder_->buildOs();
}
private:
ComputerBuilder* builder_; // 非拥有指针:Builder 生命周期由 Client 管理
};
// ========== 客户端 ==========
int main() {
// 用同一个流程(Director),配不同的建造者,得到不同的产品
GamingComputerBuilder gamingBuilder;
ComputerDirector director1(&gamingBuilder);
director1.construct();
Computer gamingPc = gamingBuilder.getResult();
std::cout << "=== 游戏主机配置 ===\n";
gamingPc.describe();
OfficeComputerBuilder officeBuilder;
ComputerDirector director2(&officeBuilder);
director2.construct();
Computer officePc = officeBuilder.getResult();
std::cout << "\n=== 办公主机配置 ===\n";
officePc.describe();
return 0;
}
逐段解释:
Computer 是纯粹的"数据类":只有 setter 和 describe(),不含任何构建逻辑——SRP 得到满足。ComputerBuilder 是抽象接口,5 个 buildXxx() 方法对应 5 个"部件生产步骤";getResult() 返回产品。注意用了纯虚函数 + 虚析构(多态删除安全)。Computer computer_ 作为装配中的半成品——这就是"分步构建"的载体。ComputerDirector::construct() 里是固定不变的调用顺序,它只认识 ComputerBuilder* 接口,完全不认识 GamingBuilder / OfficeBuilder——新增建造者时它不需要任何修改(OCP)。程序输出:
=== 游戏主机配置 ===
CPU : Intel i9-14900K
GPU : NVIDIA RTX 4090
RAM : 64 GB
Storage: 2TB NVMe SSD
OS : Windows 11 Pro
=== 办公主机配置 ===
CPU : Intel i5-13400
GPU : 集成显卡(UHD 730)
RAM : 16 GB
Storage: 512GB SSD
OS : Ubuntu 24.04 LTS
现代 C++ 提示:本例对象轻量,getResult() 用值拷贝即可;若产品昂贵(含大容器、文件句柄),可改为 std::unique_ptr<Computer> 或 std::move 转移所有权,避免拷贝开销。
在 Qt 项目中,最典型的 Builder 场景是构造 SQL 查询:一条 SELECT 语句由"选哪些列、从哪张表、什么条件、怎么排序、限制多少行"多个可选步骤组成,参数多且顺序自由——正是 Builder 的主场。下面实现一个链式风格的 QueryBuilder,并用 Qt 自带的 SQLite 驱动(无需额外安装任何东西)真实执行。
QueryBuilder.h
#ifndef QUERYBUILDER_H
#define QUERYBUILDER_H
#include <QString>
#include <QStringList>
// 建造者:分步构建 SQL SELECT 语句
// 每个步骤方法返回 *this(引用),支持链式调用(fluent interface)
class QueryBuilder {
public:
QueryBuilder& select(const QStringList& columns); // 选择列
QueryBuilder& from(const QString& table); // 数据表
QueryBuilder& where(const QString& condition); // 过滤条件(可多次调用,AND 连接)
QueryBuilder& orderBy(const QString& column, bool ascending = true);
QueryBuilder& limit(int n); // 行数限制
QString build() const; // 产出最终 SQL(产品)
private:
QStringList columns_{"*"}; // 默认查所有列
QString table_;
QStringList conditions_;
QString orderColumn_;
bool ascending_ = true;
int limit_ = -1; // -1 表示不限制
};
#endif // QUERYBUILDER_H
QueryBuilder.cpp
#include "QueryBuilder.h"
QueryBuilder& QueryBuilder::select(const QStringList& columns) {
columns_ = columns;
return *this; // 返回引用以支持链式调用
}
QueryBuilder& QueryBuilder::from(const QString& table) {
table_ = table;
return *this;
}
QueryBuilder& QueryBuilder::where(const QString& condition) {
conditions_.append(condition);
return *this;
}
QueryBuilder& QueryBuilder::orderBy(const QString& column, bool ascending) {
orderColumn_ = column;
ascending_ = ascending;
return *this;
}
QueryBuilder& QueryBuilder::limit(int n) {
limit_ = n;
return *this;
}
QString QueryBuilder::build() const {
QString sql = "SELECT " + columns_.join(", ") + " FROM " + table_;
if (!conditions_.isEmpty())
sql += " WHERE " + conditions_.join(" AND ");
if (!orderColumn_.isEmpty())
sql += QString(" ORDER BY %1 %2").arg(orderColumn_, ascending_ ? "ASC" : "DESC");
if (limit_ > 0)
sql += QString(" LIMIT %1").arg(limit_);
sql += ";";
return sql;
}
main.cpp
#include <QCoreApplication>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#include "QueryBuilder.h"
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
// 使用 Qt 自带 SQLite 驱动,打开内存数据库(无需任何安装)
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
if (!db.open()) {
qCritical() << "无法打开数据库:" << db.lastError().text();
return 1;
}
// 建表并插入测试数据(重复使用同一个 QSqlQuery 对象)
QSqlQuery query(db);
query.exec("CREATE TABLE employee (id INTEGER PRIMARY KEY,"
" name TEXT, department TEXT, salary INTEGER)");
query.exec("INSERT INTO employee VALUES (1, '张三', '研发部', 28000)");
query.exec("INSERT INTO employee VALUES (2, '李四', '研发部', 24000)");
query.exec("INSERT INTO employee VALUES (3, '王五', '市场部', 18000)");
query.exec("INSERT INTO employee VALUES (4, '赵六', '财务部', 21000)");
// 用建造者分步构造查询:链式调用,每一步都清晰可读
QueryBuilder builder;
const QString sql = builder.select({"name", "department", "salary"})
.from("employee")
.where("department = '研发部'")
.orderBy("salary", /*ascending=*/false)
.limit(2)
.build();
qDebug() << "生成的 SQL:" << sql;
qDebug() << "执行结果:";
if (query.exec(sql)) {
while (query.next()) {
qDebug() << " " << query.value(0).toString()
<< query.value(1).toString()
<< query.value(2).toInt();
}
} else {
qCritical() << "SQL 执行失败:" << query.lastError().text();
}
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(QueryBuilderDemo VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Sql) # Qt5 改为: find_package(Qt5 REQUIRED COMPONENTS Core Sql)
add_executable(QueryBuilderDemo
main.cpp
QueryBuilder.h
QueryBuilder.cpp
)
target_link_libraries(QueryBuilderDemo PRIVATE Qt6::Core Qt6::Sql) # Qt5 改为 Qt5::Core Qt5::Sql
程序输出:
生成的 SQL: "SELECT name, department, salary FROM employee WHERE department = '研发部' ORDER BY salary DESC LIMIT 2;"
执行结果:
张三 研发部 28000
李四 研发部 24000
Qt 自身如何使用 Builder 思想:
addOption() / addPositionalArgument() 分步"搭建"解析器,最后 process() 完成构建,再用 value() 取产品。步骤与取值分离,正是 Builder 的流程/表示分离。QFont、QTextCharFormat、QNetworkRequest 等大量类的 setter 返回对象引用,支持 font.setFamily("...").setPointSize(12) 式链式调用——和 QueryBuilder 的 return *this 是同一个技巧。insertText() / insertBlock() / insertImage() 分步拼接出复杂的 QTextDocument,构建过程与最终文档分离。QStringBuilder(% 运算符),但它是"表达式模板优化字符串拼接"的性能工具,不是 GoF 建造者模式,别混淆。Qt 5 / Qt 6 差异:本示例仅用 Core + Sql 模块,两个版本代码完全一致,只有 CMake 里 Qt6:: 与 Qt5:: 前缀的区别(已在注释中标出)。
以 Qt 示例为例,按步骤走一遍完整流程:
① 准备阶段:main() 打开内存 SQLite 数据库,建 employee 表并插入 4 条测试数据。此时 QueryBuilder 对象刚构造,处于"空白状态"(columns 默认 *,其余为空)。
② 分步构建:链式调用 select() → from() → where() → orderBy() → limit()。每个方法只做两件事:把参数写进自己的成员变量、return *this 把对象交回给下一次调用。调用顺序自由——先 where 再 select 也完全合法,这正是"分步"相对于"构造参数"的灵活性。
③ 最终产出:调用 build(),它读取所有成员变量,按固定模板拼出 SQL 字符串:"SELECT 列 FROM 表 WHERE 条件 ORDER BY 列 DESC LIMIT 2;"。build() 是"终结操作"——在此之前对象只是半成品,之后得到完整产品。
④ 执行与展示:query.exec(sql) 执行生成的 SQL,query.next() 逐行读取,qDebug 打印员工姓名、部门、工资。用户(调用方)从头到尾没有直接拼过一个字符串,只描述"想要什么",不关心"怎么拼"。
对照纯 C++ 示例:main() 创建 ConcreteBuilder → 创建 Director(传入 Builder 接口)→ director.construct() 按固定顺序调用 5 个 buildXxx() → getResult() 取回装配好的 Computer。区别在于:纯 C++ 示例有 Director 强制"固定顺序";链式 QueryBuilder 是简化变体(无 Director),用"自由分步 + 终结 build()"换取灵活性——两种风格都是 Builder 家族的合法形态。
从架构角度拆解这个设计,每个决策都有明确目的:
假设不引入 Builder,需求继续增长,代码会沿着下面几条路恶化:
if (type == "gaming") 构造一堆参数; else if (type == "office") ...。每加一种配置,工厂函数就多一个分支,函数越来越长,最终变成没人敢动的"面条代码"。更糟的是,所有配置类型都耦合在一个函数里,测试要覆盖全部分支。这些症状的共同根源:创建对象的复杂度没有被封装。Builder 的价值不在于"写出来好看",而在于把这份复杂度收拢到一个地方、变成可替换的单元。
适合使用(3~5 个场景):
不适合使用(2~4 个场景):
struct { ... } + 列表初始化已经够好。1. Builder vs 抽象工厂(Abstract Factory,Day 3)——最容易混淆的一对:
| 维度 | Builder | Abstract Factory |
|---|---|---|
| 构建方式 | 分步构建,一步步装配一个对象 | 一步返回一个完整产品对象 |
| 关注点 | 复杂对象的构造过程(怎么组装) | 一族相关产品的接口一致性(造什么) |
| 产品粒度 | 一个复杂的聚合对象 | 一族相互关联的对象(如按钮+输入框+滚动条) |
| 控制权 | Director 控制步骤顺序,可复用于多种产品 | 工厂方法内部一次性完成,调用方不参与步骤 |
| 典型对比 | 装机:同一流程,不同配件 | 品牌店:同一品牌(小米/华为)下的全家桶产品 |
一句话记忆:抽象工厂"一步到位给一整套",Builder"一步一步攒一台"。在 Qt 里:QStyleFactory 的 create() 是一步返回风格的抽象工厂;而 QCommandLineParser 的分步 addOption + process 是 Builder。
2. Builder vs 工厂方法(Factory Method,Day 2):工厂方法通过子类决定"创建哪个具体产品"(产品种类不同),Builder 通过不同 Builder 决定"同一产品怎么组装"(产品形态不同)。工厂方法通常是"一行代码返回产品",Builder 是"一串调用攒产品"。
3. Builder vs 原型(Prototype,明日预告):原型是克隆一个现成对象(copy 已有实例再微调);Builder 是从零分步构建。原型适合"初始模板已存在、主要工作是复制+改小部分",Builder 适合"没有模板、必须逐步拼装"。二者常配合使用:用 Prototype 造初始对象,用 Builder 调整细节。
Builder 在 Qt 框架里没有单一"标准实现",但它分步构建 + 链式 API的风格渗透在大量类中,读源码时处处可见:
addOption()(分步添加部件)→ process()(终结构建)→ value()(取出产品)。Qt 把"命令行解析配置"这个复杂对象,拆成了"步骤"与"结果"两个阶段,使用者按需组合选项,互不干扰。insertText() / insertBlock() / setCharFormat() 分步拼接——每步改变文档状态,最终得到完整文档。这就是"分步构建复杂对象"的活教材。QFont::setFamily().setPointSize()、QTextCharFormat、QNetworkRequest、QSqlQuery::setForwardOnly() 等大量 API 返回 QFont& / QTextCharFormat& 之类的引用以支持链式调用——底层技巧与我们 QueryBuilder 的 return *this 完全一致。这是 Qt 对 Builder"简化形态"的偏爱:不引入 Director,靠链式调用让调用方自由编排。QStringLiteral("a") % b % c 用表达式模板在编译期规划拼接,减少临时对象分配。它解决的是"性能"而非"构建流程",只是名字带 Builder,理解 Qt 时值得知道它的真实定位。为什么 Qt 这样设计?因为 Qt 的公共 API 必须"对新手友好、对老手灵活"。链式 Builder 形态让调用方只写自己关心的步骤,其余用默认值——既避免了构造函数参数爆炸,又保持了调用代码的声明式可读性。这也是你在 Qt 项目里写自定义 Builder 时应该追求的目标:默认值要贴心,链式要顺滑,build() 要兜底校验。
A:核心区别在"构建方式"与"关注点"。Abstract Factory 关注产品族:一步返回一组相关产品,调用方拿到的是"整套东西";Builder 关注构建过程:分步装配一个复杂对象,调用方(或 Director)控制每一步。打个比方:抽象工厂是"去品牌专卖店一次性买齐全家桶",Builder 是"按装机单一步步攒一台电脑"。实现上,Abstract Factory 通常返回产品接口指针,Builder 通过 getResult() 在最后返回产品。
A:把"参数"变成"方法调用"。每个可选参数对应一个 buildXxx()/setXxx() 方法,调用方只调用关心的步骤,其余用默认值;参数语义从"第 6 个 true 是什么"变成 .withWifi(true) 这样自解释的链式调用。同时校验逻辑从构造函数挪进各步骤方法,错误定位更精准,新增特性只加方法不加构造重载,符合 OCP。
A:每个步骤方法返回 *this 的引用(QueryBuilder&),调用方就能连续 .a().b().c()。注意三点:① 返回引用而非值,避免拷贝开销和"改的是副本"的 bug;② 方法内部先修改自身状态再返回 *this,顺序别写反;③ 若 Builder 会被复制,要考虑复制语义(一般禁止拷贝或正确实现拷贝构造)。Qt 的 QFont 等类正是这么做的。
A:不是必须的,取决于"流程是否固定"。当所有产品都遵循相同的构建顺序(如装机必须先 CPU 后内存)时,Director 把顺序固化下来,防止调用方乱序——推荐保留;当步骤自由组合(如 SQL 查询,where 和 orderBy 顺序无所谓)时,可以省略 Director,由调用方直接用链式 API 编排(我们的 QueryBuilder 就是这种形态)。省略 Director 更灵活,但失去了"流程统一"的保障,属于权衡。
A:产品类只暴露 getter,构造函数设为 private,只允许 Builder 访问(friend 或 Builder 嵌套在产品内)。Builder 持有产品的"待定值",build() 时一次性构造并返回不可变产品。这样对象一旦产出就不可变,线程安全且杜绝半初始化状态——在配置类、值对象场景非常实用。
练习(20~30 分钟):HTTP 请求构造器。
为一个网络客户端实现 HttpRequestBuilder,支持链式构建请求:
设计目标:调用方写出的代码形如 HttpRequest req = HttpRequestBuilder().method("POST").url("https://api.example.com/upload").header("Content-Type", "application/json").body("{...}").timeout(10).build();,且能在编译期或运行期拦截非法组合。
提示 1:校验集中在 build() 里做,用 QString 或 std::optional 表示"未设置"状态,别用空字符串硬扛。
提示 2:headers 用 QMap<QString, QString> 或 std::map 存;想想 build() 返回的 HttpRequest 是值语义还是 unique_ptr,为什么?