跳转至

抽象工厂模式

分类:创建型模式。对应《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。客户也不必创建工厂能提供的所有产品,奶酪披萨只获取它需要的原料。

抽象工厂接口

原书配套源码中的创建接口如下:

1
2
3
4
5
6
7
8
public interface PizzaIngredientFactory {
    Dough createDough();
    Sauce createSauce();
    Cheese createCheese();
    Veggies[] createVeggies();
    Pepperoni createPepperoni();
    Clams createClam();
}

这里的 Dough、Sauce、Cheese 等分别是原料抽象类型。具体工厂实现这些方法,返回对应地区的具体产品。

例如,纽约工厂的三个创建方法是:

public Dough createDough() {
    return new ThinCrustDough();
}

public Sauce createSauce() {
    return new MarinaraSauce();
}

public Cheese createCheese() {
    return new ReggianoCheese();
}

芝加哥工厂实现同一接口,分别返回 ThickCrustDough、PlumTomatoSauce、MozzarellaCheese。其余创建方法按产品族表实现;完整类型定义见可运行示例。

披萨如何使用原料工厂

奶酪披萨通过构造器接收工厂,在准备阶段请求所需原料。以下片段省略日志和其他制作步骤:

class CheesePizza extends Pizza {
    private final PizzaIngredientFactory ingredientFactory;

    public CheesePizza(PizzaIngredientFactory ingredientFactory) {
        super("Cheese Pizza");
        this.ingredientFactory = java.util.Objects.requireNonNull(ingredientFactory);
    }

    @Override
    public void prepare() {
        dough = ingredientFactory.createDough();
        sauce = ingredientFactory.createSauce();
        cheese = ingredientFactory.createCheese();
    }
}

CheesePizza 只调用抽象接口,不直接选择薄面团或厚面团。给它注入纽约工厂或芝加哥工厂,可以复用同一个披萨类,得到不同地区的原料组合。

ClamPizza 的准备步骤还会调用 createClam():同一套披萨制作逻辑在纽约得到新鲜蛤蜊,在芝加哥得到冷冻蛤蜊。

与工厂方法一起工作

纽约店的工厂方法创建原料工厂,并把它注入披萨。下面是仅保留两种口味的教学简化:

class NYPizzaStore extends PizzaStore {
    @Override
    protected Pizza createPizza(String type) {
        PizzaIngredientFactory ingredientFactory =
                new NYPizzaIngredientFactory();

        if ("cheese".equals(type)) {
            return new CheesePizza(ingredientFactory);
        }
        if ("clam".equals(type)) {
            return new ClamPizza(ingredientFactory);
        }
        throw new IllegalArgumentException("不支持的披萨:" + type);
    }
}

当客户调用 new NYPizzaStore().orderPizza("cheese") 时:

  1. orderPizza() 调用纽约店实现的 createPizza()。
  2. createPizza() 创建 CheesePizza,并注入 NYPizzaIngredientFactory。
  3. orderPizza() 调用披萨的 prepare()。
  4. prepare() 向原料工厂请求面团、酱汁和奶酪。
  5. orderPizza() 继续执行 bake()、cut()、box()。
职责 对应模式与对象 创建的结果
决定如何创建披萨 工厂方法:具体披萨店的 createPizza() CheesePizza、ClamPizza 等披萨对象
提供地区配套原料 抽象工厂:PizzaIngredientFactory 的具体实现 Dough、Sauce、Cheese 等原料对象

两种模式在此分别处理披萨和原料两个层次的变化。原书在工厂方法阶段使用地区专属的披萨类,在引入原料工厂后,可以让不同地区复用 CheesePizza 等类。

为什么不能只按创建方法的数量判断

在常见代码中,把一个创建方法扩展成几个创建方法,确实可能就是工厂方法示例与抽象工厂示例最直观的结构差异。抽象工厂把这些方法组织在一起,是为了让多个相关产品随同一个工厂一起切换。

判断时结合客户如何使用这些产品:

  • 工厂方法关注某个创建步骤由具体创建者决定实现。
  • 抽象工厂关注一组相关产品的创建规则如何作为一套提供和替换。
  • 多个互不相关的创建工具方法放进同一个类,并不自动形成有意义的产品族。

抽象工厂通常通过多个工厂方法实现,两种模式可以同时出现。删除其他产品的创建方法后,如果只剩一个抽象创建方法和若干具体实现,这段简化代码可以按工厂方法的常见结构理解;这不是所有代码增删一个方法就自动改变模式的规则。

抽象工厂也可以直接创建相关的披萨产品,但本篇遵循原书的原料工厂示例,不把其他教学改写当成原书实现。

适用场景与扩展成本

  • 客户需要一起使用多种相关产品,而且存在可以替换的产品族,例如不同平台的按钮、输入框和菜单。
  • 需要集中选择一套产品实现,减少客户分别装配时混用不同系列的机会。配套关系来自具体工厂的实现约定;普通 Java 接口并不会自动阻止开发者返回另一族的对象。
  • 新增地区产品族时,可以新增原料工厂并在装配处选择它,披萨的原料使用逻辑通常不必修改。
  • 新增原料种类时,例如增加一个独立的 createOil(),通常需要修改抽象工厂接口、所有具体工厂及使用该原料的客户代码。

如果仅创建一个简单对象,或者各产品完全独立、不需要成套切换,应先评估直接创建、简单工厂或工厂方法是否已经足够。

运行示例与来源

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 : 创建

协作过程

  1. PizzaTestDrive.main() 创建两家门店,并分别订购奶酪披萨;PizzaStore.orderPizza() 调用具体门店的 createPizza()。
  2. NYPizzaStore 创建 NYPizzaIngredientFactory 并注入新的 CheesePizza;ChicagoPizzaStore 则注入 ChicagoPizzaIngredientFactory。两家门店还会设置各自的披萨名称。
  3. 订购流程调用 CheesePizza.prepare(),后者依次请求 createDough()、createSauce() 和 createCheese(),将返回的对象存入继承自 Pizza 的字段。
  4. 纽约工厂返回薄面团、玛丽娜拉酱汁和 ReggianoCheese;芝加哥工厂返回厚面团、李子番茄酱汁和 MozzarellaCheese。配套关系由具体工厂的返回规则表达。
  5. orderPizza() 继续烘烤、切片和装盒。披萨创建由门店的工厂方法负责,原料产品族由抽象工厂负责,二者在同一订单中协作。

固定版本源码:

评论