【设计模式精讲】6.抽象工厂(Abstract Factory)

【设计模式精讲】6.抽象工厂(Abstract Factory)

【摘要】:跨平台 UI 里混进一个别家风格的控件、家具店里现代沙发配了维多利亚椅子------这类「产品不配套」的 bug,根源不是某个对象造错了,而是各处散造的对象彼此不知情。本文从一次控件混搭事故讲起,给出抽象工厂的意图与结构:声明一组创建 一族 相关产品的方法,由具体工厂保证成套。C++ 实现按 Refactoring Guru 的跨平台 UI 示例展开,传统写法与智能指针版对照;进阶讨论「增族易、增品难」的经典代价及注册表缓解。文末对比 POCO 数据库连接族与 AOSP HAL 模块族的真实形态,并辨析它与工厂方法的一线之隔。
【关键词】:抽象工厂、产品族、具体工厂、开闭原则、跨平台

1. 一套界面里混进了别家的控件

渲染引擎支持多套主题,各控件单独创建:

cpp 复制代码
// 说明性片段(省略各控件类的定义)
// ❌ 每个控件各自造,风格可能不配套
void renderUi() {
  Button* b = new WinButton();     // Windows 按钮
  Menu* m = new MacMenu();         // ❌ Mac 菜单!
  ScrollBar* s = new WinScrollBar();
  b->paint(); m->paint(); s->paint();
}

这段代码编译运行都正常,测试却偶现「某台机器上菜单画风突变」。原因是 创建逻辑散落各处WinButtonMacMenu 的组合没有任何机制阻止,全靠人肉记住「这批代码用哪个变体」。随着产品从三种涨到十种、变体从两套涨到五套,每一处 new 都是混搭事故的候选。第 5 篇的工厂方法能管住「一种产品怎么造」,但十个工厂方法彼此之间仍然互不知情------需要有人为一整族产品的配套负责,这正是抽象工厂登场的位置。

2. 模式意图与定义

  • 一句话定义 (GoF 原文意图):Provide an interface for creating families of related or dependent objects without specifying their concrete classes ------为创建一族相关或依赖的对象提供一个接口,且无需指定它们的具体类。GoF 给它的别名是 Kit(套件):一个具体工厂就是一「套」配套产品。Refactoring Guru 的表述同样直接:抽象工厂能创建一系列相关的对象,而无需指定其具体类。
  • 解决的问题 :RG 的家具店例子最传神------椅子、沙发、咖啡桌是一 系列产品 ,现代、维多利亚是 变体;顾客不能收到现代沙发配维多利亚椅子。抽象工厂让「同一工厂造出的产品必然配套」,且新增变体(再开一种风格)无需修改客户端代码。

与工厂方法的关系一句话:工厂方法造「一个」,抽象工厂造「一族」 ;RG 进一步点破:抽象工厂通常就是基于一组工厂方法实现的------把第 5 篇的多个 createXxx() 打包进同一个接口,就得到了抽象工厂。GoF 的动机也一致:UI 工具包要跨 Motif / PM 多种外观,控件不应在各处硬编码具体类。

3. UML 图 + 结构说明

