Day 2:工厂方法模式(Factory Method)

把 new 从调用方剥离——"创建哪个对象"由子类决定,是迈向创建型模式家族的第一步。

目录

01

今日主题

Day 2:工厂方法模式(Factory Method Pattern)

一句话定义:工厂方法定义一个创建对象的接口,但让子类决定实例化哪一个类——把 new 从调用方剥离,创建逻辑延迟到子类。

昨天我们学了单例:它回答"如何保证全局只有一个实例"。今天工厂方法回答的是另一个方向的问题:"当调用方不知道(或不应该知道)具体要创建什么类型的对象时,谁来决定?"。在单例的明日预告里我提到过:instance() 本质上是一个"只产一个产品的工厂"。今天我们把"工厂"的概念正式展开——当每次调用可能产生不同对象、且创建哪种对象需要由策略或配置决定时,就需要工厂方法。

02

为什么需要这个模式

先看一个真实的软件问题。假设你正在开发一个跨平台的 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;
}

这段代码有什么问题?

  • 违反开闭原则(OCP):明天要支持 macOS,你必须回来修改 createTrayIcon() 加一个 else-if——每加一个平台,改一次"分发中心"。
  • 调用方知道太多:MainWindow 被迫知道所有平台类的存在和差异,高耦合。
  • if/else 蔓延:如果托盘图标还有"高 DPI 版""无障碍版"等维度,条件判断会指数膨胀。
  • 不可测试:测试 MainWindow 时必须真的运行在对应平台,无法注入 mock 托盘。

需求继续增加会怎样?加一个 LinuxTrayIcon 的子类、加一个"测试托盘",每次都要改创建函数——创建逻辑的变更频率远高于托盘图标本身的逻辑,这就是需要工厂方法重构的信号。

03

核心思想

一句话:把"创建对象"这个动作本身抽象成一个接口(工厂方法),让"创建什么"由子类决定。

生活类比:餐厅点餐。你(调用方)对服务员说"来一份今日例汤"(调用工厂方法),具体是番茄汤还是蘑菇汤,由后厨(子类工厂)决定。你不需要知道今天的汤是什么,也不需要参与"做汤"的过程——你只依赖"今日例汤"这个稳定接口。明天菜单换了,你不需要改任何代码。

回到软件工程:

  • 哪些代码在变化?——具体创建哪个产品(Windows 还是 Linux 托盘)。
  • 哪些代码应该保持稳定?——调用方对"创建接口"的依赖、产品的抽象接口。
  • 为什么降低耦合?——调用方只依赖 createTrayIcon() 这个抽象方法和 TrayIcon 抽象类,与任何具体类解耦。新增平台 = 新增一个工厂子类 + 一个产品子类,零修改既有代码。

注意区分两个层次:工厂方法(Factory Method) 是"通过继承,让子类决定创建什么";抽象工厂(Abstract Factory) 是"通过组合,创建一族相关产品"。今天先掌握前者,明天对比两者。

04

UML / 角色关系

┌─────────────────────┐        ┌──────────────────────┐
│    Creator (抽象)    │        │   Product (抽象)      │
│---------------------│        │----------------------│
│ + createProduct()   │───────▶│ + operation()        │
│   (工厂方法,虚函数)  │ 创建    │                      │
└─────────┬───────────┘        └──────────┬───────────┘
          ▲                              ▲
          │继承                           │继承
┌─────────┴───────────┐        ┌─────────┴───────────┐
│ ConcreteCreator     │ 创建    │ ConcreteProduct     │
│---------------------│───────▶│---------------------│
│ + createProduct()   │        │ + operation()       │
│   (返回具体产品)     │        │                     │
└─────────────────────┘        └─────────────────────┘

角色说明:

  • Product(抽象产品):定义产品的公共接口,如 TrayIcon。
  • ConcreteProduct(具体产品):具体的平台实现,如 WindowsTrayIcon。
  • Creator(抽象创建者):声明工厂方法 createProduct(),调用方只依赖它。
  • ConcreteCreator(具体创建者):重写工厂方法,返回具体的产品实例。

依赖关系:调用方 → Creator(抽象)→ Product(抽象)。调用方不依赖任何 Concrete 类。谁创建谁:ConcreteCreator 创建 ConcreteProduct。谁调用谁:调用方调用 Creator::createProduct(),实际执行的是 ConcreteCreator 的重写版本。扩展点:新增产品 = 新增 Product 子类 + Creator 子类,已有代码全部不动。

05

最小 C++ 示例

// 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] 渲染原生风格对话框
06

Qt 实战示例

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 依赖)。

07

代码执行流程

以 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 仅作中转。

08

为什么这样设计

  • 解耦发生在哪里:lambda 中的 UI 逻辑只依赖 ChartFactory 抽象和 ChartWidget 抽象,与 BarChart/LineChart 完全解耦。新增"饼图",UI 代码零改动。
  • 扩展点(OCP):加 PieChart + PieChartFactory 两个新类即可,符合"对扩展开放、对修改封闭"。唯一的 if/else 在"选择工厂"处,而这个选择本身也可以抽成注册表进一步消除。
  • 依赖倒置(DIP):高层(UI)不依赖低层(具体图表类),双方都依赖抽象(ChartFactory / ChartWidget)。
  • 组合优于继承:本例用"工厂组合进调用方"的方式组织,产品接口用继承实现多态——创建逻辑与使用逻辑分离。
  • 可测试性:测试时注入 TestChartFactory(返回 mock 图表)即可,无需真实 GUI。
