Day 3:抽象工厂模式(Abstract Factory)

把"一套配套对象"的组装交给一个工厂——不再创建单个对象,而是创建一整族相互匹配的对象。

目录

  1. 今日主题
  2. 为什么需要这个模式
  3. 核心思想
  4. UML / 角色关系
  5. 最小 C++ 示例
  6. Qt 实战示例
  7. 代码执行流程
  8. 为什么这样设计
  9. 不使用会怎样
  10. 何时使用
  11. 与其他模式的区别
  12. Qt 源码中的体现
  13. 面试常见问题
  14. 今日练习
  15. 今日总结
①

今日主题

抽象工厂模式(Abstract Factory):提供一个创建一系列相关或相互依赖对象(即"产品族")的接口,而无需指定它们的具体类。

如果说工厂方法模式解决的是"一个产品"的创建问题,那么抽象工厂解决的是"一整组配套产品"的创建问题——它把工厂方法从"一个类一个工厂"升级为"一个产品族一个工厂"。

一句话记忆:工厂方法管"一个",抽象工厂管"一窝"。一个工厂 = 一个产品族(family),族内所有产品保证风格统一、互相配套。
②

为什么需要这个模式

先看一段"没有模式"的坏代码。假设我们要做一个跨平台 GUI 库,支持 Windows 和 macOS 两套风格,每套风格都有按钮和对话框两种控件。新手最常见的写法:

// 坏味道:客户端直接 new 具体类,用 if-else 手工组装"配套对象"
void buildUI(const std::string& platform) {
    std::unique_ptr<Button>  btn;
    std::unique_ptr<Dialog>  dlg;

    if (platform == "windows") {
        btn = std::make_unique<WindowsButton>();   // 平台判断散落各处
        dlg = std::make_unique<WindowsDialog>();
    } else if (platform == "mac") {
        btn = std::make_unique<MacButton>();
        dlg = std::make_unique<MacDialog>();
    } else {
        throw std::runtime_error("未知平台");
    }

    btn->render();
    dlg->show();
}

这段代码的问题非常典型:

  • OCP 违背(开闭原则):每新增一个平台(Linux、iOS),就要在所有出现 if-else 的地方各加一个分支;新增一种产品(文本框),又要在每个分支里各加一段创建代码。改一处牵动全身,改 N 处漏一处就出 bug。
  • 知识泄漏 / 耦合过深:业务代码(buildUI)本不该知道"WindowsButton 存在"这件事,现在却直接依赖了所有具体类——高层模块直接依赖低层模块,违背 DIP(依赖倒置)。
  • 配套关系无法保证:if-else 是"手写"的约定,没有任何机制保证"选了 Windows 就全套都是 Windows 的"。某天有人漏改一个分支,就会出现"Windows 的按钮 + Mac 的对话框"这种不伦不类的组合。
  • 不可测试:想用 Mock 按钮测试业务逻辑?做不到——具体类被写死在代码里。

抽象工厂把"平台选择 + 配套对象创建"整体封装进一个工厂对象,业务代码只面向UIFactory 抽象编程,问题全部消失。

③

核心思想

通俗解释:把"一个产品族"打包成一个整体来创建。客户端手里拿着一张"工厂卡",这张卡要么是"Windows 全家桶卡"、要么是"macOS 全家桶卡",刷卡时不管你要按钮还是对话框,吐出来的都是同一套风格的东西——你永远拿不到"Windows 按钮 + macOS 对话框"的混搭。

生活类比(麦当劳套餐):点套餐时你只对服务员说"来一份 A 套餐",而不必说"来一个香辣鸡腿堡 + 一杯中杯可乐 + 一份薯条"。A 套餐就是抽象工厂,它保证三样东西是配套组合的(都是中份、都是同一系列);具体是哪家店做出来的(哪个具体工厂)你并不关心,反正配方统一。

生活类比(宜家样板间):宜家的每个样板间就是一个产品族——沙发、茶几、台灯、地毯都是一个色系、一个风格的。你照着样板间整套购买(抽象工厂),绝不会出现"北欧风沙发 + 中式红木茶几"的灾难。抽象工厂就是那个"保证整屋风格统一"的样板间。