#mermaid-svg-dQKO3Iwe8miRYccL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dQKO3Iwe8miRYccL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dQKO3Iwe8miRYccL .error-icon{fill:#552222;}#mermaid-svg-dQKO3Iwe8miRYccL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dQKO3Iwe8miRYccL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dQKO3Iwe8miRYccL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dQKO3Iwe8miRYccL .marker.cross{stroke:#333333;}#mermaid-svg-dQKO3Iwe8miRYccL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dQKO3Iwe8miRYccL p{margin:0;}#mermaid-svg-dQKO3Iwe8miRYccL g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-dQKO3Iwe8miRYccL g.classGroup text .title{font-weight:bolder;}#mermaid-svg-dQKO3Iwe8miRYccL .cluster-label text{fill:#333;}#mermaid-svg-dQKO3Iwe8miRYccL .cluster-label span{color:#333;}#mermaid-svg-dQKO3Iwe8miRYccL .cluster-label span p{background-color:transparent;}#mermaid-svg-dQKO3Iwe8miRYccL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dQKO3Iwe8miRYccL .cluster text{fill:#333;}#mermaid-svg-dQKO3Iwe8miRYccL .cluster span{color:#333;}#mermaid-svg-dQKO3Iwe8miRYccL .nodeLabel,#mermaid-svg-dQKO3Iwe8miRYccL .edgeLabel{color:#131300;}#mermaid-svg-dQKO3Iwe8miRYccL .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-dQKO3Iwe8miRYccL .label text{fill:#131300;}#mermaid-svg-dQKO3Iwe8miRYccL .labelBkg{background:#ECECFF;}#mermaid-svg-dQKO3Iwe8miRYccL .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-dQKO3Iwe8miRYccL .classTitle{font-weight:bolder;}#mermaid-svg-dQKO3Iwe8miRYccL .node rect,#mermaid-svg-dQKO3Iwe8miRYccL .node circle,#mermaid-svg-dQKO3Iwe8miRYccL .node ellipse,#mermaid-svg-dQKO3Iwe8miRYccL .node polygon,#mermaid-svg-dQKO3Iwe8miRYccL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dQKO3Iwe8miRYccL .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL g.clickable{cursor:pointer;}#mermaid-svg-dQKO3Iwe8miRYccL g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-dQKO3Iwe8miRYccL g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-dQKO3Iwe8miRYccL .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-dQKO3Iwe8miRYccL .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-dQKO3Iwe8miRYccL .dashed-line{stroke-dasharray:3;}#mermaid-svg-dQKO3Iwe8miRYccL .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-dQKO3Iwe8miRYccL #compositionStart,#mermaid-svg-dQKO3Iwe8miRYccL .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #compositionEnd,#mermaid-svg-dQKO3Iwe8miRYccL .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #dependencyStart,#mermaid-svg-dQKO3Iwe8miRYccL .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #dependencyStart,#mermaid-svg-dQKO3Iwe8miRYccL .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #extensionStart,#mermaid-svg-dQKO3Iwe8miRYccL .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #extensionEnd,#mermaid-svg-dQKO3Iwe8miRYccL .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #aggregationStart,#mermaid-svg-dQKO3Iwe8miRYccL .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #aggregationEnd,#mermaid-svg-dQKO3Iwe8miRYccL .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #lollipopStart,#mermaid-svg-dQKO3Iwe8miRYccL .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL #lollipopEnd,#mermaid-svg-dQKO3Iwe8miRYccL .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dQKO3Iwe8miRYccL .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-dQKO3Iwe8miRYccL .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dQKO3Iwe8miRYccL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dQKO3Iwe8miRYccL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dQKO3Iwe8miRYccL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 造族
<<interface>>
GUIFactory
+createButton() : Button*
+createMenu() : Menu*
WinFactory
+createButton() : WinButton
+createMenu() : WinMenu
MacFactory
+createButton() : MacButton
+createMenu() : MacMenu
<<interface>>
Button
+paint() : void
<<interface>>
Menu
+paint() : void
WinButton
MacButton
WinMenu
MacMenu

RG 拆出的五个参与者,比工厂方法多出「族」的维度:

  • 抽象产品(Abstract Product) :族中每个产品各声明一个接口(ButtonMenu)------注意是 每族成员一个接口,不是一个总接口;
  • 具体产品(Concrete Product) :各变体实现对应抽象产品(WinButtonMacMenu);
  • 抽象工厂(Abstract Factory) :声明 一组 创建方法,覆盖族内所有产品,返回类型一律是抽象产品;
  • 具体工厂(Concrete Factory) :每个变体一个工厂,只造本变体的产品(WinFactory 只出 Win*)------配套性由这一条硬性约束保证;
  • 客户端(Client) :只持有 GUIFactory* 与抽象产品指针,具体类名一个不出现;具体工厂在程序初始化阶段按配置/环境选好,注入给所有需要的代码。

