4、抽象工厂模式:概念与结构、PHP实现、在跨平台UI组件库中的应用、与工厂方法的区别
4.1 抽象工厂模式的概念与结构
抽象工厂模式,说白了就是「工厂的工厂」。
我刚开始接触这个模式时,觉得它有点绕。但后来在项目中用多了,发现它其实很直观——当你的系统需要处理「一系列相关产品」时,抽象工厂就是最佳选择。
举个例子。你想想看,一个UI组件库,需要支持Windows、macOS、Linux三种风格。每种风格下,都有按钮、输入框、弹窗这些组件。如果只用工厂方法,你得为每个组件写一个工厂类。那数量就爆炸了。
抽象工厂的核心思想是:定义一个创建产品族的接口,每个具体工厂负责生产一整族产品。
它的结构包含这几个角色:
- 抽象工厂(AbstractFactory):声明创建一系列产品的方法
- 具体工厂(ConcreteFactory):实现抽象工厂,生产某一族的具体产品
- 抽象产品(AbstractProduct):定义一类产品的接口
- 具体产品(ConcreteProduct):实现抽象产品接口
- 客户端(Client):只依赖抽象工厂和抽象产品
关键点:客户端代码完全不关心具体产品是怎么创建的。它只知道「我要一个按钮」,至于这个按钮是圆角的还是方角的,由具体工厂决定。
4.2 PHP实现抽象工厂模式
咱们直接上代码。我习惯先定义抽象产品接口:
<?php
// 抽象产品:按钮
interface Button {
public function render(): string;
}
// 抽象产品:输入框
interface Input {
public function render(): string;
}
// 抽象产品:弹窗
interface Modal {
public function render(): string;
}
然后定义具体产品。这里以Windows风格为例:
// Windows风格按钮
class WindowsButton implements Button {
public function render(): string {
return "渲染Windows风格按钮(圆角、蓝色)";
}
}
// Windows风格输入框
class WindowsInput implements Input {
public function render(): string {
return "渲染Windows风格输入框(带边框阴影)";
}
}
// Windows风格弹窗
class WindowsModal implements Modal {
public function render(): string {
return "渲染Windows风格弹窗(带关闭动画)";
}
}
macOS风格的产品类也类似,只是渲染逻辑不同:
class MacButton implements Button {
public function render(): string {
return "渲染macOS风格按钮(扁平、无边框)";
}
}
class MacInput implements Input {
public function render(): string {
return "渲染macOS风格输入框(圆角、无边框)";
}
}
class MacModal implements Modal {
public function render(): string {
return "渲染macOS风格弹窗(毛玻璃效果)";
}
}
接下来是核心——抽象工厂接口:
interface UIFactory {
public function createButton(): Button;
public function createInput(): Input;
public function createModal(): Modal;
}
具体工厂实现:
class WindowsFactory implements UIFactory {
public function createButton(): Button {
return new WindowsButton();
}
public function createInput(): Input {
return new WindowsInput();
}
public function createModal(): Modal {
return new WindowsModal();
}
}
class MacFactory implements UIFactory {
public function createButton(): Button {
return new MacButton();
}
public function createInput(): Input {
return new MacInput();
}
public function createModal(): Modal {
return new MacModal();
}
}
客户端使用起来非常清爽:
class Application {
private UIFactory $factory;
public function __construct(UIFactory $factory) {
$this->factory = $factory;
}
public function renderUI(): void {
$button = $this->factory->createButton();
$input = $this->factory->createInput();
$modal = $this->factory->createModal();
echo $button->render() . "\n";
echo $input->render() . "\n";
echo $modal->render() . "\n";
}
}
// 运行时决定风格
$os = 'windows'; // 可以从配置文件读取
$factory = match($os) {
'windows' => new WindowsFactory(),
'mac' => new MacFactory(),
default => throw new \InvalidArgumentException("不支持的平台"),
};
$app = new Application($factory);
$app->renderUI();
我的习惯:在实际项目中,我会把工厂的选择逻辑封装到一个「工厂提供者」类中。这样客户端代码只需要调用 FactoryProvider::getFactory('windows'),完全不用关心工厂是怎么创建的。
4.3 在跨平台UI组件库中的应用
我在几年前参与过一个企业级SaaS项目,需要同时支持Web端和桌面端。Web端用Bootstrap,桌面端用Electron。当时UI组件库的设计就用了抽象工厂模式。
为什么选它?因为:
- 每个平台都有按钮、表格、表单、弹窗等组件
- 不同平台的组件实现完全不同(Web用HTML+CSS,桌面用原生控件)
- 业务逻辑层不希望关心平台差异
抽象工厂模式完美解决了这个问题。我们定义了一个 ComponentFactory 接口,然后有 WebFactory 和 DesktopFactory 两个实现。业务代码只需要注入工厂,就能拿到对应平台的组件。
我曾经踩过的坑:一开始我把所有组件都放在一个工厂里,结果工厂接口变得非常庞大。后来发现,应该按「产品族」来拆分。比如「基础组件工厂」只负责按钮、输入框、标签,「高级组件工厂」负责表格、树形控件、图表。每个工厂的产品族内聚性更强。
抽象工厂在跨平台场景下的优势很明显:
| 场景 | 使用前 | 使用后 |
|---|---|---|
| 新增一个平台 | 修改多处业务代码 | 只需新增一个具体工厂 |
| 新增一个组件 | 所有平台都要改 | 抽象工厂加方法,各平台实现 |
| 切换平台 | 大量if-else判断 | 一行代码换工厂 |
4.4 与工厂方法的区别
这个问题面试经常问。我简单说下核心区别:
- 工厂方法:一个工厂只生产一种产品。比如
ButtonFactory只生产按钮。 - 抽象工厂:一个工厂生产一族产品。比如
WindowsFactory生产Windows风格的所有组件。
你想想看,如果我用工厂方法来实现跨平台UI,会是什么样?
// 工厂方法模式
$buttonFactory = new WindowsButtonFactory();
$inputFactory = new WindowsInputFactory();
$modalFactory = new WindowsModalFactory();
$button = $buttonFactory->create();
$input = $inputFactory->create();
$modal = $modalFactory->create();
每个组件都要单独创建工厂。如果平台多了,工厂类的数量就是「组件数 × 平台数」。而抽象工厂只需要「平台数」个工厂。
选择建议:
- 如果只创建单一产品,用工厂方法
- 如果需要创建一系列相关产品,用抽象工厂
- 如果产品族经常变化,抽象工厂更合适
- 如果产品等级结构经常变化,工厂方法更灵活
我记得有一次重构,就是把一堆工厂方法改成了抽象工厂。改完之后,代码量减少了40%,而且新增平台时只需要加一个文件。嗯,那种感觉真的很爽。
小技巧:抽象工厂和工厂方法并不互斥。我经常在抽象工厂内部使用工厂方法来实现具体产品的创建。这样既保证了产品族的一致性,又保留了单个产品的扩展性。
最后说一句:设计模式是工具,不是教条。我在项目中见过有人为了用抽象工厂而用抽象工厂,结果把简单问题搞复杂了。记住,只有当你的系统确实需要处理「产品族」时,抽象工厂才值得引入。