5、抽象工厂模式:模式动机与定义、角色与结构、C++代码实现、产品族与产品等级结构
5.1 模式动机:为什么需要抽象工厂?
先聊个实际场景。我在做跨平台UI库的时候,遇到过这么个问题:
同一个应用,要跑在Windows、macOS、Linux上。每个平台的按钮、文本框、菜单长得都不一样。如果我用简单工厂,每加一个平台就得改工厂代码。用工厂方法呢?每个产品都得配一个工厂类,产品一多,工厂类就爆炸了。
你想想看,一个UI库至少有按钮、文本框、复选框、滚动条……每种控件在不同平台都有实现。如果每个控件都单独搞工厂,那代码量简直不敢看。
抽象工厂就是来解决这个痛点的。它把「一组相关的产品」打包在一起生产。比如Windows平台下的所有控件,由一个Windows工厂统一创建。这样平台切换时,只需要换一个工厂对象就行。
5.2 模式定义与角色结构
抽象工厂模式的定义其实就一句话:为创建一组相关或相互依赖的对象提供接口,而不需要指定具体类。
说白了,就是「工厂的工厂」。每个具体工厂负责生产一整套产品族。
5.2.1 四个核心角色
| 角色 | 职责 | 我见过的坑 |
|---|---|---|
| 抽象工厂 | 声明创建抽象产品对象的操作接口 | 接口粒度要适中,太粗太细都不行 |
| 具体工厂 | 实现创建具体产品对象的操作 | 每个工厂对应一个产品族 |
| 抽象产品 | 定义一类产品的公共接口 | 比如「按钮」的抽象接口 |
| 具体产品 | 实现抽象产品的具体类 | 比如「Windows按钮」 |
5.2.2 产品族与产品等级结构
这两个概念是理解抽象工厂的关键。我画个图帮你理清:
看懂了吗?产品等级结构是纵向的——按钮、文本框、复选框这些抽象类型。而产品族是横向的——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