03 抽象工厂(创建型)

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

作者 David更新于

03 抽象工厂 · Abstract Factory(创建型)

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

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

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

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

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

二、解决什么痛:一族对象,必须“成套”

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

不用抽象工厂,痛在哪?你会写出这种代码:

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

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

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

三、怎么实现:一个工厂,一族创建方法

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

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()。产品维度越稳定,抽象工厂越合适;产品种类老是增删,它就是负担。
  • 别为“可能有第二个平台”就上抽象工厂。 如果你现在只有一个平台,而且第二个八字没一撇,这套接口 + 多工厂的架子就是纯开销。抽象工厂的复杂度不低,值不值得,取决于“产品族真的会有多个”这个前提是否成立。

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

五、开源里怎么用

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

创建型到这里,我们已经会造“一个”(工厂方法)和“一族”(抽象工厂)。下一篇建造者,换个维度:当单个对象本身复杂到“零件又多又杂、还有一堆可选项”时,怎么优雅地把它拼出来。

本页目录