核心要点拆开看:

  • 产品族(family):位于不同产品等级结构、但属于同一系列的多个产品(WindowsButton 和 WindowsDialog 同属"Windows 族")。
  • 一致性约束由结构保证:具体工厂一次性实现所有创建方法,族内配套关系是"编译期固化"的,不是靠程序员自觉。
  • 客户端只见抽象:只依赖 UIFactory、Button、Dialog 三个抽象,具体类在客户端代码中零出现。
④

UML / 角色关系

抽象工厂有四个角色:

角色职责本例中
AbstractFactory(抽象工厂)声明一组创建产品的抽象方法UIFactory
ConcreteFactory(具体工厂)实现某个产品族的全部创建方法WindowsFactory / MacFactory
AbstractProduct(抽象产品)为每类产品定义公共接口Button / Dialog
ConcreteProduct(具体产品)某产品族中的具体实现WindowsButton / MacDialog 等
┌────────────────┐ 使用  ┌─────────────────┐
│    Client      │──────▶│ AbstractFactory │
│ (buildUI)      │       │ createButton()  │
└────────────────┘       │ createDialog()  │
                         └────────┬────────┘
                      implements   │   implements
              ┌────────────────────┼────────────────────┐
              ▼                    ▼                    ▼
   ┌────────────────────┐  ┌────────────────────┐
   │  WindowsFactory    │  │    MacFactory      │
   │ createButton() ────┼─▶│ createButton() ────┼──┐
   │ createDialog() ────┼─▶│ createDialog() ────┼──┤
   └────────────────────┘  └────────────────────┘  │
              │                    │                │
              ▼                    ▼                ▼
   ┌────────────────────┐  ┌────────────────────┐
   │ WindowsButton      │  │   MacButton        │  │
   │ (implements Button)│  │ (implements Button)│  │
   └────────────────────┘  └────────────────────┘  │
              │                    │                │
              ▼                    ▼                ▼
   ┌────────────────────┐  ┌────────────────────┐
   │ WindowsDialog      │  │    MacDialog       │
   │ (implements Dialog)│  │ (implements Dialog)│
   └────────────────────┘  └────────────────────┘

关键关系:客户端持有的是抽象工厂引用;每新增一个产品族(如 Linux 族),只需新增一个具体工厂 + 一组具体产品,客户端代码一行都不用改——这就是开闭原则的直接体现。

⑤

最小 C++ 示例

完整可编译的 C++17 示例(跨平台 UI 工厂)。所有资源用 std::unique_ptr 管理,无手工 delete、无悬空指针:

#include <iostream>
#include <memory>
#include <string>

// ========== 抽象产品 1:按钮 ==========
class Button {
public:
    virtual ~Button() = default;
    virtual void render() const = 0;
};

// ========== 抽象产品 2:对话框 ==========
class Dialog {
public:
    virtual ~Dialog() = default;
    virtual void show() const = 0;
};

// ========== 具体产品:Windows 族 ==========
class WindowsButton : public Button {
public:
    void render() const override {
        std::cout << "[Windows] 绘制直角灰色按钮\n";
    }
};

class WindowsDialog : public Dialog {
public:
    void show() const override {
        std::cout << "[Windows] 弹出直角边框对话框\n";
    }
};

// ========== 具体产品:macOS 族 ==========
class MacButton : public Button {
public:
    void render() const override {
        std::cout << "[macOS] 绘制圆角彩色按钮\n";
    }
};

class MacDialog : public Dialog {
public:
    void show() const override {
        std::cout << "[macOS] 弹出毛玻璃效果对话框\n";
    }
};

// ========== 抽象工厂:一次生产"一套"相关产品 ==========
class UIFactory {
public:
    virtual ~UIFactory() = default;
    virtual std::unique_ptr<Button> createButton() const = 0;
    virtual std::unique_ptr<Dialog> createDialog() const = 0;
};

