5、抽象工厂模式:模式动机与定义、角色与结构、C++代码实现、产品族与产品等级结构

5.1 模式动机:为什么需要抽象工厂?

先聊个实际场景。我在做跨平台UI库的时候,遇到过这么个问题:

同一个应用,要跑在Windows、macOS、Linux上。每个平台的按钮、文本框、菜单长得都不一样。如果我用简单工厂,每加一个平台就得改工厂代码。用工厂方法呢?每个产品都得配一个工厂类,产品一多,工厂类就爆炸了。

你想想看,一个UI库至少有按钮、文本框、复选框、滚动条……每种控件在不同平台都有实现。如果每个控件都单独搞工厂,那代码量简直不敢看。

抽象工厂就是来解决这个痛点的。它把「一组相关的产品」打包在一起生产。比如Windows平台下的所有控件,由一个Windows工厂统一创建。这样平台切换时,只需要换一个工厂对象就行。

核心思想: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

5.2 模式定义与角色结构

抽象工厂模式的定义其实就一句话:为创建一组相关或相互依赖的对象提供接口,而不需要指定具体类

说白了,就是「工厂的工厂」。每个具体工厂负责生产一整套产品族。

5.2.1 四个核心角色

角色 职责 我见过的坑
抽象工厂 声明创建抽象产品对象的操作接口 接口粒度要适中,太粗太细都不行
具体工厂 实现创建具体产品对象的操作 每个工厂对应一个产品族
抽象产品 定义一类产品的公共接口 比如「按钮」的抽象接口
具体产品 实现抽象产品的具体类 比如「Windows按钮」

5.2.2 产品族与产品等级结构

这两个概念是理解抽象工厂的关键。我画个图帮你理清:

产品族与产品等级结构 产品等级结构 产品族 按钮 文本框 复选框 Windows按钮 Windows文本框 Windows复选框 macOS按钮 macOS文本框 macOS复选框 Linux按钮 Linux文本框 Linux复选框 Windows产品族 macOS产品族 Linux产品族 纵向:产品等级结构

看懂了吗?产品等级结构是纵向的——按钮、文本框、复选框这些抽象类型。而产品族是横向的——Windows下的一套控件、macOS下的一套控件。

抽象工厂的核心就是:一个工厂生产一个完整的产品族。Windows工厂生产Windows按钮+Windows文本框+Windows复选框。macOS工厂同理。

5.3 C++代码实现

来,直接上代码。这是我个人比较喜欢的一种写法,清晰且易于扩展。

5.3.1 抽象产品接口

// 按钮抽象接口
class Button {
public:
    virtual ~Button() = default;
    virtual void render() = 0;
    virtual void onClick() = 0;
};

// 文本框抽象接口
class TextBox {
public:
    virtual ~TextBox() = default;
    virtual void render() = 0;
    virtual void setText(const std::string& text) = 0;
};

5.3.2 具体产品实现

// Windows按钮
class WindowsButton : public Button {
public:
    void render() override {
        std::cout << "[Windows] 绘制按钮:圆角、蓝色渐变" << std::endl;
    }
    void onClick() override {
        std::cout << "[Windows] 按钮点击:水波纹效果" << std::endl;
    }
};

// macOS按钮
class MacOSButton : public Button {
public:
    void render() override {
        std::cout << "[macOS] 绘制按钮:扁平、圆角矩形" << std::endl;
    }
    void onClick() override {
        std::cout << "[macOS] 按钮点击:弹性动画" << std::endl;
    }
};

// Windows文本框
class WindowsTextBox : public TextBox {
public:
    void render() override {
        std::cout << "[Windows] 绘制文本框:带边框、阴影" << std::endl;
    }
    void setText(const std::string& text) override {
        std::cout << "[Windows] 文本框设置文字:" << text << std::endl;
    }
};

// macOS文本框
class MacOSTextBox : public TextBox {
public:
    void render() override {
        std::cout << "[macOS] 绘制文本框:无边框、毛玻璃效果" << std::endl;
    }
    void setText(const std::string& text) override {
        std::cout << "[macOS] 文本框设置文字:" << text << std::endl;
    }
};

5.3.3 抽象工厂与具体工厂

// 抽象工厂:声明创建整套UI组件的方法
class UIFactory {
public:
    virtual ~UIFactory() = default;
    virtual std::unique_ptr<Button> createButton() = 0;
    virtual std::unique_ptr<TextBox> createTextBox() = 0;
};

// Windows工厂:生产Windows风格的所有组件
class WindowsFactory : public UIFactory {
public:
    std::unique_ptr<Button> createButton() override {
        return std::make_unique<WindowsButton>();
    }
    std::unique_ptr<TextBox> createTextBox() override {
        return std::make_unique<WindowsTextBox>();
    }
};

// macOS工厂:生产macOS风格的所有组件
class MacOSFactory : public UIFactory {
public:
    std::unique_ptr<Button> createButton() override {
        return std::make_unique<MacOSButton>();
    }
    std::unique_ptr<TextBox> createTextBox() override {
        return std::make_unique<MacOSTextBox>();
    }
};

5.3.4 客户端使用

void buildUI(UIFactory& factory) {
    auto btn = factory.createButton();
    auto txt = factory.createTextBox();

    btn->render();
    txt->render();
    txt->setText("Hello, Abstract Factory!");
    btn->onClick();
}

int main() {
    // 运行时决定使用哪个产品族
    std::string os = getPlatform(); // "Windows" 或 "macOS"

    std::unique_ptr<UIFactory> factory;
    if (os == "Windows") {
        factory = std::make_unique<WindowsFactory>();
    } else {
        factory = std::make_unique<MacOSFactory>();
    }

    buildUI(*factory);
    return 0;
}
我的习惯:std::unique_ptr 管理工厂和产品对象的生命周期。这样既避免了裸指针的麻烦,又保持了多态性。

5.4 避坑指南与经验总结

我曾经踩过的坑: 有一次我把抽象工厂的接口设计得太细了,每个产品都单独一个创建方法。结果产品一多,接口就膨胀得厉害。后来我学会了——接口粒度要跟产品族的边界对齐。如果某个产品不是每个产品族都有的,就别往抽象工厂里塞。

抽象工厂模式最适合的场景:

  • 系统需要独立于产品的创建、组合和表示——客户端只跟抽象接口打交道
  • 系统要配置多个产品族中的某一个——运行时切换工厂对象即可
  • 产品族内的产品需要一起使用——保证UI风格统一
  • 你想隐藏产品类的具体实现——客户端根本不知道具体类名

嗯,这里要注意:抽象工厂模式也有它的代价。加一个新的产品等级结构(比如加一个「滑块」控件),就得修改抽象工厂接口,所有具体工厂也得跟着改。这就是所谓的「开闭原则的倾斜」——对产品族扩展友好,对产品等级结构扩展不友好。

我个人建议,在设计初期就把产品等级结构想清楚。一旦定下来,尽量别动。如果确实需要频繁扩展产品类型,那可能工厂方法模式更适合你。

一句话总结: 抽象工厂模式 = 工厂方法模式的「组合升级版」。它把一组相关的产品打包生产,保证产品族内的一致性。代价是产品等级结构的扩展成本较高。

公众号:蓝海资料掘金营,微信 deep3321