抽象工厂模式:创建一系列相关对象,无需指定具体类
抽象工厂模式,说白了就是「工厂的工厂」。
我刚开始接触设计模式时,觉得工厂方法已经够用了。直到有一次,我在做一个跨平台UI框架的项目——Windows、macOS、Linux三套界面风格,每个平台都有按钮、输入框、菜单这些组件。如果用工厂方法,我得为每个组件写三个工厂类,那代码量……嗯,你想想看,光是按钮就有WindowsButtonFactory、MacButtonFactory、LinuxButtonFactory,再来个输入框……简直灾难。
抽象工厂就是来解决这个问题的。它让你创建一组「配套」的对象,而且客户端代码完全不用关心具体是哪个平台。
核心思想
抽象工厂模式的定义其实很直白:
提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们的具体类。
关键点在于「一系列」和「相关」。不是随便什么对象都能塞进一个抽象工厂里,它们必须属于同一个「产品族」。
举个例子:
- Windows风格:蓝色按钮、圆角输入框、扁平菜单
- macOS风格:灰色按钮、无边框输入框、圆角菜单
- Linux风格:橙色按钮、方形输入框、经典菜单
这三组组件,每组内部是「配套」的。你不能把Windows的按钮和macOS的输入框混在一起用,那界面就乱套了。
结构图
先看一张图,把整体结构理清楚:
这张图里,AbstractFactory 定义了两个创建方法:createProductA() 和 createProductB()。每个具体工厂(ConcreteFactory1、ConcreteFactory2)负责生产一整套产品。客户端只跟抽象工厂和抽象产品打交道,具体是哪个平台的组件,它根本不知道。
什么时候用?
我个人习惯,遇到以下场景就会考虑抽象工厂:
- 系统需要独立于产品的创建、组合和表示——说白了,客户端不想知道「这东西怎么造出来的」。
- 系统要配置多个产品族中的某一个——比如换皮肤、换数据库、换UI风格。
- 产品族内的对象必须一起使用——就像前面说的,Windows按钮配Windows输入框,不能混搭。
- 你想提供一组产品类库,只暴露接口不暴露实现——嗯,这也是封装变化的好办法。
代码示例
来看一个实际例子。假设我们在做一个跨平台的界面库:
// 抽象产品:按钮
interface Button {
void render();
}
// 抽象产品:输入框
interface Input {
void render();
}
// 具体产品:Windows按钮
class WindowsButton implements Button {
public void render() {
System.out.println("渲染Windows风格按钮 - 蓝色、直角");
}
}
// 具体产品:Windows输入框
class WindowsInput implements Input {
public void render() {
System.out.println("渲染Windows风格输入框 - 带边框、白色背景");
}
}
// 具体产品:macOS按钮
class MacButton implements Button {
public void render() {
System.out.println("渲染macOS风格按钮 - 灰色、圆角");
}
}
// 具体产品:macOS输入框
class MacInput implements Input {
public void render() {
System.out.println("渲染macOS风格输入框 - 无边框、毛玻璃效果");
}
}
// 抽象工厂
interface UIFactory {
Button createButton();
Input createInput();
}
// 具体工厂:Windows工厂
class WindowsFactory implements UIFactory {
public Button createButton() {
return new WindowsButton();
}
public Input createInput() {
return new WindowsInput();
}
}
// 具体工厂:macOS工厂
class MacFactory implements UIFactory {
public Button createButton() {
return new MacButton();
}
public Input createInput() {
return new MacInput();
}
}
// 客户端代码
class Application {
private Button button;
private Input input;
public Application(UIFactory factory) {
button = factory.createButton();
input = factory.createInput();
}
public void render() {
button.render();
input.render();
}
}
// 使用
public class Main {
public static void main(String[] args) {
// 运行时决定用哪个工厂
String os = System.getProperty("os.name").toLowerCase();
UIFactory factory;
if (os.contains("win")) {
factory = new WindowsFactory();
} else {
factory = new MacFactory();
}
Application app = new Application(factory);
app.render();
}
}
你看,客户端 Application 完全不知道它用的是 Windows 还是 macOS 的组件。它只认 UIFactory 接口。这就是抽象工厂的威力——把「产品族」的创建逻辑集中管理,客户端代码干干净净。
优缺点分析
| 优点 | 缺点 |
|---|---|
| 隔离了具体类,客户端只依赖抽象接口 | 增加新产品族困难——每加一个产品族就要加一套具体工厂 |
| 保证产品族内对象的一致性 | 支持新产品类型困难——如果要在所有工厂里加一个「滑块」组件,所有具体工厂都得改 |
| 符合开闭原则——增加新工厂不影响现有代码 | 代码结构比工厂方法更复杂,小项目慎用 |
注意:抽象工厂不是银弹。如果你的产品族经常变化,或者产品类型经常增加,抽象工厂会让你很痛苦。我曾经在一个项目中,产品类型从3个扩展到8个,每个具体工厂都要加5个新方法……那段时间改代码改到想吐。
避坑指南
我踩过不少坑,分享几个给你:
- 别滥用抽象工厂。如果你的系统只有一组产品,或者产品之间没有「配套」关系,用简单工厂或者工厂方法就够了。抽象工厂的复杂度不是白给的。
- 产品族要稳定。如果你不确定未来会不会加新产品类型,可以考虑用「参数化工厂方法」——在创建方法里加个类型参数,但这样会牺牲一些类型安全。
- 注意工厂的粒度。一个抽象工厂不要管太多产品类型。我见过有人把数据库连接、日志、缓存、消息队列全塞到一个抽象工厂里……那玩意儿比意大利面还乱。
我的习惯:抽象工厂通常配合「依赖注入」一起用。在Spring里,你只需要配置不同的Bean,就能切换整个产品族。比如开发环境用Mock工厂,生产环境用真实工厂,测试环境用Stub工厂——切换起来特别方便。
与工厂方法的区别
很多人搞混这两个模式。我简单说一下:
- 工厂方法:一个工厂只生产一种产品。你要生产按钮,就写一个按钮工厂;要生产输入框,再写一个输入框工厂。
- 抽象工厂:一个工厂生产一整套产品。你要Windows风格,一个Windows工厂就能生产按钮、输入框、菜单……所有东西。
说白了,工厂方法是「单一产品」的创建,抽象工厂是「产品族」的创建。抽象工厂内部往往用工厂方法来实现每个产品的创建。
抽象工厂模式,是我在跨平台开发、主题切换、多数据库支持等场景下的首选。它让代码的扩展性好了不止一个档次。但记住,别为了用模式而用模式——如果你的项目只有一套UI,写个简单工厂就够了,别整抽象工厂那一套。
嗯,今天就聊到这儿。抽象工厂的核心就是「配套」二字,你记住这个,就不会用错。