// ========== 具体工厂:Windows 产品族 ==========
class WindowsFactory : public UIFactory {
public:
    std::unique_ptr<Button> createButton() const override {
        return std::make_unique<WindowsButton>();  // 族内配套由这里保证
    }
    std::unique_ptr<Dialog> createDialog() const override {
        return std::make_unique<WindowsDialog>();
    }
};

// ========== 具体工厂:macOS 产品族 ==========
class MacFactory : public UIFactory {
public:
    std::unique_ptr<Button> createButton() const override {
        return std::make_unique<MacButton>();
    }
    std::unique_ptr<Dialog> createDialog() const override {
        return std::make_unique<MacDialog>();
    }
};

// ========== 客户端:只依赖抽象,不认识任何具体类 ==========
void buildUI(const UIFactory& factory) {
    auto btn = factory.createButton();
    auto dlg = factory.createDialog();
    btn->render();
    dlg->show();
}

int main() {
    std::cout << "=== 在 Windows 平台构建界面 ===\n";
    WindowsFactory winFactory;
    buildUI(winFactory);          // 传入哪个工厂,就是哪套风格

    std::cout << "\n=== 在 macOS 平台构建界面 ===\n";
    MacFactory macFactory;
    buildUI(macFactory);
    return 0;
}

程序输出:

=== 在 Windows 平台构建界面 ===
[Windows] 绘制直角灰色按钮
[Windows] 弹出直角边框对话框

=== 在 macOS 平台构建界面 ===
[macOS] 绘制圆角彩色按钮
[macOS] 弹出毛玻璃效果对话框

注意:buildUI 的函数签名里只有 UIFactory、Button、Dialog 三个抽象类型——把 main 里的工厂换成 LinuxFactory,整个业务代码零改动即可切换到 Linux 风格。这就是抽象工厂与直接 new 的本质区别。

⑥

Qt 实战示例

我们实现一个主题化样式工厂:深色/浅色两套"产品族",每套包含按钮风格与面板风格两个"产品"。UI 代码只面对 ThemeFactory 抽象,切换主题就是切换工厂。

ThemeFactory.h(完整头文件)

#ifndef THEMEFACTORY_H
#define THEMEFACTORY_H

#include <QString>
#include <memory>

// 抽象产品 1:按钮风格
class ButtonStyle {
public:
    virtual ~ButtonStyle() = default;
    virtual QString background() const = 0;   // 背景色
    virtual QString foreground() const = 0;   // 文字色
};

// 抽象产品 2:面板风格
class PanelStyle {
public:
    virtual ~PanelStyle() = default;
    virtual QString background() const = 0;   // 背景色
    virtual QString border() const = 0;       // 边框色
};

// 抽象工厂:一个工厂生产"一套"互相匹配的风格对象
class ThemeFactory {
public:
    virtual ~ThemeFactory() = default;
    virtual std::unique_ptr<ButtonStyle> makeButtonStyle() const = 0;
    virtual std::unique_ptr<PanelStyle>  makePanelStyle()  const = 0;
};

#endif // THEMEFACTORY_H

ThemeFactory.cpp(实现 + 简单工厂入口)

#include "ThemeFactory.h"

// ---------- 深色主题产品族 ----------
class DarkButtonStyle : public ButtonStyle {
public:
    QString background() const override { return QStringLiteral("#2b2b2b"); }
    QString foreground() const override { return QStringLiteral("#ffffff"); }
};

class DarkPanelStyle : public PanelStyle {
public:
    QString background() const override { return QStringLiteral("#1e1e1e"); }
    QString border()       const override { return QStringLiteral("#444444"); }
};

class DarkThemeFactory : public ThemeFactory {
public:
    std::unique_ptr<ButtonStyle> makeButtonStyle() const override {
        return std::make_unique<DarkButtonStyle>();
    }
    std::unique_ptr<PanelStyle> makePanelStyle() const override {
        return std::make_unique<DarkPanelStyle>();
    }
};