4. 传统 C++ 写法(C++11 之前)

按 GoF 时代的形态实现,示例改编自 RG 的跨平台 UI:Application 只认抽象工厂与抽象产品,初始化代码按环境选定具体工厂。裸指针交货、禁拷贝靠私有声明:

cpp 复制代码
// C++98/03 写法
#include <iostream>
#include <stdexcept>
#include <string>

// ---- 抽象产品:族内每种产品一个接口 ----
class Button {
public:
  virtual ~Button() {}
  virtual void paint() = 0;
};

class Menu {
public:
  virtual ~Menu() {}
  virtual void paint() = 0;
};

// ---- 具体产品:Win / Mac 两个变体 ----
class WinButton : public Button {
public:
  void paint() { std::cout << "Win 按钮\n"; }
};
class WinMenu : public Menu {
public:
  void paint() { std::cout << "Win 菜单\n"; }
};
class MacButton : public Button {
public:
  void paint() { std::cout << "Mac 按钮\n"; }
};
class MacMenu : public Menu {
public:
  void paint() { std::cout << "Mac 菜单\n"; }
};

// ---- 抽象工厂:一族创建方法 ----
class GUIFactory {
public:
  virtual ~GUIFactory() {}
  virtual Button* createButton() = 0;
  virtual Menu* createMenu() = 0;
};

// ---- 具体工厂:每个变体一个,只造本族 ----
class WinFactory : public GUIFactory {
public:
  virtual Button* createButton() {
    return new WinButton();
  }
  virtual Menu* createMenu() {
    return new WinMenu();
  }
};

class MacFactory : public GUIFactory {
public:
  virtual Button* createButton() {
    return new MacButton();
  }
  virtual Menu* createMenu() {
    return new MacMenu();
  }
};

// ---- 客户端:只依赖抽象 ----
class Application {
public:
  explicit Application(GUIFactory* f) : m_f(f) {}
  ~Application() { delete m_f; }

  void createUi() {
    // 同一工厂出活,必然配套
    Button* b = m_f->createButton();
    Menu* m = m_f->createMenu();
    b->paint();
    m->paint();
    delete b;
    delete m;
  }

private:
  Application(const Application&);
  Application& operator=(const Application&);

  GUIFactory* m_f;
};

// ---- 初始化代码:唯一出现具体类名的地方 ----
GUIFactory* makeFactory(const std::string& os) {
  if (os == "Windows") return new WinFactory();
  if (os == "Mac") return new MacFactory();
  throw std::runtime_error("未知环境: " + os);
}

对照第 1 节的坏味道:混搭在结构上已不可能------WinFactory 根本造不出 MacMenu。新增第三套主题(如 LinuxFactory)时,写一个工厂加四个产品类,ApplicationmakeFactory 之外的代码零改动,这是 RG 列出的开闭原则收益;代价也如 RG 所说:接口和类一下子多出许多,代码比之前复杂。

5. 现代 C++ 进阶写法

升级一:智能指针交货 。C++11 起创建方法返回 std::unique_ptr<Button>,客户端的 delete 与「谁拥有」问题一并消失;工厂本身也可由 make_unique 造出:

cpp 复制代码
// 节选(产品定义同第 4 节,略)
#include <memory>

class ModernGUIFactory {
public:
  virtual ~ModernGUIFactory() = default;
  virtual std::unique_ptr<Button> createButton()
      = 0;
  virtual std::unique_ptr<Menu> createMenu() = 0;
};

class ModernWinFactory
    : public ModernGUIFactory {
public:
  std::unique_ptr<Button> createButton()
      override {
    return std::make_unique<WinButton>();
  }
  std::unique_ptr<Menu> createMenu() override {
    return std::make_unique<WinMenu>();
  }
};

