
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页 :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);
}
三个细节值得注意:
-
buildType()/buildRegion()返回HumanBuilder*而不是void------ 每个建造步骤都return this,于是可以链式调用。装配顺序由调用方(Director)决定。 -
build()声明为final------ 产品怎么生成是基类定死的,子类只负责"填什么部件",不能改"产品怎么产出"。 -
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();
}
三个设计要点:
-
Builder是Hero的静态内部类,Hero的构造函数私有 ,两者用friend打通。结果是除了 Builder,没人能造出 Hero------产品构造的合法性被类型系统兜住。 -
状态先攒在 Builder 身上,
build()时一次交付。中间任何时刻对象都不是"半成品",不存在构造到一半被别人看到的可能。 -
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,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!