【设计模式系列 (四) 】建造者模式

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️设计模式系列专栏:设计模式系列

系列上期内容:【设计模式系列 (三) 】单例模式

系列下期内容:暂无


目录

[1. 概述](#1. 概述)

[2. 结构](#2. 结构)

[3. 实现一:带指挥者的经典版(人种)](#3. 实现一:带指挥者的经典版(人种))

[3.1 产品](#3.1 产品)

[3.2 抽象建造者](#3.2 抽象建造者)

[3.3 具体建造者](#3.3 具体建造者)

[3.4 指挥者](#3.4 指挥者)

[3.5 客户端](#3.5 客户端)

[4. 实现二:链式 Builder(英雄)](#4. 实现二:链式 Builder(英雄))

[4.1 产品与建造者](#4.1 产品与建造者)

[4.2 客户端](#4.2 客户端)

[5. 两种实现怎么选](#5. 两种实现怎么选)

[6. 优缺点与适用环境](#6. 优缺点与适用环境)

[7. 面试专题](#7. 面试专题)

[7.1 开场题:说说建造者模式吧](#7.1 开场题:说说建造者模式吧)

[7.2 建造者 vs 工厂(常问)](#7.2 建造者 vs 工厂(常问))

[7.3 高频追问清单](#7.3 高频追问清单)

[7.4 什么时候不该用建造者](#7.4 什么时候不该用建造者)

结语


建造者模式(Builder Pattern)要解决的是一个非常具体的坏味道:构造器参数失控。

本文代码取自design-pattern-cpp 仓库的 design-pattern/builder/,使用 C++11。为阅读方便,片段省略了头文件保护宏。


1. 概述

Wikipedia :The builder pattern is an object creation software design pattern with the intentions of finding a solution to the telescoping constructor anti-pattern.

建造者模式是一种对象创建软件设计模式,旨在找到伸缩构造器反模式的解决方案。

伸缩性构造的反模式:指通过构造器实现对象构建参数初始化,如果对象属性比较多,导致构造器的参数个数不可控

GoF:Separate the construction of a complex object from its representation so that the same construction process can create different representations.

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

两个关键词:

① 伸缩构造器反模式(telescoping constructor anti-pattern)

一个对象属性一多,构造函数就变成这样:

cpp 复制代码
Hero(std::string name, HairType* hairType, HairColor* hairColor,
     Armor armor, Profession* profession, Weapon* weapon);

想只传前三个?只能再补一堆重载:

cpp 复制代码
Hero(std::string name);
Hero(std::string name, HairType* hairType);
Hero(std::string name, HairType* hairType, HairColor* hairColor);
// ... 组合爆炸

调用处则是 new Hero("张飞", nullptr, color, Armor(), nullptr, nullptr) 这种靠数位置才能看懂的代码。参数一多,谁是谁全靠猜。

② 同构不同果

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。


2. 结构

建造者模式把"造对象"拆成四步分工,共四个角色:

角色 职责
Builder(抽象建造者) 声明两类方法:一类是 buildPartXxx(),负责逐个构造部件;另一类是 build(),负责返回成品。既可以是抽象类,也可以是接口
ConcreteBuilder(具体建造者) 实现各部件具体的构造与装配方法,明确自己要造的是哪个产品
Product(产品) 被构建的复杂对象,包含多个部件
Director(指挥者/导演) 负责安排部件的建造次序 ,与抽象建造者关联,在 construct() 中调用建造者的装配方法。客户端通常只与指挥者交互

3. 实现一:带指挥者的经典版(人种)

3.1 产品

cpp 复制代码
class Human
{
    // 人种类型名称
    CC_SYNTHESIZE(std::string, type, Type);
    // 人种分布区域
    CC_SYNTHESIZE(std::string, region, Region);
};

CC_SYNTHESIZE 是仓库的宏,展开后是"受保护的成员 + 公开的 getter/setter",后面几个类都在用它,等价于手写 getType()/setType()。

3.2 抽象建造者

cpp 复制代码
class HumanBuilder
{
    // 关联一个人种实例
    CC_SYNTHESIZE(Human, m_human, Human);
public:
    virtual HumanBuilder* buildType() = 0;
    virtual HumanBuilder* buildRegion() = 0;
    // 将成员变量实例创建出一个新的实例对象
    virtual Human* build() final;
};
​
// cpp
builder::Human* builder::HumanBuilder::build()
{
    // 通过拷贝创建新的实例
    return new Human(m_human);
}

三个细节值得注意:

  1. buildType() / buildRegion() 返回 HumanBuilder* 而不是 void ------ 每个建造步骤都 return this,于是可以链式调用。装配顺序由调用方(Director)决定。

  2. build() 声明为 final ------ 产品怎么生成是基类定死的,子类只负责"填什么部件",不能改"产品怎么产出"。

  3. build() 是拷贝出一份新产品 ------ 建造者内部的 m_human 是"草稿",每次 build() 都产出一个独立的成品,建造者本身可以复用。

3.3 具体建造者

cpp 复制代码
class YellowManBuilder : public HumanBuilder
{
    DECLARE_CLASS(builder::YellowManBuilder);
public:
    HumanBuilder* buildType() override;
    HumanBuilder* buildRegion() override;
};
​
// cpp
IMPLEMENT_CLASS(builder::YellowManBuilder);
​
builder::HumanBuilder* builder::YellowManBuilder::buildType()
{
    m_human.setType("黄种人");
    return this;                            // ← 链式的关键
}
​
builder::HumanBuilder* builder::YellowManBuilder::buildRegion()
{
    m_human.setRegion("华夏");
    return this;
}

DECLARE_CLASS / IMPLEMENT_CLASS 是仓库的自注册反射宏,让这个类可以在运行时按类名字符串被创建。这里用它是为了配合下面的配置化客户端。

另外两个具体建造者只有字面量不同:WhiteManBuilder 造"白种人 / 金星",BlackManBuilder 造"黑种人 / 火星"。

3.4 指挥者

cpp 复制代码
class HumanDirector
{
    CC_SYNTHESIZE_SET(HumanBuilder*, m_builder, Builder);
public:
    Human* construct() {
        return m_builder->buildType()->buildRegion()->build();
    }
};

装配顺序(先 type 后 region)被固化在这里。 换一个具体建造者,同一套顺序就能造出不同的产品------这正是"同样的构建过程创建不同的表示"。

3.5 客户端

cpp 复制代码
int main()
{
    CREATE_PROPERTIES(prop, conf);                       // 读取配置
    string bcname = prop.getProperty("bp");              // bp=builder::YellowManBuilder
    GET_INSTANCE_BY_NAME(HumanBuilder*, builder, bcname); // 反射创建建造者
    HumanDirector director;
    director.setBuilder(builder);
    Human* man = director.construct();
    std::cout << man->getType() << ":" << man->getRegion() << std::endl;
    delete builder;
    delete man;
    return 0;
}

客户端里没有出现任何一个具体建造者的类名 ,全部来自 config/conf.properties:

cpp 复制代码
bp=builder::YellowManBuilder

改一行配置就能换成白种人或黑种人,代码零改动。


4. 实现二:链式 Builder(英雄)

经典版有 Director 显得重。如果只是想让"构造成员变量"这件事可读,可以省掉 Director,用链式 Builder------也就是现代语言里最常见的写法。

4.1 产品与建造者

cpp 复制代码
class Hero
{
    CC_SYNTHESIZE_GET(std::string, name, Name);
    CC_SYNTHESIZE_GET(HairType*, type, Type);
    CC_SYNTHESIZE_GET(HairColor*, color, Color);
public:
    class Builder {
        friend class Hero;                  // 只有 Hero 能访问 Builder 的私有成员
    private:
        std::string name;
        HairType* type;
        HairColor* color;
    public:
        Builder();
        Hero* build();
        Builder* withName(std::string name);
        Builder* withHairType(HairType* type);
        Builder* withHairColor(HairColor* color);
    };
private:
    Hero(Builder* builder);                 // ← 构造私有,外部只能通过 Builder 造
public:
    ~Hero();
    void show();
    static Hero::Builder build();           // ← 链式入口
};
builder::Hero::Builder* builder::Hero::Builder::withName(std::string name)
{
    this->name = name;
    return this;
}
​
builder::Hero* builder::Hero::Builder::build()
{
    return new Hero(this);                  // 一次性把攒好的状态交给产品
}
​
builder::Hero::Hero(Builder* builder)
{
    this->name  = builder->name;
    this->type  = builder->type;
    this->color = builder->color;
}
​
builder::Hero::~Hero()
{
    if (type)  delete type;                 // 所有权随指针转移给了产品
    if (color) delete color;
}
​
builder::Hero::Builder builder::Hero::build()
{
    return Hero::Builder();
}

三个设计要点:

  1. Builder 是 Hero 的静态内部类,Hero 的构造函数私有 ,两者用 friend 打通。结果是除了 Builder,没人能造出 Hero------产品构造的合法性被类型系统兜住。

  2. 状态先攒在 Builder 身上,build() 时一次交付。中间任何时刻对象都不是"半成品",不存在构造到一半被别人看到的可能。

  3. withXxx() 返回 Builder*(this) ,天然支持链式;name 是值、type/color 是指针,指针的所有权在 build() 时转移给 Hero ,由 ~Hero() 负责释放。

4.2 客户端

cpp 复制代码
int main()
{
    Hero* h1 = Hero::build()
        .withName("张飞")
        ->withHairType(new HairType("双马尾"))
        ->withHairColor(new HairColor("金黄色"))
        ->build();
    h1->show();
​
    Hero::Builder b;
    Hero* h2 = b.withName("赵云")->build();
    h2->show();
​
    delete h1;
    delete h2;
    return 0;
}

调用处的可读性就是建造者模式最大的收益:方法名即字段名,参数位置不再需要记忆。

⚠️ 一个小陷阱:Hero::build() 返回的是 Builder 临时对象 ,withName() 返回指向它的指针。整条链写在一个表达式 里是安全的(临时对象活到表达式结束),但一旦拆成多行赋值就会悬空。要稳,就把 Hero::Builder b; 显式声明成变量再链(上面 h2 的写法)。


5. 两种实现怎么选

带 Director(人种) 链式 Builder(英雄)
装配顺序由谁定 Director 固化,客户端不关心 客户端按需自由组合
产品构造权限 建造者内部持有草稿,build() 拷贝产出 产品构造私有,只有 Builder 能造
是否必须按固定流程 是(同一构建过程造不同表示) 否(想给几个字段就给几个字段)
典型场景 参数化的流水线生产,产品族差异大 参数很多的单个对象,字段可选
主要收益 隔离构建过程,扩展新产品不改客户端 消灭伸缩构造器,调用处可读

一句话:要"同一套流程造不同的东西"就用 Director 版;只是"参数太多写不下了"就用链式版。


6. 优缺点与适用环境

主要优点

  • 客户端不必知道产品内部组成细节,产品本身与创建过程解耦,同样的创建过程能造出不同产品;

  • 各具体建造者彼此独立 ,替换或新增建造者很方便。指挥者面向抽象建造者编程,符合开闭原则;

  • 可以把复杂产品的创建步骤分解到不同方法中,更精细地控制创建过程。

主要缺点

  • 产品需要有较多共同点 、组成部件相似。如果产品之间差异极大,不适合使用,适用范围受限;

  • 如果产品内部变化复杂,会导致需要定义大量具体建造者类,系统变得庞大,增加理解与运行成本。

适用环境

  • 需要生成的产品对象有复杂的内部结构,通常包含多个成员属性;

  • 需要生成的产品对象的属性之间相互依赖,需要指定生成顺序;

  • 对象的创建过程独立于创建该对象的类------引入指挥者后,创建过程被封装在指挥者中,而不在建造者或客户端里;

  • 需要隔离复杂对象的创建和使用,并使得同样的创建过程可以创建不同的产品。


7. 面试专题

7.1 开场题:说说建造者模式吧

考察点是概念、角色、应用场景:

概念 :建造者模式将一个复杂对象的构建 与它的表示分离,使得同样的构建过程可以创建不同的表示。它主要用于解决构造器参数失控的"伸缩构造器反模式"。

角色 :抽象建造者 Builder 声明部件的构造方法与 build();具体建造者 ConcreteBuilder 实现各部件的具体装配;产品 Product 是被构建的复杂对象;指挥者 Director 负责安排建造次序。

使用场景:需要生成的对象内部结构复杂、包含多个成员属性时;对象的属性之间相互依赖、有生成顺序要求时。

可扩展:把与工厂模式的区别、优缺点、项目中的用法接上(见下)。

7.2 建造者 vs 工厂(常问)

建造者模式 工厂模式
关注点 装配顺序------一步步把部件组装起来 创建哪个对象------把实例化过程封装起来
返回时机 所有部件都装完才返回成品 一次调用直接返回对象
产品复杂度 复杂对象,多部件、多属性 相对简单,通常是单个产品(或一个产品族)
客户端 可以参与指定装配过程(链式版) 只给参数或选工厂,不关心过程
典型问法 "怎么造" "造哪个"

一句话版本:建造者关注装配顺序,工厂模式关注如何使用对象。

7.3 高频追问清单

追问 答法
buildXxx() 为什么返回 this? 实现链式调用,也让 Director 能把多个装配步骤串成一个表达式
build() 为什么声明成 final? 产品如何产出是基类的契约,子类只该决定"填什么部件",不该改"怎么产出成品"
为什么要把产品构造函数私有化? 强制所有创建都走 Builder,杜绝参数不全或顺序错误的半成品对象
Builder 和产品之间为什么要 friend? 产品构造私有后,只有 Builder 能访问它的私有构造;用友元打通,比开放 public 构造更安全
Director 到底有什么用? 把装配顺序这层知识从客户端移走。换建造者不改顺序,改顺序不改建造者,两边都符合开闭原则
建造者能不能省掉 Director? 能。链式 Builder 就是省掉 Director 的形态,代价是装配顺序交回客户端
指针成员的所有权怎么管理? 仓库里是"Builder 只暂存指针,build() 时把所有权交给产品,由产品的析构函数统一 delete"。更稳的做法是换成 std::unique_ptr
建造者模式有什么缺点? 产品差异大时不适用;具体建造者类数量容易膨胀;比直接 new 多了不少类,有过度设计的风险
和"可选参数默认值"有什么区别? 默认值解决参数省略,解决不了参数顺序 和构建过程两个问题;建造者两者都能解决

7.4 什么时候不该用建造者

判断标准还是那一条:这个对象真的有那么多参数吗?

  • 成员只有两三个 → 老老实实写构造函数,建造者是纯粹的成本;

  • 属性之间没有先后依赖、也不需要校验 → 用聚合初始化或默认参数就够了;

  • 产品之间差异极大、几乎没有共同部件 → 建造者模式会让具体建造者类爆炸,得不偿失。

建造者的价值在于隔离"复杂对象的装配过程",对象不复杂的时候,隔离它就是过度设计。


结语

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!

相关推荐
羑悻的小杀马特1 小时前
Docker高阶实战:从Redis集群到C++微服务,全面解析镜像优化与生产环境部署+镜像制作常见问题详解
c++·redis·docker·镜像制作·dockefile
别动我齐刘海1 小时前
ROS2 Jazzy + C++ 时间系统——Time / Clock / Duration
linux·c++·人工智能·学习·机器学习·机器人·自动驾驶
重生之小比特2 小时前
【C++进阶】unordered_set 和unordered_map 深度解析
c++·哈希算法
IT_陈寒2 小时前
用Vue时这个响应式陷阱坑了我一整天
前端·人工智能·后端
孙启超2 小时前
【AI开发之Rust】第 20 课:壳侧 API 封装 —— 把 Rust 能力接进真实 UI
开发语言·后端·rust
BingoGo2 小时前
用 Jev 与 Laravel AI SDK 检测垃圾邮件和自动回复
后端·laravel
JaguarJack2 小时前
用 Jev 与 Laravel AI SDK 检测垃圾邮件和自动回复
后端·ai·php·laravel·服务端
mldong2 小时前
同一套充血模型,八种语言八种长相
后端·架构
chenbingjie_c9 小时前
C++ 进阶核心知识点总结:模板、内存分区、new/delete、内存池
开发语言·c++