09

不使用会怎样

反例:所有创建逻辑堆在调用方,用 if/else 或 switch 分发:

// 坏味道:switch 发散,每加一种图表都要改这里
switch (chartType) {
    case Bar:  return new BarChart();
    case Line: return new LineChart();
    case Pie:  return new PieChart();   // 加一个就要改一次
    // ... 越来越多的 case
}

随着类型增加:switch 越来越长(发散式变化);调用方被迫知道所有具体类(耦合加深);测试困难(无法替换实现);而且这种 switch 往往在多个地方重复出现(创建、配置、序列化),改一处漏一处。当你在代码里看到多个位置出现同样的"类型 → 对象"分发逻辑时,就是引入工厂方法的时机。

10

何时使用

适合使用:

  • 类事先不知道需要创建哪种对象(如根据配置文件/运行时参数决定)。
  • 创建逻辑需要在子类中定制(框架提供钩子,子类决定产品)。
  • 想集中管理对象创建,避免调用方散落 new。
  • 跨平台/多实现场景(Windows/Linux/macOS 各自工厂)。
  • 框架设计:让用户扩展产品而不改框架核心(Qt 插件系统即此思想)。

不适合使用:

  • 只有一个具体产品、且永远不会变——工厂只是增加一层间接。
  • 对象构造简单(无参数或构造参数固定)——直接 new 更清晰。
  • 过度设计警示:为了"未来可能扩展"而提前抽象,YAGNI。先写直接 new,出现第二个变体时再重构引入工厂。
11

与其他模式的区别

Factory Method vs Abstract Factory:前者用继承(子类覆盖工厂方法),创建一个产品;后者用组合(工厂对象聚合多个工厂方法),创建一族相关产品。场景:只需一种产品 → 工厂方法;需要配套产品族(如"Windows 风格全套控件")→ 抽象工厂。

Factory Method vs Singleton:昨天的 instance() 是"永远返回同一个"的工厂;工厂方法"每次可返回不同对象"。单例约束唯一性,工厂方法提供多态创建。

Factory Method vs Builder:工厂方法一步创建完整对象;Builder 分步构建复杂对象(逐步设置参数)。简单创建用工厂,复杂多步组装用 Builder。

12

Qt 源码中的体现

Qt 框架中工厂方法思想无处不在:

  • QStyleFactory::create():根据样式名("Fusion"/"Windows")创建 QStyle 对象——典型的工厂方法(结合了注册表)。
  • QPlatformTheme / QPA(Qt Platform Abstraction):Qt 通过平台插件工厂创建窗口、剪贴板、托盘等平台相关对象,这正是"跨平台 = 工厂 + 插件"的架构。
  • Qt 插件系统 QPluginLoader:加载插件并通过接口工厂实例化——框架核心不依赖具体插件类。
  • QImageReader::createImageReader():根据文件格式创建对应的图像读取器。

为什么 Qt 这样设计?因为 Qt 要支持几十种平台和格式,如果每个功能都写 if/else 分发,代码将不可维护。工厂 + 插件让 Qt 核心保持稳定,能力通过"创建者"扩展。

13

面试常见问题

先思考,再看答案:

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 异常安全。

14

今日练习

练习:实现一个支持多种格式的序列化工厂(15~30 分钟)

需求:实现一个数据导出程序,支持 JSON / XML / CSV 三种格式。定义 Exporter 抽象产品(含 export(const QVariantMap&)),用工厂方法模式实现 ExporterFactory,调用方根据用户选择(或配置文件)创建对应导出器。

提示:① 三个具体产品类实现各自格式;② 工厂用 std::unique_ptr 返回;③ 思考:如果把"格式选择"也做成注册表(map<QString, 工厂>),if/else 就彻底消失了——试试看。

15

今日总结

一句话记忆:工厂方法 = "把 new 交给子类"——调用方只问"给我一个产品",不问"是什么产品"。

看到以下代码特征时,可以考虑这个模式:

  • 多处出现"根据类型/平台/配置 new 不同类"的 if/else 或 switch。
  • 调用方需要知道太多具体类,修改一个类型要改多个地方。
  • 创建逻辑复杂(参数装配、资源获取),想集中管理。
  • 框架需要留扩展点,让用户注入自己的产品。

明日预告:Day 3 抽象工厂模式(Abstract Factory)。今天我们用继承让"子类决定单产品",明天将看到:当需要创建一族相关产品(如"一套 Windows 风格控件")时,如何用组合让"产品族"整体可替换——工厂方法 vs 抽象工厂的对比也会更清晰。

一句话总结:工厂方法把"创建哪个对象"的决策权交给子类,让调用方与具体类彻底解耦。