抽象工厂

抽象工厂

抽象工厂是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。

问题分析

譬如某家具店有一系列相关产品,例如椅子(Chair)、沙发(Sofa)和咖啡桌(Coffee­Table);系列产品的不同变体。例如,你可以使用现代(Mordern)、维多利亚(Victorian),装饰风艺术(Art­Deco)等风格生成椅子、沙发和咖啡桌。我们需要根据用户的场景动态地为他们构建不同的产品。

抽象工厂模式建议为系列中的每件产品明确声明接口(例如椅子、沙发或咖啡桌)。然后,确保所有产品变体都继承这些接口。例如,所有风格的椅子都实现椅子接口;所有风格的咖啡桌都实现咖啡桌接口,以此类推。接下来,我们需要声明抽象工厂,包含系列中所有产品构造方法的接口。例如创建椅子(create­Chair)、创建沙发(create­Sofa)和创建咖啡桌(create­Coffee­Table)。这些方法必须返回抽象产品类型,即我们之前抽取的那些接口:椅子,沙发和咖啡桌等等。

对于系列产品的每个变体,我们都将基于抽象工厂接口创建不同的工厂类。每个工厂类都只能返回特定类别的产品,例如,现代家具工厂(Modern­Furniture­Factory)只能创建现代椅子(Mordern­Chair)、现代沙发(Modern­Sofa)和现代咖啡桌(Modern­Coffee­Table)对象。客户端代码可以通过相应的抽象接口调用工厂和产品类。你无需修改实际客户端代码,就能更改传递给客户端的工厂类,也能更改客户端代码接收的产品变体。

优劣对比

如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下,你可以使用抽象工厂。

抽象工厂为你提供了一个接口,可用于创建每个系列产品的对象。只要代码通过该接口创建对象,那么你就不会生成与应用程序已生成的产品类型不一致的产品。如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下,你可以考虑使用抽象工厂。在设计良好的程序中,如果一个类与多种类型产品交互,就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。

实现方式

抽象产品(Abstract Product)为构成系列产品的一组不同但相关的产品声明接口。具体产品(Concrete Product)是抽象产品的多种不同类型实现。所有变体(维多利亚/现代)都必须实现相应的抽象产品(椅子/沙发)。抽象工厂(Abstract Factory)接口声明了一组创建各种抽象产品的方法。具体工厂(Concrete Factory)实现抽象工厂的构建方法。每个具体工厂都对应特定的产品变体,并且仅创建此种产品变体。

  • 首先以不同的产品类型与产品变体为维度绘制矩阵,为所有产品声明抽象产品接口,然后让所有具体产品类实现这些接口。

  • 然后声明抽象工厂接口,并且在接口中为所有抽象产品提供一组构造方法。

  • 接下来为每种产品变体实现一个具体工厂类,在应用程序中开发初始化代码。该代码根据应用程序配置或当前环境,对特定具体工厂类进行初始化。

  • 最后将该工厂对象传递给所有需要创建产品的类。找出代码中所有对产品构造方法的直接调用,将其替换为对工厂对象中相应构造方法的调用。

尽管具体工厂会对具体产品进行初始化,其构造函数签名必须返回相应的 抽象 产品。这样,使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。客户端(Client)只需通过抽象接口调用工厂和产品对象,就能与任何具体工厂/产品变体交互。

案例:跨平台组件

通过应用抽象工厂模式,使得客户端代码无需与具体 UI 类耦合,就能创建跨平台的 UI 元素,同时确保所创建的元素与指定的操作系统匹配。

跨平台应用中的相同 UI 元素功能类似,但是在不同操作系统下的外观有一定差异。此外,你需要确保 UI 元素与当前操作系统风格一致。你一定不希望在 Windows 系统下运行的应用程序中显示 macOS 的控件。抽象工厂接口声明一系列构造函数,客户端代码可调用它们生成不同风格的 UI 元素。每个具体工厂对应特定操作系统,并负责生成符合该操作系统风格的 UI 元素。

// 抽象工厂接口声明了一组能返回不同抽象产品的方法。这些产品属于同一个系列
// 且在高层主题或概念上具有相关性。同系列的产品通常能相互搭配使用。系列产
// 品可有多个变体,但不同变体的产品不能搭配使用。
interface GUIFactory is
    method createButton():Button
    method createCheckbox():Checkbox


// 具体工厂可生成属于同一变体的系列产品。工厂会确保其创建的产品能相互搭配
// 使用。具体工厂方法签名会返回一个抽象产品,但在方法内部则会对具体产品进
// 行实例化。
class WinFactory implements GUIFactory is
    method createButton():Button is
        return new WinButton()
    method createCheckbox():Checkbox is
        return new WinCheckbox()

// 每个具体工厂中都会包含一个相应的产品变体。
class MacFactory implements GUIFactory is
    method createButton():Button is
        return new MacButton()
    method createCheckbox():Checkbox is
        return new MacCheckbox()


// 系列产品中的特定产品必须有一个基础接口。所有产品变体都必须实现这个接口。
interface Button is
    method paint()

// 具体产品由相应的具体工厂创建。
class WinButton implements Button is
    method paint() is
        // 根据 Windows 样式渲染按钮。

class MacButton implements Button is
    method paint() is
        // 根据 macOS 样式渲染按钮

// 这是另一个产品的基础接口。所有产品都可以互动,但是只有相同具体变体的产
// 品之间才能够正确地进行交互。
interface Checkbox is
    method paint()

class WinCheckbox implements Checkbox is
    method paint() is
        // 根据 macOS 样式渲染复选框。

class MacCheckbox implements Checkbox is
    method paint() is
        // 根据 macOS 样式渲染复选框。

// 客户端代码仅通过抽象类型(GUIFactory、Button 和 Checkbox)使用工厂
// 和产品。这让你无需修改任何工厂或产品子类就能将其传递给客户端代码。
class Application is
    private field button: Button
    constructor Application(factory: GUIFactory) is
        this.factory = factory
    method createUI() is
        this.button = factory.createButton()
    method paint() is
        button.paint()


// 程序会根据当前配置或环境设定选择工厂类型,并在运行时创建工厂(通常在初
// 始化阶段)。
class ApplicationConfigurator is
    method main() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            factory = new WinFactory()
        else if (config.OS == "Mac") then
            factory = new MacFactory()
        else
            throw new Exception("错误!未知的操作系统。")

        Application app = new Application(factory)

其运作方式如下:应用程序启动后检测当前操作系统。根据该信息,应用程序通过与该操作系统对应的类创建工厂对象。其余代码使用该工厂对象创建 UI 元素。这样可以避免生成错误类型的元素。使用这种方法,客户端代码只需调用抽象接口,而无需了解具体工厂类和 UI 元素。此外,客户端代码还支持未来添加新的工厂或 UI 元素。

这样一来,每次在应用程序中添加新的 UI 元素变体时,你都无需修改客户端代码。你只需创建一个能够生成这些 UI 元素的工厂类,然后稍微修改应用程序的初始代码,使其能够选择合适的工厂类即可。

下一页