如果说简单工厂是"给满地乱飞的 new 搞个前台窗口",
那工厂方法大概就是:
把那个前台拆成一排窗口, 每个窗口只接待自己那一类客人, 具体怎么服务,交给各个窗口自己决定。
很多团队都是这样一路走过来的:
- 第一阶段:到处乱
new,哪儿缺对象就地new一下; - 第二阶段:憋出一个简单工厂,把
new收拢到一个地方; - 第三阶段:某天点开这个工厂 cpp,发现里面 if-else 满天飞, 才开始意识到:是不是该把“怎么造”这件事,拆给不同场景各自负责?
工厂方法模式,就是在这个节点接过简单工厂的接力棒:
- 不再只有一个“上帝工厂”掌管所有类型;
- 而是为不同场景、不同产品线, 各自准备一个小工厂子类;
- 让“创建哪种对象”这件事, 变成一个可以被重写的虚函数——也就是那个“工厂方法”。
用一句比较好记的话说就是:
简单工厂帮你把
new收拢到一个地方, 工厂方法帮你决定:这一行new到底该为谁服务。
下面就沿着这个演化过程,聊聊工厂方法是怎么长出来的,它解决了什么,又该怎么优雅地用在 C++ 代码里。
1. 问题回顾:简单工厂为什么会“长胖”?
在简单工厂那一篇里,我们已经画过这张小图:
Client → SimpleFactory → Product
刚开始只有三五个产品的时候,
你把零散的 new Xxx(...) 都搬进 SimpleFactory::create(type) 里,
会有一种“终于有人管收垃圾了”的舒适感。
但一年两年之后,故事往往会发展成这样:
- 产品线从 3 种涨到 10 种;
- 同一种产品在线上、压测、本地开发要注入不同实现;
- 部分产品还要根据地区、客户等级、AB 实验切不同的策略组合。
很多团队是在线上事故里,才第一次意识到这件事有多要命:
- 某个同事只是在“压测环境分支”里多加了一点新逻辑;
- 线上分支还是走旧实现;
- 结果简单工厂内部三套 if 同时存在, 谁在用哪套实现,全靠大家记得改「对」那个环境分支。
结果就是那个“集中管理创建逻辑”的简单工厂, 自己先长成了一个 if-else 垃圾场:
std::unique_ptr<Task> createTask(const Request& req, Env env) {
if (env == Env::Prod) {
if (req.type == "image") {
return std::make_unique<ImageTask>(...);
} else if (req.type == "video") {
return std::make_unique<VideoTask>(...);
}
// ... 线上更多分支
} else if (env == Env::Staging) {
// 压测环境一套略有不同的实现
} else if (env == Env::Dev) {
// 本地开发再来一套 mock 实现
}
return nullptr;
}
看着还是一个“工厂函数”, 但已经完全背离了当初“把变动点集中起来”的初衷:
- 任何一个场景多/少一个实现,这个函数都得改;
- 想单独测某个场景,只能在这一坨 if-else 里小心翼翼地加分支;
- 你根本没法“只替换某个场景下的创建逻辑”, 因为所有场景都被绑死在一个函数体里。
从经验上说,只要你在简单工厂里开始根据“环境/渠道/场景”分大块 if, 基本就已经站在工厂方法模式的门口了。
2. 一堆小工厂:把场景拆开养
工厂方法的第一步,其实很朴素:
别再试图用一个大工厂服务所有人, 把不同场景拆成多种“工厂类型”, 让调用方只依赖一个抽象工厂接口。
延续简单工厂里那个 Task 的故事,
我们先抽出一个“工厂接口”:
struct TaskFactory {
virtual std::unique_ptr<Task> create(const Request& req) = 0;
virtual ~TaskFactory() = default;
};
真实项目里,这个接口后面往往跟着好几个实现文件, 每个实现都只关心自己负责的那一撮场景, 比起所有逻辑堆在一个函数里,看起来顺眼多了。
然后,为不同场景各自实现一套“怎么造”:
struct ProdTaskFactory : TaskFactory {
std::unique_ptr<Task> create(const Request& req) override {
if (req.type == "image") {
return std::make_unique<ImageTask>(req.payload, 80);
} else if (req.type == "video") {
return std::make_unique<VideoTask>(req.payload, 2000);
}
// ... 线上特有的实现
return nullptr;
}
};
struct DevTaskFactory : TaskFactory {
std::unique_ptr<Task> create(const Request& req) override {
if (req.type == "image") {
return std::make_unique<MockImageTask>(req.payload);
} else if (req.type == "video") {
return std::make_unique<MockVideoTask>(req.payload);
}
// ... 本地调试用的各种 mock
return nullptr;
}
};
业务代码这边,只握着一个 TaskFactory&:
void handleRequest(const Request& req, TaskFactory& factory) {
auto task = factory.create(req);
if (!task) return;
task->run();
}
真正决定“这次要 new 哪一种 Task”,
就交给具体的工厂子类去重写 create() 了。
你可以在程序启动时, 根据配置/命令行参数/编译选项, 选出一个合适的工厂实例, 然后把这个工厂当成依赖,传给所有需要造 Task 的地方。
从结构上看,路线图已经变成了:
Client → TaskFactory(interface)
↑
+--------+--------+
| |
ProdTaskFactory DevTaskFactory
这就是工厂方法的基本味道:
- 调用方只认识一个“工厂抽象”;
- 场景差异被拆成不同的“工厂子类”;
- “创建哪种具体对象”由虚函数多态接管,而不是 if-else 混战。
3. GoF 版的经典例子:把“new 哪种文档”变成虚函数
在《GoF》那本书里,工厂方法有一个很经典的讲法:
- 有一个
Application基类,负责“打开文档、保存文档”等公共流程; - 但每种应用(文字处理、绘图、表格)需要创建的
Document类型不一样; - 于是它把“new 哪种文档”这件事,挪到一个可重写的虚函数里。
翻译成现代 C++,大概是这样:
struct Document {
virtual void print() = 0;
virtual ~Document() = default;
};
struct Application {
void openDocument() {
auto doc = createDocument(); // 这里不关心具体类型
docs_.push_back(std::move(doc));
}
protected:
virtual std::unique_ptr<Document> createDocument() = 0; // 工厂方法
private:
std::vector<std::unique_ptr<Document>> docs_;
};
struct WordApplication : Application {
protected:
std::unique_ptr<Document> createDocument() override {
return std::make_unique<WordDocument>();
}
};
struct SpreadsheetApplication : Application {
protected:
std::unique_ptr<Document> createDocument() override {
return std::make_unique<SheetDocument>();
}
};
这里有几个关键点:
openDocument()是一段和具体文档类型无关的流程代码;- 真正的“new 哪种 Document”,被推迟到
createDocument()这个虚函数里; - 不同的应用类型,只需要继承
Application并重写这个工厂方法即可。
换句话说:
工厂方法往往长在某个“流程类”的保护区域里, 让你可以在不改流程的前提下,替换掉它内部创建的那些对象。
这和我们前面讲的 TaskFactory 版本,本质上是一回事:同一套思路,两种落地方式。
- 共同点是:调用方只依赖一个“抽象的创建入口”(
TaskFactory::create()或Application::createDocument()), 业务流程里不再写死new 某个具体类型; - 差别只是:这个“抽象的创建入口”是放在一个独立工厂类里, 还是放在流程基类里,当成一个可以被子类重写的受保护虚函数。
换成更日常一点的话来说:
TaskFactory版本,是把“造对象”单独拎出来,交给一个专职的工厂对象;Application版本,是把“造对象”塞回到流程基类里, 但用虚函数的方式留给子类拍板:这次到底该 new 谁。
你可以把这两种写法都看成是在做同一件事:
把“new 哪个具体类”的决定,从业务代码里抽出来, 挂到一个可以被重写 / 替换的抽象点上。
“这里两个版本里,最后都是靠虚函数来拍板 new 谁; 差别只在于,这个虚函数是挂在独立的工厂对象上, 还是挂在流程基类上,靠继承来扩展。”
在实际项目里,两种写法经常是交叉使用的。
4. 简单工厂 vs 工厂方法:核心差异在哪?
很多人第一次碰到工厂方法,心里都会打个问号:
不都是“集中创建逻辑”吗? 为啥要多搞一层工厂子类和虚函数?
我一般会拉着新人,从几个很接地气的问题聊起:
1)谁在负责“知道所有产品长什么样”?
- 简单工厂:
- 所有具体产品类型都摆在同一个函数里摊开;
- 新增/删除产品,十有八九要改这个工厂。
- 工厂方法:
- 抽象工厂接口只说“我能造出某个抽象类型”;
- 至于背后具体有哪些产品,散落在不同工厂子类里,各管一摊。
2)你主要在扩哪条维度?“类型”还是“场景”?
- 简单工厂更适合产品不多、场景单一的时候, 主要是在“再多一两个具体类型”这条线上伸长;
- 工厂方法更适合对付场景维度的大爆炸, 比如“线上 / 测试 / 本地”、“不同客户 / 不同地区”。
3)你习惯怎么改?在一个函数里加 if,还是多写几个小类?
- 简单工厂时代,升级路径往往是“给
create()再补一个 if 分支”; - 工厂方法时代,更自然的做法是“再写一个小工厂子类,实现同一个接口”。
如果往下拧半圈,可以从“依赖关系”和“if-else 的下场”这两个角度再看一眼:
- 在简单工厂里,Client 直接依赖的是一个具体的
SimpleFactory实现, 这个工厂里塞满了所有环境 / 所有类型的组合逻辑, 新增一个场景,十有八九要回到同一个create()里加分支—— 有点像“一个人知道所有秘密”。 - 在工厂方法里,Client 只依赖
TaskFactory这个抽象, 至于有ProdTaskFactory、DevTaskFactory还是别的什么, Client 不操心,只要有人在外面把“对的工厂对象”塞进来就行—— 变成“每个场景自己管自己的秘密”。
同时,那坨最吓人的 if-else 也不是原封不动地搬家,而是被拆解掉了:
- 原来在一个大工厂函数里,根据
env写一长串if (env == Prod) ... else if (env == Dev) ...; - 现在往往是在程序启动的组装阶段,用一次判断 / 配置,选出一个合适的工厂实例,
后面的业务代码就只是
factory.create(req),再也不用到处写if (env == ...); - 场景这个维度,从“if 里的一个表达式”,升级成了“有自己类的一级公民”, 让编译器帮你记住:这个场景有自己的一套工厂实现。
在一个项目刚起步的时候, 你很难一口气把所有场景、所有产品线都想清楚, 用简单工厂先把“创建逻辑从业务里拔出来”,一点问题没有。
等到有一天,你开始频繁按环境、渠道、客户维度切换实现, 那种“再给简单工厂加两个 if 就完事了”的轻松感就会慢慢消失, 这时候,让每种场景各自拿一个工厂子类, 往往会比继续在同一个函数里打补丁舒服得多。
也就是在这个阶段,工厂方法自然就该上场了。
如果从“重构简单工厂”的角度来粗暴地记它: 其实就是给已经长胖的简单工厂按场景做一次归类, 再抽出一个共同的工厂基类,让不同场景用子类各自把那摊逻辑接走。
5. 和抽象工厂、单例、依赖注入的那点事
工厂方法经常会和别的概念缠在一起出现, 如果只看名词,很容易看着看着就晕菜:
- 有人把“工厂接口 + 多个工厂子类”叫工厂方法;
- 再把“工厂接口 + 多个产品族”往前推一格,叫抽象工厂;
- 顺手再把那个工厂做成单例,最后整个系统到处
XxxFactory::instance()……
先拎几个最容易混淆的点出来:
- 抽象工厂主要操心的是“一组相关产品族”:
- 比如同一主题下的一整套 Button / TextBox / Menu;
- 同一数据库驱动下的一整套 Connection / Statement / ResultSet;
- 它手里往往有好几个工厂方法,分别生产这个族里的不同产品。
- 工厂方法更多是在“单一产品线 + 多个创建策略”这个缝里活动:
- 比如都是
Task,只是线上和本地的实现不同; - 或者都是
Document,只是 Word / Sheet / Slide 版本不同。
- 比如都是
和单例、依赖注入的关系,我一般这么拆给同事听:
- 工厂本身要不要是单例,是一个生命周期 / 访问方式的问题;
- 工厂方法解决的是“谁拍板:这次该 new 哪种实现”;
- 依赖注入则关心“谁负责把这个工厂(或它造出来的东西)塞进来”。
在现代 C++ 项目里,我自己比较舒服的一种搭配是:
- 用工厂方法 / 抽象工厂把“创建逻辑”抽象出来;
- 用构造函数注入(或者一个很克制的手写 IoC 容器)把工厂传给需要的人;
- 至于工厂是不是全局一份,放到更外层慢慢权衡,而不是在业务代码里到处
instance()。
6. 小结:当你开始嫌简单工厂“臃肿”的那一刻
回头看简单工厂 → 工厂方法这条线, 其实就是从:
- “所有人排队到一个窗口办事”,
走向:
- “根据自己的需求,去不同窗口找更懂你的人”。
作为一个写 C++ 写久了的人,我更愿意这样记工厂方法:
- 它不是推翻简单工厂,而是帮简单工厂善后。
- 简单工厂帮你把
new从业务里拔出来; - 工厂方法帮你把已经长胖的简单工厂拆开养。
- 简单工厂帮你把
- 当你开始在工厂里根据“环境/渠道/场景”写大块 if 时, 就差不多该考虑“是不是该让这些场景各自拿一个工厂子类”。
- 当你有一个流程类,总是在里面
new各种不同实现时, 不妨问问自己: “能不能把这几行new挪进一个受保护的虚函数里, 让子类接手这件事?”
真正有用的,不是你背得出多少模式定义,
而是每次准备敲下一个 new 的时候,
脑子里会闪一下:
这次只是多一个简单类型, 还是已经牵扯到一整套场景切换?
如果只是前者,也许一个简单工厂就够了; 如果已经是后者,那就是工厂方法发光发热的时候了。
你也可以现在就对照一下自己项目里的代码:
- 那个造对象的地方,是一个“万能简单工厂”, 还是已经拆成了几种更聚焦的小工厂?
- 你们现在是靠修改同一个大型工厂函数来切环境, 还是在不同环境下注入了不同的工厂实现?
如果这两篇「简单工厂 / 工厂方法」能让你想起自己项目里某个又爱又恨的工厂类, 可以回头把那篇简单工厂再翻一遍, 对照着看一看你们现在停在了哪一个阶段。
再往前走一步,就是整套产品族一起换皮的抽象工厂模式——
不是只换一个 Task,而是一整家“家居套餐”一起换主题。
等这一篇写完,我们就可以把「简单工厂 / 工厂方法 / 抽象工厂」打包成一个小系列,
发给团队里还在 if-else 垃圾场里挣扎的同事一起吐槽。