如果说单例是"给全局变量穿西装",那简单工厂大概就是"给满地乱飞的 new 搞个前台窗口"。
老一代 C / C++ 项目里,new(或者更早的 malloc)就像是空气:
- 想要一个对象?
new一下; - 想换个实现?把旧的
new注释掉,写个新的;
时间一长,代码里就会出现一种熟悉的画面:
- 业务里到处都是
new Xxx(...); - 每个地方
new的参数都不太一样; - 想换一个实现、加一个分支,得把半个代码库翻一遍。
很多人第一次真正开始讨厌这套写法,往往是某次线上事故:
- 有人只是想给
ImageTask多加一个参数; - 改了其中几个调用点,漏掉了别的角落;
- 结果部分请求走了「旧逻辑 + 新假设」,线上行为一片混乱,
排查了半天才发现罪魁祸首就是那几行分散的
new。
后来总会有人在 code review 上忍不住吐槽:
要不,把这些
new集中到一个地方, 谁要对象就来这个地方排队领, 这不就有点像“工厂”了吗?
这就是所谓简单工厂的出发点:把“创建什么对象、怎么创建”这件事,从业务代码里抽出来,塞进一个专门的地方统一处理。
从设计理念上讲,这一招非常“朴素面向对象”:
- 既然“创建对象”这件事到处在发生、又经常变化,
- 那就给它单独找个角色来负责,
- 至少先做到:变的地方收拢到一坨,不要到处长蘑菇。
可以用一句话来概括这段心路:
真正可怕的不是到处乱 new, 而是你以为自己“集中管理了”,结果只是换了一个地方乱 new。
下面我们就沿着这个动机,聊聊简单工厂是怎么长出来的,它解决了什么,又有什么边界。
1. 到处乱飞的 new:五分钟写完,五年救火
先看一段非常真实的业务代码味道(为保护当事人,这里稍微改编一下):
void handleRequest(const Request& req) {
if (req.type == "image") {
auto* task = new ImageTask(req.payload, /* quality = */ 80);
task->run();
} else if (req.type == "video") {
auto* task = new VideoTask(req.payload, /* bitrate = */ 2000);
task->run();
} else if (req.type == "text") {
auto* task = new TextTask(req.payload, /* lang = */ "zh-CN");
task->run();
} else {
// ...
}
}
刚写出来的时候,这段代码还挺“直男式清晰”的:
- if 分支一对一对应到不同的
Task; - 参数也在这块硬编码好,看上去一目了然。
几年之后你再看:
- 需求一变,
Task的构造函数签名改了:- 你得把所有调用点翻一遍;
- 新增一个
AudioTask,if-else 又长了一截; - 你想在别的模块也复用这套“
type→ 对象”的逻辑, 只能复制粘贴整段if-else + new。
这里最刺眼的坏味道有两个:
- “创建什么”跟“怎么用”混在一起:
handleRequest明明更关心“拿到一个基类指针把它run()一下”;- 却被迫知道每个
Task构造函数的细节;
- 变化分布在很多地方:
- 一旦新增/调整某个
Task, - 你要去所有
new它的地方同步修改。
- 一旦新增/调整某个
简单工厂想做的,就是把这些容易变的 new 细节收拢到一个地方:
- “变”的时候只动一处;
- “用”的地方只管拿到一个能用的对象,别管它是哪一款。
2. 先别谈模式,先拉出一个“造对象的函数”
用最土的方式改一下上面的代码,你大概会写出这么一个函数:
std::unique_ptr<Task> createTask(const Request& req) {
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);
} else if (req.type == "text") {
return std::make_unique<TextTask>(req.payload, "zh-CN");
}
return nullptr; // 或者抛异常/返回某种默认任务
}
void handleRequest(const Request& req) {
auto task = createTask(req);
if (!task) return; // 忽略/记录错误
task->run();
}
这一步其实就已经把“简单工厂”的核心干完了:
handleRequest里不再出现new;- 它只关心“我要一个
Task来run()”; - 至于底层到底是
ImageTask还是VideoTask, 完全交给createTask决定。
从职责划分的角度看:
handleRequest更像是业务流程编排;createTask更像是对象创建策略的集中处。
如果你愿意起个更有仪式感的名字,
给这个函数所在的类取个名叫 TaskFactory,
这就是最常见的“简单工厂类”了。
你可以把这一步理解成:
- 从“到处 new”升级成“集中在一个地方 new”;
- 从“业务代码既管流程又管构造细节”, 升级成“业务只管流程,构造细节交给一个小工厂”。
3. 为什么叫“简单”工厂?它简单在哪,又哪里会撑不住?
很多书会用一张图解释简单工厂:
- 一堆具体产品类:
ImageTask/VideoTask/TextTask; - 一个工厂类:
TaskFactory,里面一个create(TaskType); - 外面的人只跟工厂打交道,不再直接
new产品。
如果用一行极简示意图来画,大概是这样:
Client → SimpleFactory → Product
刚上手的时候,这种结构有一种“久旱逢甘霖”的爽感:
- 你把业务里零零散散的
new Xxx都搬进工厂; - 以后新增一个
AudioTask,只要在工厂里多加一段if (type == audio); - 其它几十处
handleRequest()原地不动,继续拿着std::unique_ptr<Task>调run()。
所以它叫“简单”,一方面是写起来简单:
- 就是一个函数 + 一串
if-else; - 不需要动用虚函数、抽象基类那一整套“正统工厂方法”配置;
另一方面是心智模型简单:
- “这堆 Task 都是一个产品族,我就认一个入口
TaskFactory::create”; - 其他代码一律只面对
Task这个基类; - 你甚至可以在 code review 上画一张很漂亮的“使用方 → 工厂 → 产品”小图。
问题来了:这种爽感一般只能维持到“产品还不算太多”的阶段。
几乎每个老项目都会经历这么一段历史:
- 第一年只有
ImageTask/VideoTask/TextTask,工厂很干净; - 第二年产品线扩到 8 种、10 种任务,
createTask()里的if-else已经要翻屏了; - 再后来,每加一个新业务,就有人顺手在工厂里再添一段分支, 结果那个号称“集中管理创建逻辑的地方”,自己成了新的 超级 if-else 垃圾堆。
更要命的是,随着业务发展,变化往往不再是“再来一个 Task 子类”这么简单:
- 有时候你想按渠道区分不同的 Task 实现(比如 web / mobile / 内部接口);
- 有时候又想按地区 / 客户等级套不同的参数模板;
- 甚至有的 Task 需要注入一整套策略对象、配置对象。
这时候你会发现:
- 一个单一的
TaskFactory想同时管住“类型维度 + 渠道维度 + 配置维度”, - 要么把参数列表堆成一团、要么在工厂里套多层
if (region == ...) { if (channel == ...) ... }, - 看着就不像是“集中变化点”,更像是“把所有变化点拧成一团”。
还有一个在大公司里很常见的故事:
- 某个产品线想在运行时从配置中心拉一段 JSON, 决定这次究竟要 new 哪种策略组合;
- 简单工厂这种“编译期写死
if-else”的模式一下就吃不消了, 你要么开始在工厂里写一整套解析逻辑,要么干脆放弃它,另起炉灶。
所以简单工厂的“简单”,其实是有前提的:
产品族相对稳定,变化主要体现在“偶尔多一两种具体产品”。
在这个范围内,它非常对味:
- 让你先尝到把
new挪出业务代码的甜头; - 让调用方只面对一个稳定的抽象类型;
- 让“新增一个小类型”只改工厂一处代码。
真正值得带走的,不是那个“万能大工厂类”, 而是这一步:
先学会把创建逻辑从业务流程里剥离出来, 再根据产品复杂度,决定要不要进一步拆成工厂方法 / 抽象工厂。
当你发现那个工厂类自己开始发胖、分支横飞、难以复用, 基本上就该考虑往「工厂方法模式」甚至「抽象工厂」那一路升级了—— 也就是:
- 不是所有产品都从一个地方 new 出来;
- 而是每个具体工厂负责自己那一撮产品,
- 通过多态把“创建哪种产品”的决策下放到子类。
用一句很粗糙但好记的对比:
- 简单工厂 = 一个大工厂 + 一坨
if-else; - 工厂方法 = 一堆小工厂,每个小工厂只管自己这一撮产品。
在工厂方法里,你通常会有一个抽象工厂接口,例如:
struct TaskFactory {
virtual std::unique_ptr<Task> create(const Request&) = 0;
virtual ~TaskFactory() = default;
};
不同渠道 / 不同产品线各自继承这个接口,实现自己的 create():
- Web 版有一个
WebTaskFactory; - 移动端有一个
MobileTaskFactory; - 测试环境甚至可以有一个
MockTaskFactory。
调用方只握着一个 TaskFactory& 接口指针,真正“创建哪种 Task”这件事,
交给具体工厂子类的虚函数决定——这就是工厂方法的基本味道。
所以一个很实用的 heuristics 是:
- 当你的简单工厂开始根据“渠道/环境/场景”写分支,
- 当你在工厂里看到越来越多「如果是线上就 new A,如果是测试环境就 new B」,
这往往就是一个信号:
是时候让这些场景各自拿一个工厂子类, 而不是继续在一个简单工厂里堆 if-else 了。
这一步就超出了“简单工厂”的范围, 留给后面的章节慢慢展开。
4. 和单例、Service Locator 的小八卦
有趣的是,简单工厂和单例这俩,在很多旧项目里经常勾肩搭背一起出现:
- 先来一个
TaskFactory; - 再把它做成
TaskFactory::instance()的单例; - 然后到处
TaskFactory::instance().createTask(...)。
这在小项目里没什么问题, 但一旦规模大起来,就很容易滑向“简易 Service Locator”:
- 你以为每个模块只依赖一个
Task; - 实际上它还暗搓搓依赖了“全局那一个工厂”;
- 等你要写测试、要做分层,才发现所有人都在背后直接摸这个工厂。
从设计理念上看,这也是后来很多人更推崇“显式依赖”的原因:
- 需要工厂,就把
TaskFactory&当成构造参数传进来; - 工厂内部再统一管理 new 的细节;
- 至于这个工厂本身是不是单例、是不是从 IoC 容器里拿, 那是更外层的架构问题,不是业务代码该操心的。
用一句有点偏门的总结就是:
简单工厂关心的是“把变动点集中起来”, 单例关心的是“这东西全局只有一份”, 把两者随手绑一起,很容易变成“ 既哪儿都能 new,又哪儿都能访问”—— 写起来爽,改起来哭。
5. 小结:简单工厂是一块“训练轮”
站在今天回头看这些年的 C++ 项目,我越来越觉得: 简单工厂的地位,有点像你学骑车时后面那对小轮子—— 不帅,但在某个阶段非常救命。
很多团队从「哪儿想 new 就哪儿 new」的 C 式写法, 慢慢往稍微讲点设计的 OO 过渡,大概都会走这么几步:
- 先憋出第一个简单工厂,心里第一次有了个念头: “原来创建这件事也可以集中起来管。”
- 再过一阵子,业务代码里几乎看不到
new了, 大家只知道“我要一个Task”,至于是ImageTask还是别的,丢给工厂去纠结; - 然后有一天你点开那个工厂 cpp,发现它胖得一塌糊涂, 这才痛下决心:该把创建逻辑拆出去,走向工厂方法、抽象工厂,甚至 IoC 容器那条路。
所以说简单工厂是「训练轮」,不是骂它幼稚, 而是承认它在一个特定阶段确实帮了大忙:
- 它逼着你把“创建”和“使用”分开来看;
- 它把满地乱飞的
new收拢到一个地方,好改、好搜; - 它最后还用自己变成 if-else 垃圾场的方式, 反过来提醒你:是时候学一点更高级的工厂玩法了。
真要给这段路程找几句可以贴在工位上的话,大概是这样:
- 能不在业务里
new,就别在业务里new,让它们排队走工厂; - 一旦工厂开始频繁根据渠道/环境/场景分支,就考虑拆成多个更小、更专一的工厂;
- 模式的名字没那么重要,更重要的是你脑子里多了一个格子: “创建这件事,本身也是可以单独设计的。”
等你养成这种习惯之后,写代码的节奏会有点不一样:
每当手里准备敲下一个
new, 脑子里会先闪一下: “这个东西以后会不会经常变? 要不要给它找个地方集中照顾?”
如果你看到这里,对那个“胖成垃圾堆的工厂类”已经有点画面感, 下一篇我们就顺势聊聊 工厂方法模式:
- 同样是“造对象”,
- 它是怎么靠“一堆小工厂 + 多态”把简单工厂里那坨 if-else 慢慢拆回一地的。