# 03 抽象工厂（创建型）

> 工厂方法一次造一个产品，抽象工厂一次造一整族配套产品——还保证它们不串味(不会 Windows 按钮配 macOS 复选框)。讲清“产品族”痛点、Go 实现，以及它对“加产品族友好、加产品种类痛苦”的著名短板。

- 作者：David（道雾轩）
- 专栏：经典设计模式浅析（https://daiw.org/manual/design-patterns.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/design-patterns/abstract-factory
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 03 抽象工厂 · Abstract Factory（创建型）

上一篇的工厂方法，一次造一个产品。这一篇要解决的是升级版难题：**我要一次造一整套配套的产品，而且这套东西之间不能串味。**

## 一、这是什么：宜家的整套家具风格

去宜家买家具，你选的往往不是单件，而是**一个系列**：北欧现代风的沙发、茶几、电视柜是一套，配一起协调；美式复古风又是另一套。你不会拿北欧沙发去配美式复古茶几——**风格得统一**。

“现代风工厂”能给你造出成套的现代家具，“复古风工厂”能给你造出成套的复古家具。你只要选定一个工厂，它交付的**一整族**产品自动是配套的。这就是抽象工厂。

配套代码是经典的跨平台 GUI：一个“Windows 工厂”造出成套的 Windows 按钮 + Windows 复选框；一个“macOS 工厂”造出成套的 macOS 控件。你选定平台工厂，拿到的整套控件风格自动统一。

## 二、解决什么痛：一族对象，必须“成套”

工厂方法解决“造哪个”的问题；抽象工厂在此之上，多压了一个更狠的约束：**这些对象之间有“家族”关系，必须来自同一族，混搭就出事。**

不用抽象工厂，痛在哪？你会写出这种代码：

```go
// 痛点:平台判断散落各处,而且极易"串味"
button := &WindowsButton{}
checkbox := &MacCheckbox{}      // ← 手滑!Windows 按钮配了 macOS 复选框
```

编译器不会拦你——它俩都满足各自的接口。但运行起来，界面风格一半 Windows 一半 macOS，活见鬼。**问题的根源是：“保证同族”这件事，没有任何机制来强制，全靠程序员自觉。** 一旦平台判断逻辑散落在几十个地方，串味是迟早的事。

<Callout type="info">
  **抽象工厂隔离的“会变的东西”，是“整个产品族”。** 会变的不是单个控件，而是“这一整套用哪个平台的实现”。把“造一族产品”的责任收拢到一个工厂对象里，同族一致性就由这个工厂**从结构上**保证了——你根本没机会串味，因为一个 `WindowsFactory` 压根造不出 macOS 复选框。
</Callout>

## 三、怎么实现：一个工厂，一族创建方法

关键区别就一点：工厂方法接口只有**一个**创建方法，抽象工厂接口有**一组**——每个方法造这一族里的一种产品。

```go title="go/abstract-factory/main.go（核心）"
// 抽象工厂:声明一组创建方法,生产成套的相关产品
type GUIFactory interface {
	CreateButton() Button       // 造这一族的按钮
	CreateCheckbox() Checkbox   // 造这一族的复选框
}

// 具体工厂:Windows 一族
type WindowsFactory struct{}
func (f *WindowsFactory) CreateButton() Button     { return &WindowsButton{} }
func (f *WindowsFactory) CreateCheckbox() Checkbox { return &WindowsCheckbox{} }

// 客户端:只依赖抽象工厂 + 抽象产品,拿到的整族自动配套
func renderUI(factory GUIFactory) {
	button := factory.CreateButton()
	checkbox := factory.CreateCheckbox()
	// button 和 checkbox 保证同族——因为它俩出自同一个 factory
	fmt.Println(button.Render(), checkbox.Render())
}
```

`renderUI(&WindowsFactory{})` 交付纯 Windows 套装，`renderUI(&MacFactory{})` 交付纯 macOS 套装。**“同族一致”不再靠自觉，而是靠“它们出自同一个工厂对象”这个事实来保证。** 想支持 Linux？加一个 `LinuxFactory` 实现这仨方法，客户端代码一行不改。

## 四、有哪些坑

- **和工厂方法怎么分？** 一句口诀：**工厂方法是“一个方法造一个产品的变体”，抽象工厂是“一个对象造一族产品的变体”。** 前者关注“这一个东西造哪种”，后者关注“这一整套用哪个系列”。实现上，抽象工厂的每个方法，单拎出来看都像一个工厂方法——抽象工厂常被看作“工厂方法的集合”。
- **著名的软肋：加“产品种类”是噩梦。** 这是抽象工厂最该被记住的缺点。加一个**产品族**(比如新增 Linux 平台)很爽——只需新增一个工厂类。但加一个**产品种类**(比如所有平台都要新增一个 `Slider` 控件)是灾难——你得去改 `GUIFactory` 接口，然后**每一个**具体工厂都得跟着实现 `CreateSlider()`。产品维度越稳定，抽象工厂越合适；产品种类老是增删，它就是负担。
- **别为“可能有第二个平台”就上抽象工厂。** 如果你现在只有一个平台，而且第二个八字没一撇，这套接口 + 多工厂的架子就是纯开销。抽象工厂的复杂度不低，值不值得，取决于“产品族真的会有多个”这个前提是否成立。

<Callout type="warn">
  抽象工厂是创建型里“最重”的模式之一。上它之前先确认两点：**(1) 你真的有“成套配套”的一族产品；(2) 这个“族”真的会有多个变体需要整体切换。** 两个都满足，它是利器；缺一个，它就是过度设计的典型。
</Callout>

## 五、开源里怎么用

- **Java JDK**:`javax.xml.parsers.DocumentBuilderFactory`、`SAXParserFactory`——它们生产一族 XML 解析相关对象，具体实现可通过配置整体替换；`java.awt.Toolkit` 也是按平台生产成套 AWT 组件的抽象工厂。
- **数据库/驱动生态**：一个“方言工厂”对应一个数据库，成套地生产该数据库的连接、方言 SQL 生成器、类型映射器——切换数据库 = 切换整个工厂族。
- **跨平台 UI 框架**：Qt、GTK、以及各类游戏引擎的渲染后端(DirectX 一族 / OpenGL 一族 / Vulkan 一族)，都在用抽象工厂的思路，让“切换整套底层实现”变成“换一个工厂”。
- **Go 里为什么少见“纯抽象工厂”？** 因为 Go 用**接口组合**和**返回接口的函数**就能自然表达“一族相关构造”，很少需要把它郑重其事地封成一个 `XxxFactory` 接口。这再次印证那句话：**模式是语言能力的补丁，语言越强，某些模式就越“隐形”。**

---

创建型到这里，我们已经会造“一个”(工厂方法)和“一族”(抽象工厂)。下一篇[建造者](https://daiw.org/manual/design-patterns/builder)，换个维度：当**单个**对象本身复杂到“零件又多又杂、还有一堆可选项”时，怎么优雅地把它拼出来。
