把"一套配套对象"的组装交给一个工厂——不再创建单个对象,而是创建一整族相互匹配的对象。
抽象工厂模式(Abstract Factory):提供一个创建一系列相关或相互依赖对象(即"产品族")的接口,而无需指定它们的具体类。
如果说工厂方法模式解决的是"一个产品"的创建问题,那么抽象工厂解决的是"一整组配套产品"的创建问题——它把工厂方法从"一个类一个工厂"升级为"一个产品族一个工厂"。
先看一段"没有模式"的坏代码。假设我们要做一个跨平台 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();
}
这段代码的问题非常典型:
抽象工厂把"平台选择 + 配套对象创建"整体封装进一个工厂对象,业务代码只面向UIFactory 抽象编程,问题全部消失。
通俗解释:把"一个产品族"打包成一个整体来创建。客户端手里拿着一张"工厂卡",这张卡要么是"Windows 全家桶卡"、要么是"macOS 全家桶卡",刷卡时不管你要按钮还是对话框,吐出来的都是同一套风格的东西——你永远拿不到"Windows 按钮 + macOS 对话框"的混搭。
生活类比(麦当劳套餐):点套餐时你只对服务员说"来一份 A 套餐",而不必说"来一个香辣鸡腿堡 + 一杯中杯可乐 + 一份薯条"。A 套餐就是抽象工厂,它保证三样东西是配套组合的(都是中份、都是同一系列);具体是哪家店做出来的(哪个具体工厂)你并不关心,反正配方统一。
生活类比(宜家样板间):宜家的每个样板间就是一个产品族——沙发、茶几、台灯、地毯都是一个色系、一个风格的。你照着样板间整套购买(抽象工厂),绝不会出现"北欧风沙发 + 中式红木茶几"的灾难。抽象工厂就是那个"保证整屋风格统一"的样板间。
核心要点拆开看:
UIFactory、Button、Dialog 三个抽象,具体类在客户端代码中零出现。抽象工厂有四个角色:
| 角色 | 职责 | 本例中 |
|---|---|---|
| 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++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 的本质区别。
我们实现一个主题化样式工厂:深色/浅色两套"产品族",每套包含按钮风格与面板风格两个"产品"。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"
setStyleSheet)或 QPalette,UI 层完全感知不到"深色/浅色"的具体实现;将来新增"高对比度主题",只加一个工厂 + 两个产品类,所有界面自动获得新主题能力。
以最小 C++ 示例为例,程序执行过程分四步:
main 创建 WindowsFactory(或在 Qt 例中通过 createThemeFactory("dark") 拿到深色工厂)。此时客户端手里只有抽象引用 const UIFactory&,多态绑定到具体工厂。buildUI 依次调用 factory.createButton()、factory.createDialog()。虚函数分派到 WindowsFactory 的实现,用 make_unique 在堆上构造具体产品,返回 unique_ptr<Button> / unique_ptr<Dialog>(向上转型为抽象产品)。render() / show(),运行时多态输出 Windows 风格文本。整个过程中客户端从未出现 WindowsButton 字样,也从不负责释放内存——RAII 的 unique_ptr 在离开作用域时自动回收。main 换用 MacFactory,重复步骤 2-3,输出整套 macOS 风格;配套一致性由具体工厂的固定实现保证,不存在"混搭"可能。Qt 示例的执行流程完全相同,只是入口换成了 createThemeFactory(name) 这个"工厂选择器"(它本身是一个简单工厂/工厂方法,返回的却是抽象工厂——两层工厂协作是工程中的常见组合)。
抽象工厂的设计目标,是把"创建逻辑"与"使用逻辑"彻底分离,并让一致性约束从"程序员的自觉"变成"结构上的必然":
UIFactory/Button/Dialog 三个抽象,具体类只出现在工厂内部。改产品实现、换产品族,客户端零感知。MockFactory(返回假按钮、假对话框),业务逻辑即可在无 UI 环境下单测;生产环境换真实工厂,测试代码零修改。回到第②节的坏代码,想象系统持续演进会发生什么:
WindowsButton(int style)),所有调用点全部编译失败,牵一发动全身。适合使用的场景(3-5 个):
不适合使用的场景(2-4 个):
| 对比模式 | 区别 | 一句话记忆 |
|---|---|---|
| 工厂方法(Factory Method) | 工厂方法只创建一个产品(一个产品等级结构),通过子类重写创建方法来变化;抽象工厂创建一整个产品族(多个产品等级结构),通过替换具体工厂来整体切换。实践中抽象工厂的每个创建方法内部常常就是用工厂方法实现的。 | 工厂方法管"一个";抽象工厂管"一窝"。 |
| 建造者(Builder) | Builder 关注同一个复杂对象的逐步构建(同输入不同表示,如 XML/JSON 两种输出);抽象工厂关注多个相关对象的整体创建(一步到位,成套生产)。Builder 的产物是"一个东西的多种形态",抽象工厂的产物是"一堆配套的东西"。 | Builder 造"一个复杂的";抽象工厂产"一堆配套的"。 |
| 原型(Prototype) | Prototype 通过克隆已有对象来创建新对象(复制自身);抽象工厂通过工厂方法构造全新对象。Prototype 适合创建成本高、状态初始化复杂的对象。 | Prototype 是"复印机";抽象工厂是"生产线"。 |
Qt 是抽象工厂的"重度用户",最典型的三个实例:
QPlatformTheme 子类,通过 createDialog(QPlatformDialogHelper::Type)、createMenuBar()、createSystemTrayIcon() 等一整套方法,生产该平台"配套"的原生 UI 组件。业务代码只面对 QPlatformTheme 抽象——这就是为什么同样的 QFileDialog 在 Windows 上是原生对话框、在 Linux 上是自绘对话框,而应用代码一行不用改。QStyleFactory::create(name) 是工厂方法,但各平台 QStyle 子类(Fusion、Windows、macOS 风格)各自负责"一整套控件"的绘制(按钮、滚动条、菜单……所有控件),从"产品族"角度理解:一种风格 = 一个产品族,风格切换 = 工厂切换。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 实现:
Message 与 Sender,以及抽象工厂 ChannelFactory(含 createMessage() 与 createSender())。push(ChannelFactory&) 只依赖抽象,模拟发送一条消息。提示:
createMessage() 可以接收业务参数(收件人、正文),把"构建细节"留在工厂内部;思考哪些参数该进工厂方法、哪些该进产品构造函数。push() 接收 std::function<std::unique_ptr<ChannelFactory>()> 工厂选择器,体会"工厂的工厂"如何让客户端完全不知道任何具体类名。