把 new 从调用方剥离——"创建哪个对象"由子类决定,是迈向创建型模式家族的第一步。
Day 2:工厂方法模式(Factory Method Pattern)
一句话定义:工厂方法定义一个创建对象的接口,但让子类决定实例化哪一个类——把 new 从调用方剥离,创建逻辑延迟到子类。
昨天我们学了单例:它回答"如何保证全局只有一个实例"。今天工厂方法回答的是另一个方向的问题:"当调用方不知道(或不应该知道)具体要创建什么类型的对象时,谁来决定?"。在单例的明日预告里我提到过:instance() 本质上是一个"只产一个产品的工厂"。今天我们把"工厂"的概念正式展开——当每次调用可能产生不同对象、且创建哪种对象需要由策略或配置决定时,就需要工厂方法。
先看一个真实的软件问题。假设你正在开发一个跨平台的 Qt 桌面应用,需要支持 Windows 和 Linux 两种平台下的系统托盘图标创建(不同平台实现不同):
// 坏味道示例:调用方直接 new 具体类,用 if/else 判断平台
class TrayIcon { public: virtual void show() = 0; };
class WindowsTrayIcon : public TrayIcon { public: void show() override { /* Win32 API */ } };
class LinuxTrayIcon : public TrayIcon { public: void show() override { /* StatusNotifier */ } };
// 调用方(MainWindow)直接决定创建哪个类
TrayIcon* createTrayIcon(const QString& platform) {
if (platform == "windows") return new WindowsTrayIcon();
if (platform == "linux") return new LinuxTrayIcon();
return nullptr;
}
这段代码有什么问题?
createTrayIcon() 加一个 else-if——每加一个平台,改一次"分发中心"。需求继续增加会怎样?加一个 LinuxTrayIcon 的子类、加一个"测试托盘",每次都要改创建函数——创建逻辑的变更频率远高于托盘图标本身的逻辑,这就是需要工厂方法重构的信号。
一句话:把"创建对象"这个动作本身抽象成一个接口(工厂方法),让"创建什么"由子类决定。
生活类比:餐厅点餐。你(调用方)对服务员说"来一份今日例汤"(调用工厂方法),具体是番茄汤还是蘑菇汤,由后厨(子类工厂)决定。你不需要知道今天的汤是什么,也不需要参与"做汤"的过程——你只依赖"今日例汤"这个稳定接口。明天菜单换了,你不需要改任何代码。
回到软件工程:
createTrayIcon() 这个抽象方法和 TrayIcon 抽象类,与任何具体类解耦。新增平台 = 新增一个工厂子类 + 一个产品子类,零修改既有代码。注意区分两个层次:工厂方法(Factory Method) 是"通过继承,让子类决定创建什么";抽象工厂(Abstract Factory) 是"通过组合,创建一族相关产品"。今天先掌握前者,明天对比两者。
┌─────────────────────┐ ┌──────────────────────┐
│ Creator (抽象) │ │ Product (抽象) │
│---------------------│ │----------------------│
│ + createProduct() │───────▶│ + operation() │
│ (工厂方法,虚函数) │ 创建 │ │
└─────────┬───────────┘ └──────────┬───────────┘
▲ ▲
│继承 │继承
┌─────────┴───────────┐ ┌─────────┴───────────┐
│ ConcreteCreator │ 创建 │ ConcreteProduct │
│---------------------│───────▶│---------------------│
│ + createProduct() │ │ + operation() │
│ (返回具体产品) │ │ │
└─────────────────────┘ └─────────────────────┘
角色说明:
TrayIcon。WindowsTrayIcon。createProduct(),调用方只依赖它。依赖关系:调用方 → Creator(抽象)→ Product(抽象)。调用方不依赖任何 Concrete 类。谁创建谁:ConcreteCreator 创建 ConcreteProduct。谁调用谁:调用方调用 Creator::createProduct(),实际执行的是 ConcreteCreator 的重写版本。扩展点:新增产品 = 新增 Product 子类 + Creator 子类,已有代码全部不动。
// product.h —— 抽象产品:所有对话框的公共接口
#include <iostream>
#include <memory>
#include <string>
class Dialog {
public:
virtual ~Dialog() = default;
virtual void render() const = 0; // 渲染对话框(纯虚)
};
// 具体产品 A:Windows 风格对话框
class WindowsDialog : public Dialog {
public:
void render() const override {
std::cout << "[Windows] 渲染原生风格对话框" << std::endl;
}
};
// 具体产品 B:Web 风格对话框
class WebDialog : public Dialog {
public:
void render() const override {
std::cout << "[Web] 渲染 HTML/CSS 风格对话框" << std::endl;
}
};
// creator.h —— 抽象创建者:工厂方法 createDialog()
class DialogFactory {
public:
virtual ~DialogFactory() = default;
// 工厂方法:创建对话框,由子类决定具体类型
virtual std::unique_ptr<Dialog> createDialog() const = 0;
// 模板方法:调用方统一走这个入口,不直接碰 createDialog
void showDialog() const {
auto dlg = createDialog(); // 多态调用,实际创建由子类决定
dlg->render(); // 使用产品
}
};
// 具体创建者:Windows 工厂
class WindowsDialogFactory : public DialogFactory {
public:
std::unique_ptr<Dialog> createDialog() const override {
return std::make_unique<WindowsDialog>(); // 只生产 Windows 对话框
}
};
// 具体创建者:Web 工厂
class WebDialogFactory : public DialogFactory {
public:
std::unique_ptr<Dialog> createDialog() const override {
return std::make_unique<WebDialog>();
}
};
// main.cpp —— 使用工厂方法
#include "creator.h"
int main() {
WindowsDialogFactory winFactory; // 具体创建者
WebDialogFactory webFactory;
winFactory.showDialog(); // 输出 [Windows] ...
webFactory.showDialog(); // 输出 [Web] ...
// 通过基类指针调用,验证多态
const DialogFactory* factory = &winFactory;
auto dlg = factory->createDialog();
dlg->render();
return 0;
}
代码执行过程:main 创建两个具体工厂 → 调用 showDialog()(基类模板方法)→ 内部调用 createDialog()(虚函数分发到具体工厂)→ 具体工厂 make_unique 创建对应产品 → 产品 render() 输出。注意全程使用 std::unique_ptr,RAII 自动释放,无泄漏。
程序输出:
[Windows] 渲染原生风格对话框
[Web] 渲染 HTML/CSS 风格对话框
[Windows] 渲染原生风格对话框
Qt 里工厂方法最经典的体现是通过自定义工厂类创建不同类型的 Widget。下面是一个"仪表盘应用",根据配置决定创建哪种图表控件(柱状图 / 折线图):
// chartwidget.h —— 抽象产品:图表控件
#ifndef CHARTWIDGET_H
#define CHARTWIDGET_H
#include <QWidget>
class ChartWidget : public QWidget {
Q_OBJECT
public:
explicit ChartWidget(QWidget* parent = nullptr) : QWidget(parent) {}
virtual void setData(const QVector<double>& values) = 0; // 纯虚:填充数据
};
#endif // CHARTWIDGET_H
// barchart.h / linechart.h —— 两个具体产品(简化实现)
#ifndef BARCHART_H
#define BARCHART_H
#include "chartwidget.h"
#include <QPainter>
class BarChart : public ChartWidget {
Q_OBJECT
public:
using ChartWidget::ChartWidget;
void setData(const QVector<double>& values) override { m_values = values; update(); }
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
for (int i = 0; i < m_values.size(); ++i) {
p.drawRect(i * 40, height() - m_values[i] * 10, 30, m_values[i] * 10); // 画柱
}
}
private:
QVector<double> m_values;
};
#endif
// chartfactory.h —— 抽象创建者:工厂方法 createChart()
#ifndef CHARTFACTORY_H
#define CHARTFACTORY_H
#include "chartwidget.h"
#include <memory>
class ChartFactory {
public:
virtual ~ChartFactory() = default;
virtual std::unique_ptr<ChartWidget> createChart(QWidget* parent) const = 0; // 工厂方法
};
class BarChartFactory : public ChartFactory {
public:
std::unique_ptr<ChartWidget> createChart(QWidget* parent) const override {
return std::make_unique<BarChart>(parent); // 生产柱状图
}
};
class LineChartFactory : public ChartFactory {
public:
std::unique_ptr<ChartWidget> createChart(QWidget* parent) const override {
return std::make_unique<LineChart>(parent);
}
};
#endif
// main.cpp —— 根据配置选择工厂
#include <QApplication>
#include "chartfactory.h"
#include "barchart.h"
#include "linechart.h"
#include <QVBoxLayout>
#include <QComboBox>
#include <QMainWindow>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QMainWindow window;
auto* central = new QWidget(&window);
auto* layout = new QVBoxLayout(central);
auto* combo = new QComboBox(central);
combo->addItems({"bar", "line"}); // 用户选择图表类型
layout->addWidget(combo);
// 根据用户选择决定用哪个工厂 —— 这就是"创建哪个对象由运行时决定"
QObject::connect(combo, &QComboBox::currentTextChanged, central, [central, layout](const QString& type) {
// 清掉旧的图表(删除布局里的子项)
while (QLayoutItem* item = layout->takeAt(1)) {
delete item->widget();
delete item;
}
std::unique_ptr<ChartFactory> factory;
if (type == "bar") factory = std::make_unique<BarChartFactory>();
else factory = std::make_unique<LineChartFactory>();
// 工厂方法:创建具体图表控件,挂到 Qt 父子树上(内存由 Qt 管理)
auto chart = factory->createChart(central);
chart->setData({3.2, 5.1, 4.4, 6.0, 2.8});
layout->addWidget(chart.release());
});
window.setCentralWidget(central);
window.resize(400, 300);
window.show();
return app.exec();
}
Qt 5 / Qt 6 区别:本例使用 Qt 6 的 QComboBox::currentTextChanged 和 lambda 连接语法(Qt 5 同样支持);QVector 在 Qt 6 中与 QList 统一,两者皆可。代码可在 Qt Creator 中直接编译运行(CMake 自动处理 Qt 依赖)。
以 Qt 示例为例,完整流程:
① 程序启动,main 创建 QMainWindow,界面上有一个下拉框(bar/line)。
② 用户在下拉框选择 "line" → 发射 currentTextChanged("line") 信号。
③ 连接的 lambda 执行:清空布局中的旧图表 → 根据 "line" 创建 LineChartFactory(make_unique)。
④ 调用 factory->createChart(central) —— 虚函数分发,执行 LineChartFactory::createChart,make_unique<LineChart>(parent) 创建折线图控件并挂到 Qt 父子树(parent 是 central)。
⑤ setData 填充数据并 update() 触发重绘,paintEvent 画出折线。
⑥ 布局 addWidget 显示新图表。对象生命周期:Qt 父子机制自动管理(父对象销毁时子对象随之销毁),unique_ptr 仅作中转。
ChartFactory 抽象和 ChartWidget 抽象,与 BarChart/LineChart 完全解耦。新增"饼图",UI 代码零改动。PieChart + PieChartFactory 两个新类即可,符合"对扩展开放、对修改封闭"。唯一的 if/else 在"选择工厂"处,而这个选择本身也可以抽成注册表进一步消除。TestChartFactory(返回 mock 图表)即可,无需真实 GUI。反例:所有创建逻辑堆在调用方,用 if/else 或 switch 分发:
// 坏味道:switch 发散,每加一种图表都要改这里
switch (chartType) {
case Bar: return new BarChart();
case Line: return new LineChart();
case Pie: return new PieChart(); // 加一个就要改一次
// ... 越来越多的 case
}
随着类型增加:switch 越来越长(发散式变化);调用方被迫知道所有具体类(耦合加深);测试困难(无法替换实现);而且这种 switch 往往在多个地方重复出现(创建、配置、序列化),改一处漏一处。当你在代码里看到多个位置出现同样的"类型 → 对象"分发逻辑时,就是引入工厂方法的时机。
适合使用:
不适合使用:
Factory Method vs Abstract Factory:前者用继承(子类覆盖工厂方法),创建一个产品;后者用组合(工厂对象聚合多个工厂方法),创建一族相关产品。场景:只需一种产品 → 工厂方法;需要配套产品族(如"Windows 风格全套控件")→ 抽象工厂。
Factory Method vs Singleton:昨天的 instance() 是"永远返回同一个"的工厂;工厂方法"每次可返回不同对象"。单例约束唯一性,工厂方法提供多态创建。
Factory Method vs Builder:工厂方法一步创建完整对象;Builder 分步构建复杂对象(逐步设置参数)。简单创建用工厂,复杂多步组装用 Builder。
Qt 框架中工厂方法思想无处不在:
QStyleFactory::create():根据样式名("Fusion"/"Windows")创建 QStyle 对象——典型的工厂方法(结合了注册表)。QPlatformTheme / QPA(Qt Platform Abstraction):Qt 通过平台插件工厂创建窗口、剪贴板、托盘等平台相关对象,这正是"跨平台 = 工厂 + 插件"的架构。QPluginLoader:加载插件并通过接口工厂实例化——框架核心不依赖具体插件类。QImageReader::createImageReader():根据文件格式创建对应的图像读取器。为什么 Qt 这样设计?因为 Qt 要支持几十种平台和格式,如果每个功能都写 if/else 分发,代码将不可维护。工厂 + 插件让 Qt 核心保持稳定,能力通过"创建者"扩展。
先思考,再看答案:
Q1:工厂方法模式和直接 new 相比,核心收益是什么?(提示:OCP、解耦、测试)
Q2:工厂方法中,调用方依赖的是什么?为什么说它符合依赖倒置原则?
Q3:Qt 的 QStyleFactory 用了工厂方法吗?它和"注册表模式"结合解决了什么问题?
Q4:如果只有一个产品类,还需要工厂方法吗?什么时候算过度设计?
Q5:C++ 中工厂方法返回裸指针 vs unique_ptr 的区别?为什么推荐 unique_ptr?
参考答案:Q1:调用方与具体类解耦,新增类型不改旧代码(OCP),可注入 mock 提升可测试性。Q2:依赖 Creator 和 Product 两个抽象,高层与低层都依赖抽象,即 DIP。Q3:是,QStyleFactory 按名称创建 QStyle,注册表让插件可动态注册新样式,避免硬编码分发。Q4:只有一个产品且无扩展预期时不需要,YAGNI;出现第二个变体或"创建逻辑需要子类定制"时再引入。Q5:裸指针易泄漏、所有权不明;unique_ptr 明确独占所有权、RAII 自动释放,配合 make_unique 异常安全。
需求:实现一个数据导出程序,支持 JSON / XML / CSV 三种格式。定义 Exporter 抽象产品(含 export(const QVariantMap&)),用工厂方法模式实现 ExporterFactory,调用方根据用户选择(或配置文件)创建对应导出器。
提示:① 三个具体产品类实现各自格式;② 工厂用 std::unique_ptr 返回;③ 思考:如果把"格式选择"也做成注册表(map<QString, 工厂>),if/else 就彻底消失了——试试看。
一句话记忆:工厂方法 = "把 new 交给子类"——调用方只问"给我一个产品",不问"是什么产品"。
看到以下代码特征时,可以考虑这个模式:
明日预告:Day 3 抽象工厂模式(Abstract Factory)。今天我们用继承让"子类决定单产品",明天将看到:当需要创建一族相关产品(如"一套 Windows 风格控件")时,如何用组合让"产品族"整体可替换——工厂方法 vs 抽象工厂的对比也会更清晰。