// ---------- 浅色主题产品族 ----------
class LightButtonStyle : public ButtonStyle {
public:
    QString background() const override { return QStringLiteral("#f5f5f5"); }
    QString foreground() const override { return QStringLiteral("#111111"); }
};

class LightPanelStyle : public PanelStyle {
public:
    QString background() const override { return QStringLiteral("#ffffff"); }
    QString border()       const override { return QStringLiteral("#cccccc"); }
};

class LightThemeFactory : public ThemeFactory {
public:
    std::unique_ptr<ButtonStyle> makeButtonStyle() const override {
        return std::make_unique<LightButtonStyle>();
    }
    std::unique_ptr<PanelStyle> makePanelStyle() const override {
        return std::make_unique<LightPanelStyle>();
    }
};

// 工厂选择入口:按名字返回对应产品族工厂(用 unique_ptr 明确所有权)
std::unique_ptr<ThemeFactory> createThemeFactory(const QString& name) {
    if (name == QStringLiteral("dark")) {
        return std::make_unique<DarkThemeFactory>();
    }
    return std::make_unique<LightThemeFactory>();
}

main.cpp

#include <QCoreApplication>
#include <QDebug>
#include "ThemeFactory.h"

// 客户端只依赖抽象工厂与抽象产品,主题切换零改动
static void applyTheme(const ThemeFactory& factory) {
    auto button = factory.makeButtonStyle();
    auto panel  = factory.makePanelStyle();
    qInfo().noquote() << "按钮背景:" << button->background()
                      << "文字颜色:" << button->foreground();
    qInfo().noquote() << "面板背景:" << panel->background()
                      << "边框颜色:" << panel->border();
}

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    qInfo() << "=== 深色主题 ===";
    auto dark = createThemeFactory(QStringLiteral("dark"));
    applyTheme(*dark);

    qInfo() << "=== 浅色主题 ===";
    auto light = createThemeFactory(QStringLiteral("light"));
    applyTheme(*light);

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(ThemeFactoryDemo VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

find_package(Qt6 REQUIRED COMPONENTS Core)

add_executable(theme_factory_demo
    main.cpp
    ThemeFactory.cpp
    ThemeFactory.h
)

target_link_libraries(theme_factory_demo PRIVATE Qt6::Core)

程序输出:

=== 深色主题 ===
按钮背景: "#2b2b2b" 文字颜色: "#ffffff"
面板背景: "#1e1e1e" 边框颜色: "#444444"
=== 浅色主题 ===
按钮背景: "#f5f5f5" 文字颜色: "#111111"
面板背景: "#ffffff" 边框颜色: "#cccccc"
真实工程价值:主题工厂返回的颜色值可以直接灌进 QSS(setStyleSheet)或 QPalette,UI 层完全感知不到"深色/浅色"的具体实现;将来新增"高对比度主题",只加一个工厂 + 两个产品类,所有界面自动获得新主题能力。
⑦

代码执行流程

以最小 C++ 示例为例,程序执行过程分四步:

  1. 选择工厂:main 创建 WindowsFactory(或在 Qt 例中通过 createThemeFactory("dark") 拿到深色工厂)。此时客户端手里只有抽象引用 const UIFactory&,多态绑定到具体工厂。
  2. 按需创建产品:buildUI 依次调用 factory.createButton()、factory.createDialog()。虚函数分派到 WindowsFactory 的实现,用 make_unique 在堆上构造具体产品,返回 unique_ptr<Button> / unique_ptr<Dialog>(向上转型为抽象产品)。
  3. 业务使用:客户端通过抽象接口调用 render() / show(),运行时多态输出 Windows 风格文本。整个过程中客户端从未出现 WindowsButton 字样,也从不负责释放内存——RAII 的 unique_ptr 在离开作用域时自动回收。
  4. 切换产品族:main 换用 MacFactory,重复步骤 2-3,输出整套 macOS 风格;配套一致性由具体工厂的固定实现保证,不存在"混搭"可能。

Qt 示例的执行流程完全相同,只是入口换成了 createThemeFactory(name) 这个"工厂选择器"(它本身是一个简单工厂/工厂方法,返回的却是抽象工厂——两层工厂协作是工程中的常见组合)。

⑧

为什么这样设计

抽象工厂的设计目标,是把"创建逻辑"与"使用逻辑"彻底分离,并让一致性约束从"程序员的自觉"变成"结构上的必然":

  • 解耦点:客户端 ↔ 具体产品。客户端只认识 UIFactory/Button/Dialog 三个抽象,具体类只出现在工厂内部。改产品实现、换产品族,客户端零感知。
  • OCP(开闭原则):新增产品族 = 新增一个具体工厂 + 一组具体产品,不改动任何已有代码;新增产品类型(如文本框)需要给所有工厂加方法,属于"扩展点"设计,但产品族扩展的频率远低于产品类型扩展——这正是抽象工厂的适用前提。
  • DIP(依赖倒置):高层业务模块依赖抽象工厂与抽象产品,不再依赖任何具体类;具体工厂反过来实现抽象接口,依赖关系全部指向抽象层。
  • 组合优于继承:工厂之间是平级组合关系而非继承链,产品族切换本质上是"替换一个组件",符合"组合优于继承"的指导思想。
  • 可测试性:测试中传入 MockFactory(返回假按钮、假对话框),业务逻辑即可在无 UI 环境下单测;生产环境换真实工厂,测试代码零修改。
  • 一致性由构造保证:具体工厂把整套创建方法写在一个类里,从根源上杜绝"不同系列产品混搭"。
⑨

不使用会怎样

回到第②节的坏代码,想象系统持续演进会发生什么:

  • if-else 膨胀:平台从 2 个涨到 5 个(Windows/macOS/Linux/iOS/Android),产品从 2 种涨到 6 种(按钮/对话框/菜单/滚动条/输入框/标签)。每个"创建点"要写 5×6=30 个分支,而创建点散布在十几个业务函数里——总计数百个分支,改一个平台名要全局搜索替换,漏一个就出"混搭 bug"。
  • 耦合爆炸:业务层直接 new 具体类,意味着业务模块依赖全部具体产品类。任何产品的构造函数签名变化(如 WindowsButton(int style)),所有调用点全部编译失败,牵一发动全身。
  • 一致性失控:没有结构约束,"Windows 按钮 + Mac 对话框"的组合随时可能出现;这类 bug 不会编译报错、不会崩溃,只在用户界面上表现为"风格不统一",极难定位。
  • 测试瘫痪:业务代码写死具体类,无法注入 Mock;想测试"切换平台后界面逻辑"必须真的跑在对应平台上。
  • 类爆炸(反面教材的另一面):有人为了消灭 if-else 给每种产品单独建工厂(工厂方法过度使用),结果工厂类比产品类还多,管理成本反而上升——这正是抽象工厂存在的意义:一个工厂管一组产品,工厂数量 = 产品族数量。
⑩

何时使用

适合使用的场景(3-5 个):

  • 系统需要支持多个产品族,且同一族内的产品必须配套使用(跨平台 UI 控件、主题皮肤、数据库驱动、通讯协议族)。
  • 系统将来可能增加新的产品族,且希望新增时不影响既有代码(OCP 扩展点明确)。
  • 产品族的一致性约束是硬性需求,需要用结构而非约定来保证(如"选深色主题则所有控件都深色")。
  • 你想隐藏产品创建细节,让客户端只面对抽象接口编程,同时方便测试注入 Mock 工厂。
  • Qt 中做可切换主题/平台风格的应用(QSS 主题包、自定义 style 插件)——每个主题就是一个产品族。

不适合使用的场景(2-4 个):

  • 系统只有一个产品族、或产品之间没有配套关系——工厂方法模式就够用,抽象工厂是"杀鸡用牛刀"。
  • 产品类型频繁新增(每加一种产品就要改所有具体工厂,违反 OCP)——此时应优先考虑注册表 + 工厂方法,或干脆用直接构造。
  • 创建逻辑极简单(如只有一个构造函数调用),引入工厂层反而增加间接性,不值得。
  • 产品对象生命周期简单、无族约束、调用方也需要具体类型信息——直接 new 更清晰。
过度设计提醒:抽象工厂是"结构最重"的创建型模式之一。判断标准很简单——你的系统里是否存在"必须成对/成套出现"的对象?如果没有,抽象工厂大概率是过度设计,老老实实用工厂方法或直接 new。
⑪

与其他模式的区别

对比模式区别一句话记忆
工厂方法(Factory Method) 工厂方法只创建一个产品(一个产品等级结构),通过子类重写创建方法来变化;抽象工厂创建一整个产品族(多个产品等级结构),通过替换具体工厂来整体切换。实践中抽象工厂的每个创建方法内部常常就是用工厂方法实现的。 工厂方法管"一个";抽象工厂管"一窝"。
建造者(Builder) Builder 关注同一个复杂对象的逐步构建(同输入不同表示,如 XML/JSON 两种输出);抽象工厂关注多个相关对象的整体创建(一步到位,成套生产)。Builder 的产物是"一个东西的多种形态",抽象工厂的产物是"一堆配套的东西"。 Builder 造"一个复杂的";抽象工厂产"一堆配套的"。
原型(Prototype) Prototype 通过克隆已有对象来创建新对象(复制自身);抽象工厂通过工厂方法构造全新对象。Prototype 适合创建成本高、状态初始化复杂的对象。 Prototype 是"复印机";抽象工厂是"生产线"。
⑫

Qt 源码中的体现

Qt 是抽象工厂的"重度用户",最典型的三个实例:

  • QPlatformTheme(QPA 平台抽象层):这是抽象工厂的教科书实现。每个平台插件(Windows/macOS/XCB 等)提供一个 QPlatformTheme 子类,通过 createDialog(QPlatformDialogHelper::Type)、createMenuBar()、createSystemTrayIcon() 等一整套方法,生产该平台"配套"的原生 UI 组件。业务代码只面对 QPlatformTheme 抽象——这就是为什么同样的 QFileDialog 在 Windows 上是原生对话框、在 Linux 上是自绘对话框,而应用代码一行不用改。
  • QStyleFactory + QStyle:QStyleFactory::create(name) 是工厂方法,但各平台 QStyle 子类(Fusion、Windows、macOS 风格)各自负责"一整套控件"的绘制(按钮、滚动条、菜单……所有控件),从"产品族"角度理解:一种风格 = 一个产品族,风格切换 = 工厂切换。
  • Qt Quick Controls 2 的 Style:Material / Fusion / Imagine / Universal 四种 Style 各自提供完整的控件主题族,应用通过 QT_QUICK_CONTROLS_STYLE 环境变量或 QQuickStyle 切换——底层正是"主题族工厂"的思想。

此外 QSqlDatabase 的驱动层(QSQLITE/QMYSQL/QPSQL)可理解为"注册表 + 工厂方法"风格的变体,它是按单个驱动创建,属于工厂方法而非严格抽象工厂。在 Qt 里写插件(QPluginLoader)时,一个插件暴露的 QObject 通常也承担"抽象工厂"角色:通过一个入口对象创建该插件的整套服务。

⑬

面试常见问题

Q1:抽象工厂和工厂方法的本质区别是什么?

A:工厂方法针对一个产品等级结构(一个抽象产品),通过继承子类决定创建哪个具体产品;抽象工厂针对多个产品等级结构组成的产品族,通过组合一个具体工厂对象决定整套产品。工厂方法用"继承"变化,抽象工厂用"组合/替换工厂对象"变化。另外,抽象工厂的每个创建方法内部通常就是用工厂方法实现的。

Q2:抽象工厂违背开闭原则吗?新增产品类型(不是产品族)时怎么办?

A:对"产品族扩展"开放(新增族不改旧代码);但对"产品类型扩展"是闭合的——新增一种产品要在所有具体工厂中加方法,违背 OCP。这是抽象工厂的固有代价,业界通称"产品族与产品类型的二选一"。缓解手段:产品类型稳定时用抽象工厂;产品类型易变时改用注册表 + 工厂方法,或引入 template<typename T> 泛型工厂。

Q3:如何保证"族内产品配套"不被破坏?

A:靠结构保证而非约定保证:所有创建方法集中在同一个具体工厂类中,客户端只能通过该工厂获取产品,不存在"单独 new 某个产品"的合法路径;配合抽象产品接口,编译器即可阻止跨族混搭。若需要更强的运行时约束,可在工厂构造时记录族标识并在创建时校验。

Q4:抽象工厂在依赖注入(DI)中如何应用?

A:抽象工厂本身常作为"抽象服务"注册进 DI 容器(如 registerFactory<ThemeFactory>(name)),具体工厂作为其实现绑定;业务代码通过接口注入拿到工厂,切换产品族 = 切换绑定的实现,与 Spring / QML 插件加载等机制天然契合。在 Qt 中,插件接口经常就是一个抽象工厂,QPluginLoader 负责把具体工厂实例化后交给应用。

Q5:C++ 中抽象工厂的返回类型应该怎么写(裸指针 / unique_ptr / shared_ptr)?

A:默认返回 std::unique_ptr<AbstractProduct>,语义清晰、零额外开销、所有权明确;若产品需被多方共享则返回 std::shared_ptr;裸指针只在产品由工厂长期持有(所有权归工厂)时使用,且必须文档化。绝不返回 new 裸指针让调用方 delete——这正是 RAII 与智能指针规范的核心。

⑭

今日练习

需求(建议 15-30 分钟):为一个"消息推送系统"设计抽象工厂。系统需要向三种渠道推送:邮件(Email)、短信(SMS)、站内信(InApp)。每条消息都由两部分组成:消息内容(Message)与发送器(Sender)——每种渠道的 Message 有自己专属的格式(邮件要有主题行、短信要截断到 70 字符、站内信要带跳转链接),Sender 负责实际发送。请用 C++17 实现:

  1. 定义抽象产品 Message 与 Sender,以及抽象工厂 ChannelFactory(含 createMessage() 与 createSender())。
  2. 实现三个具体工厂(EmailFactory / SmsFactory / InAppFactory)及各自的产品。
  3. 客户端 push(ChannelFactory&) 只依赖抽象,模拟发送一条消息。
  4. 扩展:新增"企业微信(WeCom)"渠道,验证既有代码是否零修改。

提示:

  • 提示 1:注意"内容格式"属于 Message 的职责(多态),不要用 if-else 判断渠道类型来拼格式。
  • 提示 2:createMessage() 可以接收业务参数(收件人、正文),把"构建细节"留在工厂内部;思考哪些参数该进工厂方法、哪些该进产品构造函数。
  • 提示 3(进阶):试着让 push() 接收 std::function<std::unique_ptr<ChannelFactory>()> 工厂选择器,体会"工厂的工厂"如何让客户端完全不知道任何具体类名。
⑮

今日总结

一句话记忆:抽象工厂 = "产品族套餐卡"——一张工厂卡,成套生产配套对象,客户端只刷卡不点菜。

代码特征信号(看到这些,考虑抽象工厂):

  • 代码中出现多组"成对/成套出现"的对象,且不同套之间风格不同(Windows 按钮 + Windows 对话框……)。
  • 客户端代码里反复出现 if (platform == ...) / if (theme == ...) 之类的分支来创建对象。
  • 一个接口里同时声明了多个 createXxx() / makeXxx() 方法,且这些方法的产品彼此关联。
  • 新增一套产品时,要改动的创建点不止一处。
  • 团队需要用"结构约束"保证产品配套,而不是靠 code review 盯出来的约定。

明日预告:Day 4 将学习 建造者模式(Builder)——当对象的构造参数多到令人窒息(几十个参数、一堆可选配置),抽象工厂也无能为力时,Builder 如何用"分步构建 + 链式调用"把复杂构造拆解成清晰、可控、可复用的步骤。这是 Qt 里 QMessageBox 静态方法、QML 属性绑定背后共同的优雅思想。