抽象工厂模式¶
分类:创建型模式。对应《Head First 设计模式》第 4 章,与简单工厂、工厂方法在同一章讨论。
抽象工厂提供创建一组相关或相互依赖对象的接口,让客户通过抽象接口使用这些产品,不必依赖它们的具体类。
类结构¶
classDiagram
direction TB
class Client
class AbstractFactory {
<<interface>>
+createProductA() AbstractProductA
+createProductB() AbstractProductB
}
class ConcreteFactory1
class ConcreteFactory2
class AbstractProductA {
<<interface>>
+operationA()
}
class AbstractProductB {
<<interface>>
+operationB()
}
class ProductA1
class ProductA2
class ProductB1
class ProductB2
Client --> AbstractFactory : 请求一组产品
Client ..> AbstractProductA : 使用
Client ..> AbstractProductB : 使用
AbstractFactory <|.. ConcreteFactory1
AbstractFactory <|.. ConcreteFactory2
AbstractProductA <|.. ProductA1
AbstractProductA <|.. ProductA2
AbstractProductB <|.. ProductB1
AbstractProductB <|.. ProductB2
ProductA1 <.. ConcreteFactory1 : 创建
ProductB1 <.. ConcreteFactory1 : 创建
ProductA2 <.. ConcreteFactory2 : 创建
ProductB2 <.. ConcreteFactory2 : 创建
角色定义¶
| 角色 | 定义 |
|---|---|
| Client(客户) | 通过抽象工厂取得产品,并依赖抽象产品接口使用它们。 |
| AbstractFactory(抽象工厂) | 定义多种相关产品的创建接口;这里的 A、B 表示两种产品类别。 |
| ConcreteFactory1 / ConcreteFactory2(具体工厂) | 分别提供产品族 1 和产品族 2 的创建规则,例如工厂 1 创建配套的 ProductA1、ProductB1。 |
| AbstractProductA / AbstractProductB(抽象产品) | 分别定义 A 类、B 类产品的公共操作。 |
| ProductA1 / ProductB1(产品族 1) | 实现各自的产品接口,按具体工厂的约定配套使用。 |
| ProductA2 / ProductB2(产品族 2) | 提供另一套产品实现;切换工厂即可切换这套创建规则。 |
原书的例子:为披萨提供原料¶
原书先使用工厂方法让纽约店、芝加哥店决定创建什么披萨。随后提出原料供应问题:不同地区可以有不同的口味和原料选择,制作披萨的代码需要使用与该地区配套的一组原料。
因此,原书把抽象工厂放在原料层。PizzaStore.createPizza() 仍然创建披萨,PizzaIngredientFactory 提供制作披萨需要的面团、酱汁、奶酪、蔬菜、香肠和蛤蜊。
产品种类与产品族¶
| 抽象产品种类 | 纽约原料工厂提供的产品 | 芝加哥原料工厂提供的产品 |
|---|---|---|
Dough:面团 |
ThinCrustDough:薄面团 |
ThickCrustDough:厚面团 |
Sauce:酱汁 |
MarinaraSauce |
PlumTomatoSauce |
Cheese:奶酪 |
ReggianoCheese |
MozzarellaCheese |
Veggies[]:蔬菜 |
大蒜、洋葱、蘑菇、红椒 | 黑橄榄、菠菜、茄子 |
Pepperoni:意式辣香肠 |
SlicedPepperoni |
SlicedPepperoni |
Clams:蛤蜊 |
FreshClams:新鲜蛤蜊 |
FrozenClams:冷冻蛤蜊 |
表中的每一行是一种产品,纽约和芝加哥两列分别构成一个产品族。选择纽约原料工厂,就选择了它提供的整套创建规则。
产品族中的每一项不必都与另一族不同:原书中两地都使用 SlicedPepperoni。客户也不必创建工厂能提供的所有产品,奶酪披萨只获取它需要的原料。
抽象工厂接口¶
原书配套源码中的创建接口如下:
这里的 Dough、Sauce、Cheese 等分别是原料抽象类型。具体工厂实现这些方法,返回对应地区的具体产品。
例如,纽约工厂的三个创建方法是:
芝加哥工厂实现同一接口,分别返回 ThickCrustDough、PlumTomatoSauce、MozzarellaCheese。其余创建方法按产品族表实现;完整类型定义见可运行示例。
披萨如何使用原料工厂¶
奶酪披萨通过构造器接收工厂,在准备阶段请求所需原料。以下片段省略日志和其他制作步骤:
CheesePizza 只调用抽象接口,不直接选择薄面团或厚面团。给它注入纽约工厂或芝加哥工厂,可以复用同一个披萨类,得到不同地区的原料组合。
ClamPizza 的准备步骤还会调用 createClam():同一套披萨制作逻辑在纽约得到新鲜蛤蜊,在芝加哥得到冷冻蛤蜊。
与工厂方法一起工作¶
纽约店的工厂方法创建原料工厂,并把它注入披萨。下面是仅保留两种口味的教学简化:
当客户调用 new NYPizzaStore().orderPizza("cheese") 时:
orderPizza()调用纽约店实现的createPizza()。createPizza()创建CheesePizza,并注入NYPizzaIngredientFactory。orderPizza()调用披萨的prepare()。prepare()向原料工厂请求面团、酱汁和奶酪。orderPizza()继续执行bake()、cut()、box()。
| 职责 | 对应模式与对象 | 创建的结果 |
|---|---|---|
| 决定如何创建披萨 | 工厂方法:具体披萨店的 createPizza() |
CheesePizza、ClamPizza 等披萨对象 |
| 提供地区配套原料 | 抽象工厂:PizzaIngredientFactory 的具体实现 |
Dough、Sauce、Cheese 等原料对象 |
两种模式在此分别处理披萨和原料两个层次的变化。原书在工厂方法阶段使用地区专属的披萨类,在引入原料工厂后,可以让不同地区复用 CheesePizza 等类。
为什么不能只按创建方法的数量判断¶
在常见代码中,把一个创建方法扩展成几个创建方法,确实可能就是工厂方法示例与抽象工厂示例最直观的结构差异。抽象工厂把这些方法组织在一起,是为了让多个相关产品随同一个工厂一起切换。
判断时结合客户如何使用这些产品:
- 工厂方法关注某个创建步骤由具体创建者决定实现。
- 抽象工厂关注一组相关产品的创建规则如何作为一套提供和替换。
- 多个互不相关的创建工具方法放进同一个类,并不自动形成有意义的产品族。
抽象工厂通常通过多个工厂方法实现,两种模式可以同时出现。删除其他产品的创建方法后,如果只剩一个抽象创建方法和若干具体实现,这段简化代码可以按工厂方法的常见结构理解;这不是所有代码增删一个方法就自动改变模式的规则。
抽象工厂也可以直接创建相关的披萨产品,但本篇遵循原书的原料工厂示例,不把其他教学改写当成原书实现。
适用场景与扩展成本¶
- 客户需要一起使用多种相关产品,而且存在可以替换的产品族,例如不同平台的按钮、输入框和菜单。
- 需要集中选择一套产品实现,减少客户分别装配时混用不同系列的机会。配套关系来自具体工厂的实现约定;普通 Java 接口并不会自动阻止开发者返回另一族的对象。
- 新增地区产品族时,可以新增原料工厂并在装配处选择它,披萨的原料使用逻辑通常不必修改。
- 新增原料种类时,例如增加一个独立的
createOil(),通常需要修改抽象工厂接口、所有具体工厂及使用该原料的客户代码。
如果仅创建一个简单对象,或者各产品完全独立、不需要成套切换,应先评估直接创建、简单工厂或工厂方法是否已经足够。
运行示例与来源¶
- 示例代码与运行方法:展示纽约/芝加哥 × 奶酪/蛤蜊四种订单。为便于独立运行,采用嵌套类型并简化原料行为,保留两种模式的协作结构。
- 《Head First 设计模式》第 4 章。以下链接固定到作者配套仓库的 2019 年版本,该版本 README 标注对应 2014 年更新版:
- 版本说明。
- 抽象工厂接口。
- 纽约原料工厂与芝加哥原料工厂。
- CheesePizza与NYPizzaStore。
Head First 示例:为地区披萨配套供应原料¶
《Head First 设计模式》第 4 章把地区差异从披萨子类进一步移到原料供应层:两家门店都可以创建 CheesePizza,但分别注入纽约或芝加哥的原料工厂。准备披萨时,同一次工厂选择就确定了面团、酱汁和奶酪的搭配,披萨只需调用原料工厂接口。
前文保留的是对应 2014 年更新版的历史来源及教学改写;本节作为新对照,独立依据作者 2020 年第二版配套代码 factory/pizzaaf 整理。这里的 Pizza、CheesePizza 等名称均指该目录中的实际类型:CheesePizza 持有包级可见的 ingredientFactory 字段,prepare() 也是包级可见,实际调用三种原料的创建方法。下图只展开其中的面团和酱汁产品族。
| 具体类名 | 对应角色 | 职责 |
|---|---|---|
CheesePizza |
原料工厂的客户 | 接收 PizzaIngredientFactory,在 prepare() 中取得制作奶酪披萨所需的原料。 |
Pizza |
披萨抽象基类 | 保存原料字段,声明抽象的 prepare() 并提供其他制作步骤。 |
PizzaIngredientFactory |
抽象工厂接口 | 声明面团、酱汁、奶酪等一组原料的创建操作。 |
NYPizzaIngredientFactory |
纽约具体工厂 | 提供薄面团和玛丽娜拉酱汁等纽约原料组合。 |
ChicagoPizzaIngredientFactory |
芝加哥具体工厂 | 提供厚面团和李子番茄酱汁等芝加哥原料组合。 |
Dough |
面团抽象产品 | 约定面团的文字描述方法。 |
Sauce |
酱汁抽象产品 | 约定酱汁的文字描述方法。 |
ThinCrustDough |
纽约面团产品 | 实现 Dough,表示薄面团。 |
MarinaraSauce |
纽约酱汁产品 | 实现 Sauce,表示玛丽娜拉酱汁。 |
ThickCrustDough |
芝加哥面团产品 | 实现 Dough,表示厚面团。 |
PlumTomatoSauce |
芝加哥酱汁产品 | 实现 Sauce,表示李子番茄酱汁。 |
示例 UML¶
图中保留两个地区、一种披萨、两种原料,共 11 个类型;省略门店与测试入口、奶酪等其余原料类型及其创建方法。CheesePizza.prepare() 实际还会调用 createCheese(),图中的省略不代表配方删去了奶酪。~ 表示包级可见,* 标记抽象方法。
classDiagram
direction TB
class Pizza {
<<abstract>>
~Dough dough
~Sauce sauce
~prepare() void*
}
class CheesePizza {
~PizzaIngredientFactory ingredientFactory
+CheesePizza(PizzaIngredientFactory ingredientFactory)
~prepare() void
}
class PizzaIngredientFactory {
<<interface>>
+createDough() Dough
+createSauce() Sauce
}
class NYPizzaIngredientFactory {
+createDough() Dough
+createSauce() Sauce
}
class ChicagoPizzaIngredientFactory {
+createDough() Dough
+createSauce() Sauce
}
class Dough {
<<interface>>
+toString() String
}
class Sauce {
<<interface>>
+toString() String
}
class ThinCrustDough {
+toString() String
}
class ThickCrustDough {
+toString() String
}
class MarinaraSauce {
+toString() String
}
class PlumTomatoSauce {
+toString() String
}
Pizza <|-- CheesePizza
CheesePizza --> PizzaIngredientFactory : 持有原料工厂
Pizza --> Dough : 保存面团
Pizza --> Sauce : 保存酱汁
PizzaIngredientFactory <|.. NYPizzaIngredientFactory
PizzaIngredientFactory <|.. ChicagoPizzaIngredientFactory
Dough <|.. ThinCrustDough
Dough <|.. ThickCrustDough
Sauce <|.. MarinaraSauce
Sauce <|.. PlumTomatoSauce
NYPizzaIngredientFactory ..> ThinCrustDough : 创建
NYPizzaIngredientFactory ..> MarinaraSauce : 创建
ChicagoPizzaIngredientFactory ..> ThickCrustDough : 创建
ChicagoPizzaIngredientFactory ..> PlumTomatoSauce : 创建
协作过程¶
PizzaTestDrive.main()创建两家门店,并分别订购奶酪披萨;PizzaStore.orderPizza()调用具体门店的createPizza()。NYPizzaStore创建NYPizzaIngredientFactory并注入新的CheesePizza;ChicagoPizzaStore则注入ChicagoPizzaIngredientFactory。两家门店还会设置各自的披萨名称。- 订购流程调用
CheesePizza.prepare(),后者依次请求createDough()、createSauce()和createCheese(),将返回的对象存入继承自Pizza的字段。 - 纽约工厂返回薄面团、玛丽娜拉酱汁和
ReggianoCheese;芝加哥工厂返回厚面团、李子番茄酱汁和MozzarellaCheese。配套关系由具体工厂的返回规则表达。 orderPizza()继续烘烤、切片和装盒。披萨创建由门店的工厂方法负责,原料产品族由抽象工厂负责,二者在同一订单中协作。
固定版本源码:
- 原料创建接口:PizzaIngredientFactory
- 纽约产品族:NYPizzaIngredientFactory
- 芝加哥产品族:ChicagoPizzaIngredientFactory
- 原料工厂的客户:CheesePizza