// 客户端注入也用值语义:
// std::unique_ptr<GUIFactory> f =
//     std::make_unique<WinFactory>();
// Application app(std::move(f));

升级二:直面「增品难」的经典痛点 。抽象工厂的开闭性是 单维度 的:加变体(新族)容易,加产品(族里添 ScrollBar 接口)麻烦------抽象工厂接口一改,所有具体工厂都得跟着改,GoF 明言这是该模式的主要缺陷。现代缓解有二:其一,GoF 建议的「参数化创建方法」,用 create(kind) 一个方法覆盖多品,C++ 里可以用枚举 + switch 实现(第 5 篇参数化工厂方法的族级版本);其二,工厂注册表------把「变体名 → 工厂」存进一张表,配合第 5 篇的自注册手法,新增变体从「改初始化代码」降为「注册一行」。再往工程里走一步,注册表甚至可以按配置文件在启动时装配,主题切换就成了纯数据变更,连重新编译都省了。

升级三:模板与 concept 替代继承 。若变体在 编译期 即可确定(按平台条件编译),可用模板参数传工厂(Application<WinFactory>),虚函数开销与 vtable 依赖一起消失;C++20 的 concept 可约束「提供 createButton/createMenu 的类型」,鸭子类型的工厂既保模板灵活性又有接口可读性。代价是模板代码膨胀、变体不能运行时切换,按需选用。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单 + RG):同族产品必然配套 (工厂封装了成套约束);隔离具体类 ------产品具体类名只出现在具体工厂内部,客户端只操纵抽象接口;换族容易------具体工厂的类名在整个程序里只出现一次(初始化处),换一套产品只改一行;产品创建代码集中一处(单一职责);新增变体不改客户端(开闭原则)。
  • ❌ 缺点:增品难------族内新增一种产品,要改抽象工厂接口和全部具体工厂(GoF 称之为支持新类产品的困难);接口与类数量暴涨,两三种产品、单一变体的场景纯属过度设计;运行时多一层间接,跨动态库边界传工厂要留意 ABI。
  • 🎯 适用场景:产品 天然成族 且变体需要整体切换------跨平台 UI 控件族、多数据库后端、多渲染后端(OpenGL/Vulkan);变体数量预计增长(新主题、新平台)。只有一种变体时,抽象工厂退化为一个简单工厂即可,别急着上。

再补两点工程视角。可测试性 是抽象工厂被低估的红利:测试里实现一个 MockFactory,返回 Stub 控件,UI 逻辑就能脱离真实平台验证------因为产品具体类名从未进入被测代码,替换点天然存在。演化路径 则常被搞反:不要一上来就抽五六个产品的大家族,先按第 5 篇用工厂方法各管各的,等「配套」真的成为约束(出现第一起混搭 bug、或第二个变体上线)再合并成族------RG 观察到的「先工厂方法、后抽象工厂」正是多数团队的自然演化顺序,逆着来往往收获一堆只有单一实现的接口;等真的合并时,也只合并「确实总是配套出现」的那几个产品,靠想象扩出的空位迟早变成负担。

〔辨析〕抽象工厂 vs 工厂方法(第 5 篇):结构上前者常由后者组装而成,但 关注点不同 ------工厂方法强调「让子类决定造什么」,重心在创建的 延迟 ;抽象工厂强调「一族配套」,重心在产品的 组合约束 。判断口诀:产品之间要不要「配套」?不用配套找工厂方法,要配套上抽象工厂。与建造者(第 7 篇):抽象工厂 立刻 返回成品族,建造者 分步 攒一个复杂对象。

7. 开源项目中的身影

POCO:数据库后端就是「一族产品」 。POCO 的 Data 子库设计堪称抽象工厂的教科书应用:SessionFactory 按连接串中的后端名("SQLite""MySQL")向已注册的 Connector 要一个 Session,而后 Session 之下产出的 StatementImpl、会话参数等 整套对象都出自同一后端------你不可能用着 SQLite 会话却拿到 MySQL 的语句实现,「配套」由工厂链在结构上保证:

