创建型模式 · 第 4 天 / 23

Day 4:建造者模式(Builder Pattern)

把一个复杂对象的构建过程与它的内部表示分离,让同样的构建流程能造出不同的产品。—— 创建型模式第四站,前三天依次学完单例、工厂方法、抽象工厂,今天解决的是"对象太复杂、构造函数参数爆炸"的问题。

① 今日主题

建造者模式(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);

这段代码的问题,随着需求增长会越来越严重:

核心矛盾:创建对象时,"步骤流程"是稳定的(先 CPU 后显卡再内存),但"每一步的具体内容"是变化的(游戏机要 i9+4090,办公机要 i5+集显)。把稳定的流程和变化的内容混在一起写进构造函数,就是上面这堆问题的根源。Builder 模式就是要把这两者拆开。

③ 核心思想

Builder 的核心思想一句话:把"组装"从"产品"中拆出去,让"流程"与"部件"各自独立演化。

拆完之后,各角色各管一件事:

生活类比:电脑城装机。销售员拿着装机单按固定流程走:选 CPU → 选显卡 → 选内存 → 选硬盘 → 装系统(这是 Director 的 construct())。但"具体选哪个型号"由顾客决定,由装机师傅执行(这是 ConcreteBuilder)。同样的流程,可以装出游戏主机、办公主机、家庭影音主机——流程一模一样,只是每个步骤选择了不同的配件。顾客完全不需要知道螺丝怎么拧、线怎么走(构建细节被封装在师傅手里)。

回到软件工程:变化的永远是"部件"(表示),稳定的是"流程"(构建过程)。设计模式永恒的主题就是——把变化的和稳定的分开,让稳定的驱动流程,让变化的易于替换。Builder 是这一思想的教科书级体现:Director 只依赖 Builder 抽象接口(依赖倒置 DIP),新增一种产品 = 新增一个 ConcreteBuilder,Director、Client、Product 一个都不用改。

注意:如果对象很简单,Builder 是小题大做;它的用武之地是"构造过程复杂到需要被单独管理"的场景。判断标准在⑩节详述。

④ UML / 角色关系

结构图(文字版):

                    ┌──────────────────┐
        ┌──────────►│     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)。

⑤ 最小 C++ 示例(C++17)

延续"电脑配置"场景,用 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;
}

逐段解释:

程序输出:

=== 游戏主机配置 ===
  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 实战示例(Qt6 + C++17)

在 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 思想:

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 的本质是"把创建对象时那些琐碎、易错、易变的步骤,从构造函数里搬出来,装进一个可替换的类里"。对象本身保持简单,复杂留给了建造者。

⑨ 不使用会怎样?

假设不引入 Builder,需求继续增长,代码会沿着下面几条路恶化:

这些症状的共同根源:创建对象的复杂度没有被封装。Builder 的价值不在于"写出来好看",而在于把这份复杂度收拢到一个地方、变成可替换的单元。

⑩ 什么时候应该使用?

适合使用(3~5 个场景):

不适合使用(2~4 个场景):

过度设计提醒:Builder 是最容易被滥用的模式之一。见到"3 个参数也套 Builder"的代码,往往是炫技而非必要。判断标准很简单:这个对象的创建过程,是否复杂到值得被单独封装?不值得,就别用。永远记住:模式是为问题服务的,问题不存在的模式是负债。

⑪ 和其他模式的区别

1. Builder vs 抽象工厂(Abstract Factory,Day 3)——最容易混淆的一对:

维度BuilderAbstract 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 调整细节。

⑫ Qt 源码 / Qt 框架中的体现

Builder 在 Qt 框架里没有单一"标准实现",但它分步构建 + 链式 API的风格渗透在大量类中,读源码时处处可见:

为什么 Qt 这样设计?因为 Qt 的公共 API 必须"对新手友好、对老手灵活"。链式 Builder 形态让调用方只写自己关心的步骤,其余用默认值——既避免了构造函数参数爆炸,又保持了调用代码的声明式可读性。这也是你在 Qt 项目里写自定义 Builder 时应该追求的目标:默认值要贴心,链式要顺滑,build() 要兜底校验。

⑬ 面试常见问题

Q1:Builder 和 Abstract Factory 有什么区别?

A:核心区别在"构建方式"与"关注点"。Abstract Factory 关注产品族:一步返回一组相关产品,调用方拿到的是"整套东西";Builder 关注构建过程:分步装配一个复杂对象,调用方(或 Director)控制每一步。打个比方:抽象工厂是"去品牌专卖店一次性买齐全家桶",Builder 是"按装机单一步步攒一台电脑"。实现上,Abstract Factory 通常返回产品接口指针,Builder 通过 getResult() 在最后返回产品。

Q2:Builder 如何解决"构造函数参数爆炸"?

A:把"参数"变成"方法调用"。每个可选参数对应一个 buildXxx()/setXxx() 方法,调用方只调用关心的步骤,其余用默认值;参数语义从"第 6 个 true 是什么"变成 .withWifi(true) 这样自解释的链式调用。同时校验逻辑从构造函数挪进各步骤方法,错误定位更精准,新增特性只加方法不加构造重载,符合 OCP。

Q3:链式调用是怎么实现的?有什么注意事项?

A:每个步骤方法返回 *this 的引用(QueryBuilder&),调用方就能连续 .a().b().c()。注意三点:① 返回引用而非值,避免拷贝开销和"改的是副本"的 bug;② 方法内部先修改自身状态再返回 *this,顺序别写反;③ 若 Builder 会被复制,要考虑复制语义(一般禁止拷贝或正确实现拷贝构造)。Qt 的 QFont 等类正是这么做的。

Q4:Director 是必须的吗?可以省略吗?

A:不是必须的,取决于"流程是否固定"。当所有产品都遵循相同的构建顺序(如装机必须先 CPU 后内存)时,Director 把顺序固化下来,防止调用方乱序——推荐保留;当步骤自由组合(如 SQL 查询,where 和 orderBy 顺序无所谓)时,可以省略 Director,由调用方直接用链式 API 编排(我们的 QueryBuilder 就是这种形态)。省略 Director 更灵活,但失去了"流程统一"的保障,属于权衡。

Q5:如何用 Builder 配合"不可变对象"?

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,为什么?

⑮ 今日总结

一句话记忆:建造者模式 = "把'怎么组装'和'组装什么'分开"——流程交给 Director,内容交给 Builder,产品保持简单。

看到以下代码特征时,考虑使用 Builder:

明日预告:Day 5 原型模式(Prototype Pattern)——创建型模式的收官之作。用"克隆"代替"重新构建",用 clone() 多态复制复杂对象。它与今天的 Builder 恰好互补:Builder 从零分步攒对象,Prototype 复制现成对象再微调。学完它,GoF 五个创建型模式全部通关,下阶段进入结构型模式(Adapter 适配器)。