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 接口,然后有 WebFactoryDesktopFactory 两个实现。业务代码只需要注入工厂,就能拿到对应平台的组件。

我曾经踩过的坑:一开始我把所有组件都放在一个工厂里,结果工厂接口变得非常庞大。后来发现,应该按「产品族」来拆分。比如「基础组件工厂」只负责按钮、输入框、标签,「高级组件工厂」负责表格、树形控件、图表。每个工厂的产品族内聚性更强。

抽象工厂在跨平台场景下的优势很明显:

场景 使用前 使用后
新增一个平台 修改多处业务代码 只需新增一个具体工厂
新增一个组件 所有平台都要改 抽象工厂加方法,各平台实现
切换平台 大量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%,而且新增平台时只需要加一个文件。嗯,那种感觉真的很爽。

小技巧:抽象工厂和工厂方法并不互斥。我经常在抽象工厂内部使用工厂方法来实现具体产品的创建。这样既保证了产品族的一致性,又保留了单个产品的扩展性。

最后说一句:设计模式是工具,不是教条。我在项目中见过有人为了用抽象工厂而用抽象工厂,结果把简单问题搞复杂了。记住,只有当你的系统确实需要处理「产品族」时,抽象工厂才值得引入