cpp 复制代码
// 说明性片段(依赖 POCO.Data,示意)
#include "Poco/Data/Session.h"

using namespace Poco::Data;

// 注册过 SQLite/MySQL 两个 Connector 后:
Session s1(SessionFactory::instance().create(
    "SQLite", "demo.db"));   // SQLite 一族
Session s2(SessionFactory::instance().create(
    "MySQL", "host=localhost"));  // MySQL 族

点评:注意它没有让 Session 直接 new 各类后端对象,而是经 ConnectorSessionImpl → 具体实现逐层委托------每一层都只依赖下一层的抽象,「具体类名只出现一次」被贯彻得很彻底。

AOSP:HAL 模块造出的设备族 。Android 早期的硬件抽象层(HAL)用 C 风格实现了同样的思想:hw_get_module("camera", &module) 拿到某个厂商的模块对象,之后由该模块的 open() 生产出配套的一族设备/操作对象。模块即「具体工厂」,hw_module_t/hw_device_t 结构体里的函数指针扮演虚函数的角色------同一个模块产出的设备族必然来自同一厂商实现,不会出现相机设备配错图像处理器的组合。这也印证了 GoF 的判断:抽象工厂不依赖面向对象语法,它是一种「成套创建」的约束结构,C 结构体照样表达。

标准库里的远亲std::pmr::memory_resource 家族(C++17)------一个 memory_resource 产出成套的 polymorphic_allocator,不同资源(monotonic、pool)整套替换,容器代码对具体类型一无所知。GoF 当年点名的 Known Uses 是 InterViews 的 WidgetKit/DialogKit(「Kit」别名的出处)与 ET++;四十年过去,「按套件换肤」的需求没变,抽象工厂也依然是标准答案。

本篇小结

当「产品配套」成为硬约束,把创建权从散落各处的 new 收拢到一个按族生产的工厂:抽象工厂用「一族创建方法 + 每变体一个具体工厂」的结构,让混搭错误从运行时偶发变成编译期不可能。代价是接口膨胀与「增品难」,两三种单品别碰它,变体不会增长也别碰它。选型口诀:单品找工厂方法,成族换肤找抽象工厂,分步攒大件找下一讲的建造者。

本文模式定义、角色划分与家具/UI 示例参考了 Refactoring Guru《设计模式》中文版「抽象工厂」一章,意图译文、后果清单与 Kit 别名参考了 GoF《Design Patterns》第 3 章 Abstract Factory 一节。

相关推荐
GKxx19 分钟前
在 HarmonyOS 上从源码构建 GCC 16(gcc/g++ + libstdc++ + libsanitizer):完整记录
c++·华为·harmonyos·鸿蒙·gcc
晴天的雨.99221 分钟前
类和对象下(内部类,匿名对象,对象拷贝时的编译器优化)
开发语言·c++·算法
旖旎夜光40 分钟前
LeetCode 238:除自身以外数组的乘积(前缀和) —— 题解
数据结构·c++·算法·leetcode·前缀和
Escalating_xu1 小时前
【C++类和对象(上)】从类的定义、封装与对象模型到内存对齐和 this 指针
开发语言·前端·c++
吃着火锅x唱着歌1 小时前
Effective C++ 学习笔记 条款45 运用成员函数模板接受所有兼容类型
c++·笔记·学习
爱和冰阔落1 小时前
【Linux】多线程打印为什么会乱?pthread 创建、等待、退出、取消与分离全实战
linux·运维·c++·redis
luj_17681 小时前
尾椎藏今生记忆?骨盆对应不确定性
开发语言·网络·c++·经验分享·算法
断点之下1 小时前
C++类和对象:六个默认成员函数详解
开发语言·c++
库玛西3 小时前
深入浅出传输层:UDP 与 TCP 协议全景指南
linux·服务器·网络·c++·笔记·tcp/ip·udp