如果把你写代码这几年剪成一支 vlog。 我敢打赌。 总有一个镜头反复出现。
手指一抖。
new 一下。
再给它挂个指针。 顺手加句注释。
心里默念。 先跑起来。 真出事再说。
别笑。 这套操作能活到今天。 说明它在“项目还小”的世界里,确实好用。
那会儿 C 还当老大。
C++ 在很多人眼里,就是“带点类的 C”。
内存管理也简单粗暴。
malloc/free 换个皮叫 new/delete。
谁要是多想两句生命周期。
还容易被人嫌“太讲究”。
等项目从作业长成线上服务。 味儿就不一样了。
代码里满地 new Xxx(...)。
仓库里站着一排大号全局变量:g_logger、g_config、g_pool。
谁顺手谁加。
谁改挂谁。
那时还没人把“设计模式”三个字挂在嘴边。 但每个老项目身上,都隐隐写着一句话。
创建这块。 有点怪。
1994 年那本《Design Patterns》出来。 画风才突然统一。 四位大佬把这些“凭手感写出来的套路”捞起来。 排好队。 起好名。 整理成 23 个模式。
后来我见过不少团队。 那本书不是买的。 是复印的。 边角卷得像被咖啡泡过。 里面还夹着一张便签。 写着“TODO:等有空重构”。
从那之后。 “单例”“工厂模式”这些词开始在 C++ 圈子里乱飞。 会议室里有人举着书讲。 走廊里有人吐槽。 又来一套 UML 图。
这一篇我不摆 UML。 也不按目录背单词。 我只想拉着你沿着一条时间线走一圈。
从满地 new 的上古现场出发。
路过单例和工厂们的救火现场。
最后看看 Builder、原型是怎么把“随缘造对象”修成生产线的。
看完这一圈。 你再去翻单例、工厂、Builder、原型那些细拆版。 会更像在看“剧情回放”。 而不是在背名词表。
1. 开局一把 new:上古写法长什么样
先别急着谈“创建型模式”。 把时间往回拨一点。 拨到你刚碰 C++ 那几年。 那会儿大家的心态特别简单。
想要一个对象?
顺手 new 一把。
用完记得 delete。
能成对就不错了。
实在懒得管? 就指望进程退出帮你收尸。
书上也大差不差就教你这些。 于是实际项目里的代码。 大多长这样:
void handle() {
Logger* logger = new Logger("/var/log/app.log");
Config* cfg = new Config("config.json");
Service* s = new Service(logger, cfg);
s->run();
}
刚写完的时候。
你很难挑出毛病。
调用链一条线往下走。
哪儿缺什么就地补一个 new。
逻辑清清楚楚。
连 code review 都会点头。
挺直观的。
问题是。 这套写法对“小玩具”很友好。 对“真工程”很无情。
项目一旦熬成“十几万人天”的老系统。
new 就不再是一个朴素的操作符。
它会变成一支支冷箭。
同一个东西。
三五个模块各自 new 一份。
构造参数。 每个人写一版。 能跑就行。
某天线上炸了。
你想追“到底是谁创建了它”。
grep 一下。
满屏都是。
你甚至不知道该先骂哪一段。
更糟的是。 那一代 C++ 特别迷恋全局状态:
Logger g_logger;
Config g_config;
ThreadPool g_pool;
新同事进组,第一次看到这一排 g_ 开头的家伙,十有八九会真心感叹一句:
“哇,好方便,哪儿都能用。”
等你维护这个系统三五年,再看到同一排东西,心情就只剩下一句:
“完了,又得查谁先动手。”
很多老项目翻车。 剧情都差不多。
半夜报警。 QPS 垂直下掉。
值班同学远程连上去。
开着 gdb。
在核心 dump 里一层层抠调用栈。
最后发现罪魁祸首。 不是哪段“惊天大逻辑”。 而是某个看起来理所当然的全局对象。 这次链接顺序换了个姿势。 构造 / 析构时机一歪。 整套逻辑就跟着侧翻。
回头看那一拨代码。 有个共同的气质:
大家都在顺手
new,但“谁负责造、造在哪儿、活多久”这些事,从来没人系统想过。
new 写在哪里。
生命周期就默认跟到哪里。
谁顺手接了指针。 谁就“差不多负责一下”。
真正该写在类型、接口、模块边界里的约束。 全靠默契和注释。 没人当成设计问题来讨论。
所谓“创建型模式”。 就是在这堆历史遗留上。 一点点长出来的一套规矩。
有人先想办法。 给全局状态穿件西装。 这就是单例。
有人把散落满地的 new 收拢起来。
集中到一个地方发号施令。
这就是工厂。
再往后。 Builder、原型这些角色干脆说: 别只关心“造什么”。 “怎么一步步把它拼好”。 也值得单独讲。
你可以把后面要出场的这些模式。
都看成是对那段“上古 new 时代”的一次次复盘。
每一个背后。
都是一群踩过坑的老同事在提醒你:
造对象这件事,别再当成“顺手敲一行
new”的小事。 它完全值得你单独设计一套说得清楚的做法。
2. 单例:给全局变量先套一件西装
第一波“补救措施”。 通常就是单例。
直觉其实很朴素。 日志后端、配置快照这类东西。 概念上本来就只该有一份。 多搞几份。 只会平添混乱。
于是一个很自然的想法冒出来:
干脆做成“全局唯一”。 再顺手给它一个看起来体面的入口。
单例做的事。 本质就三件。
保证只一份。 所有人都能摸到入口。 从进程启动活到进程结束。
相比赤膊上阵的全局变量。 这已经算给它穿上了西装。
C++11 又推了一把。
标准明确说:函数内局部 static 的初始化是线程安全的。
于是经典的 Meyers Singleton 写法。
终于不用再靠运气:
static Logger& instance() {
static Logger inst; // 第一次用时构造,以后复用
return inst;
}
写过几年 C++ 的人。 大多都经历过那种“单例真香期”。
懒得传参数? 塞进单例。
不想画清依赖? 塞进单例。
想在任何地方随手打 log、查配置? 再塞进单例。
几年之后你回头看。 项目里往往是这样的景象。
一堆 GlobalContext::instance()。
到处在读配置、拿连接、做一堆奇怪的事。
谁都能伸手改两下。
全局一起抖三抖。
写的时候像开挂。
重构的时候像背锅。
所以在这条时间线上。 单例更像一个“补丁 1.0”。
它承认。 确实有些东西该全局一份。
它也做到了。 至少把这些东西拎出来。 套上一件西装。
顺手还暴露出一个事实:
只解决“唯一性”,还远远不够。
new 还在到处飞,
创建逻辑依然散落满地,
只是多了几个看起来更体面的入口而已。
3. 简单工厂:先把 new 收拢到一个地方
当你对“满地 new”慢慢产生过敏反应。
工厂家族就该上场了。
打头阵的是简单工厂。 它问的问题特别接地气:
“能不能别谁想
new就哪儿new? 把这件事收拢到一个地方,大家排队领对象?”
于是就长出了这种味道的代码:
std::unique_ptr<Task> createTask(const Request& req) {
if (req.type == "image") return std::make_unique<ImageTask>(...);
if (req.type == "video") return std::make_unique<VideoTask>(...);
// ...
return nullptr;
}
void handle(const Request& req) {
auto task = createTask(req);
if (task) task->run();
}
handle 不再关心具体 new 谁,
它只要一个能 run() 的 Task。
这一步的收获。 其实挺朴素。
你第一次把“创建逻辑”和“业务流程”硬生生拆开。
你第一次有了一个专门管 new 的地方。
但人类有个稳定 bug:一旦有“集中点”,就忍不住往里狂塞东西。
再过几年,你点开 createTask。
画风可能就变成这样。
几十行 if-else。
一堆“线上 / 压测 / 本地”的环境分支。
外加几坨配置解析逻辑。
你以为自己在“集中管理变化点”。
实际上,只是把所有变化点拧成了一团新的大蘑菇。
这时候,简单工厂这个“训练轮”已经帮你跑出一点感觉:
创建这件事,确实值得被抽离出来单独设计。
接下来,就轮到更进阶的工厂们接力了。
4. 工厂方法 + 抽象工厂:项目长大后的版本升级
当简单工厂胖到没人敢改。 通常就该轮到工厂方法和抽象工厂出场了。
工厂方法先上。 它的想法也不玄学:
“与其一个超级工厂知道所有场景。 不如每个场景有自己的小工厂。”
于是出现了这样的抽象:
struct TaskFactory {
virtual std::unique_ptr<Task> create(const Request&) = 0;
virtual ~TaskFactory() = default;
};
线上有 ProdTaskFactory,
测试有 MockTaskFactory,
不同渠道甚至可以有各自的工厂实现。
业务代码只依赖 TaskFactory& 这个抽象,
真正 new 谁,由外层把“用哪一家工厂”注入进来。
这一步的爽点很直接。
“创建哪种产品”的决定权。
从一坨 if-else 挪到了多态上。
简单工厂帮你把 new 收拢。
工厂方法帮你把那坨 if-else 拆回一地。
等项目再大一点,你会撞上另一种情况:
问题已经不是“我要创建一个对象”,
而是“我要成套换一整组对象”。
比如 UI 主题要改。 Button / TextBox / Menu 得成套换。
比如数据库后端要切。 Connection / Statement / ResultSet 得一起动。
如果每一类都有自己小工厂,
你很容易漏换一两个,
最后界面变成“半新半旧”的魔性混搭风。
抽象工厂的套路,就是把这些“成套出现的产品”再打包一层:
先抽出一个更大的工厂接口。
比如 ThemeFactory。
接口里挂一整排创建函数。
createButton()。
createTextBox()。
这种一眼能看出“同一套家族”的那种。
Light 一套。 Dark 一套。
业务代码只面对 ThemeFactory。
这样你就不太可能把 Button 换成了深色。
TextBox 还在用浅色。
整页风格自然就统一了。
如果把工厂家族粗暴画成一条升级链。 你可以这么记。
简单工厂。
一个地方管所有 new。
先止血。
工厂方法。 按场景拆成多家工厂。 用多态分流复杂度。
抽象工厂。 把一整族产品打包发货。 保证“成套换皮”不出妖怪。
到这里,你已经从“满地 new”走到了“一条像样的对象供应链”。
5. Builder:当“构造过程本身”复杂到值得单独成戏
工厂家族聊的是“这次要哪一类对象”。
但在很多 C++ 项目里,真正折磨人的,根本不是“选哪个类”,而是——
这一坨字段,到底该怎么配,才不至于把人和机器一起搞晕。
最典型的受害者,就是那种“参数墙构造函数”:
Order(const std::string& userId,
const std::string& productId,
int quantity,
bool needInvoice,
bool allowSplit,
bool useCoupon,
bool isVipOnly,
const std::string& remark);
刚写出来的时候,你还能硬背住顺序。
半年之后再看,谁也不敢乱动第七个 bool。
Builder 上来干的。 就是把这件事拆开。 讲清楚。
先有一个干净的对象壳。
再列出一串有名字的小步骤。
最后用 build() 一下收口。
产出一份合法对象。
用点外卖来类比:
工厂更像你在选。 哪家店。 哪种套餐。
Builder 更像你在勾选。 主食。 配菜。 饮料。 备注。
中间那些“还在商量”的状态。 都留在 Builder 里慢慢折腾。 真正送到你手里的。 是一份配置清晰。 确定无误的订单。
在 C++ 项目里。 Builder 典型长在这些地方。
HTTP 请求。 复杂配置。 订单、流水这类字段巨多的对象。
需要一长串 setXxx() 才能凑齐。
字段之间还有一堆隐性约束。 这个有了就不能要那个。
如果这些逻辑都摊在业务代码里,
每个调用方都要自己小心翼翼地按某种顺序 setXxx(),
出 bug 基本全靠缘分。
Builder 做的事情,说白了就是:
把“构造过程”本身抽出来,
写成一段可以讲清楚、可以复用、可以测的代码。
工厂负责“给哪一类对象开模具”,
Builder 负责“这次具体怎么装修这一套”。
6. 原型:当你已经有了一套“样板房”
再往后,就是原型模式(Prototype)。
它关心的点,跟工厂、Builder 又有点不一样。
工厂的视角是:
“我要从无到有,新造一个对象。”
Builder 的视角是:
“这个对象构造流程太长,
我得把过程拆开说。”
原型则在问另外一个问题:
“我这已经有一个配好的对象了。
想按它的样子再来一份,
能不能别每次都从毛坯房开始装修?”
很多游戏、工作流、UI 系统里。 画风大概是这样。
系统启动时。 先撸一堆模板对象。
业务需要新实例时。
不是重头配置。
而是 clone() 一份。
再改少量字段。
在多态场景下,这很自然会长成这样的接口:
struct Shape {
virtual std::unique_ptr<Shape> clone() const = 0;
virtual ~Shape() = default;
};
圆复制出圆,
矩形复制出矩形,
调用方手里就算只有一个 Shape*,也能要来一份“同款新品”。
再配一个“原型注册表”。 按名字存样板。
"big_circle" 对应一只大圆。
"small_rect" 对应一块小矩形。
业务代码按名字 create("big_circle")。
背后其实就是 clone() 一份。
用一句比较好记的比喻收个尾:
抽象工厂像在卖“整套家居套餐”。 你买的是一套风格。 不是某一件单品。
Builder 更像请了个装修队长。 一步步装。 装到你满意为止。
原型则像逛样板间。 看中哪套就照着复制一套。 再微调几笔。
7. 一句话串起来:从 new 到对象生产线
现在把这几位拉到同一张时间线上看。 会顺很多。
一开始。
满地 new。
到处全局变量。
生命周期、依赖关系。
全靠默契和运气。
单例登场。 承认“确实有些东西该全局一份”。 给它套一层门面。 顺带暴露出全局依赖的坑。
简单工厂接力。
先把到处乱飞的 new 收到一个地方。
业务代码不再直接操刀构造。
项目再长胖。 工厂方法把大工厂拆成很多小工厂。 抽象工厂再把一整族产品打包成“主题工厂”。 方便你成套换皮。
当单个对象的构造流程长到离谱。 Builder 把“配置过程本身”抽象出来。 写成一段可读、可测的 DSL。
当你手上已经有一堆样板房。 Prototype 让你从“按规则造”切到“按样板复制”。
再往抽象一点说:
工厂系关注的是“这次要哪一类产品”;
Builder 关注的是“这套产品怎么一步步组装”;
原型 关注的是“有了样板,怎么按样板批量复制”。
它们不是互相抢饭碗,
更像是同一条生产线上,负责不同工位的一群老同事。
以后你每次准备敲下一个 new,
可以先在脑子里弹一行小弹窗:
我现在是在:
选户型? 配装修? 还是照着样板间再来一套?
把这个问题想清楚,
你自然就知道该往哪一种工具上靠。