工厂模式

通过 factory 松耦合地制造对象

每次使用 new 操作符就是在针对实现编程,因为 new 意味着在实例化一个具体的类(new ~ 具体)。实例化不应该总是公开进行,因为经常会导致耦合问题。工厂模式封装了对象的创建,让客户从具体类中解耦,从而降低了对具体类实现的依赖,进而得到了更有弹性的设计。

判断要不要使用工厂模式,最本质的参考标准有:

  • 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明

  • 代码复用:创建代码抽离到独立的工厂类之后可以被复用

  • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象

  • 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁

在实际的项目中,简单工厂和工厂方法比较常用,抽象工厂相对不常用(应用场景比较特殊)。

  • 简单工厂:由一个“对象”来处理具体类的实例化

  • 工厂方法:由一个“子类集合”来处理具体类的实例化(类有一种分类方式)

  • 抽象工厂:用一个接口来创建一组产品(类有多种分类方式)

1. 简单工厂

简单工厂其实不是一个“真正的”设计模式,它更多的是一种编程习惯,但经常被使用。

1.1 原理

  • 为了让代码的逻辑更清晰、可读性更好,要善于将功能独立的代码块封装成函数

  • 为了让类的职责更加单一、代码更加清晰,还可以进一步将函数剥离到一个独立的类中,让这个类只负责对象的创建(这个类就是简单工厂模式类)

通常情况,工厂类的命名都是 XxxFactory(但也不是必须的),工厂类中创建对象的方法一般都是 createXxx,但也有 getInstance, createInstance, newInstance 甚至是 valueOf() 等。

class PizzaStore {
    constructor(simplePizzaFactory) {
        // 获取参数,设置简单工厂 【~~组合~~】
        this.factory = simplePizzaFactory
    }

    orderPizza(type) {
        // 用简单工厂,创建具体类
        const pizza = this.factory.createPizza(type)
        // 以下代码:使用具体对象
        pizza.prepare()
        pizza.bake()
        pizza.cut()
        pizza.box()
        return pizza
    }
}

class SimplePizzaFactory {
    createPizza(type) {
        let pizza = null
        if (type === 'cheese') {
            pizza = new CheesePizza()
        } else if (type === 'veggie') {
            pizza = new VeggiePizza()
        } else if (type === 'clam') {
            pizza = new ClamPizza()
        } else if (type === 'pepperoni') {
            pizza = new PepperoniPizza()
        }
        return pizza
    }
}
// 使用
const simplePizzaFactory = new SimplePizzaFactory()
const pizzaStore = new PizzaStore(simplePizzaFactory)
pizzaStore.orderPizza('cheese')

一个常用的技巧是将简单工厂定义为静态方法,因此简单工厂模式也叫静态工厂方法模式。

1.2 更进一步

尽管在简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下是没有问题的(比如不需要频繁地添加,也没有太多具体类)。虽然应用多态或设计模式提高了代码的扩展性,更加符合开闭原则,但也增加了类的个数,牺牲了代码的可读性。

如果非得要将 if 分支逻辑去掉,比较经典的处理办法就是利用多态。实际上这就是工厂方法模式的典型代码实现,我们为(简单)工厂类再创建一个简单工厂(即工厂的工厂)来创建工厂类对象。工厂方法模式比起简单工厂模式更加符合开闭原则。

  • 简单工厂:由一个“对象”来处理具体类的实例化

  • 工厂方法:由一个“子类集合”来处理具体类的实例化

// 
class SimplePizzaFactory {
    createPizza(style, type) {
        let pizza = null

        // 产品新增了 style 分类
        if (style === 'NY') {
            if (type === 'cheese') {
                pizza = new NYStyleCheesePizza()
            } else if (type === 'veggie') {
                pizza = new NYStyleVeggiePizza()
            } else if (type === 'clam') {
                pizza = new NYStyleClamPizza()
            } else if (type === 'pepperoni') {
                pizza = new NYStylePepperoniPizza()
            }

        } else if (style === 'Chicago') {
            if (type === 'cheese') {
                pizza = new ChicagoStyleCheesePizza()
            } else if (type === 'veggie') {
                pizza = new ChicagoStyleVeggiePizza()
            } else if (type === 'clam') {
                pizza = new ChicagoStyleClamPizza()
            } else if (type === 'pepperoni') {
                pizza = new ChicagoStylePepperoniPizza()
            }
        }

        return pizza
    }
}
// 思考:用工厂方法来优化上面的代码

2. 工厂方法

2.1 原理

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法让类把实例化推迟到了子类。

通过每一个工厂,工厂方法模式让我们有办法封装具体类型的实例化。在抽象的工厂父类中,除了 factoryMethod 之外的所有方法,都是用来操作工厂方法生产的产品的。只有子工厂类,真正实现工厂方法并创建产品。

// 父类
class PizzaStore {
    // 子类可以免费获得除了工厂方法之外的所有功能
    orderPizza(type) {
        // 调用工厂方法,创建具体类
        const pizza = createPizza(type)

        pizza.prepare()
        pizza.bake()
        pizza.cut()
        pizza.box()

        return pizza
    }

    // 工厂方法,待子类来实现
    createPizza(type)
}
// 子类
class NYStylePizzaStore extends PizzaStore {
    // 实现工厂方法 【~~继承~~】
    createPizza(type) {
        let pizza = null
        if (type === 'cheese') {
            pizza = new NYStyleCheesePizza()
        } else if (type === 'veggie') {
            pizza = new NYStyleVeggiePizza()
        } else if (type === 'clam') {
            pizza = new NYStyleClamPizza()
        } else if (type === 'pepperoni') {
            pizza = new NYStylePepperoniPizza()
        }
        return pizza
    }
}

class ChicagoStylePizzaStore extends PizzaStore {
    createPizza(type) {
        let pizza = null
        if (type === 'cheese') {
            pizza = new ChicagoStyleCheesePizza()
        } else if (type === 'veggie') {
            pizza = new ChicagoStyleVeggiePizza()
        } else if (type === 'clam') {
            pizza = new ChicagoStyleClamPizza()
        } else if (type === 'pepperoni') {
            pizza = new ChicagoStylePepperoniPizza()
        }
        return pizza
    }
}
// 使用
const nyFactory = new NYPizzaFactory()
nyStore.orderPizza('cheese')  // 它里面会调用工厂方法,以创建具体类

当然工厂类和工厂方法也可以不是抽象的,我们可以定义一个缺省的工厂方法,来生产一些具体产品。这样一来,我们总是有办法创建产品的,即使工厂类没有子类。

2.2 使用场景

当创建逻辑比较复杂,是一个大工程的时候,就考虑用工厂模式来封装对象的创建过程,将对象的创建和使用相分离。

那么,“创建逻辑比较复杂”是指哪些情况呢?有以下两种:

  1. 代码中存在 if-else 分支判断(比如规则配置解析)动态地根据不同类型来创建不同的对象,此时就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。

    • 当每个对象的创建逻辑都比较简单时,推荐使用简单工厂

    • 当每个对象的创建逻辑都比较复杂时,推荐使用工厂方法

  2. 尽管不需要根据不同的类型来创建不同的对象,但是单个对象本身的创建过程比较复杂(比如还要组合其它类对象,做各种初始化操作),此时也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。因为单个对象本身的创建逻辑就比较复杂,所以建议使用工厂方法模式。

除了上面提到的这两种情况外,如果创建对象的逻辑并不复杂,那可以直接通过 new 来创建对象,不需要使用工厂模式。

3. 抽象工厂

3.1 解决的问题

如果类的分类方式不止一种(比如既可以按照文件格式来分类,又可以按照解析对象来分类),此时如果还继续用工厂方法来实现的话,就会产生大量的类。

抽象工厂就是针对这种非常特殊的场景诞生的,我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象,这样就可以有效地减少工厂类的个数。

3.2 原理

抽象工厂模式提供一个接口,来创建相关或依赖对象的家族(创建产品的家族),而不需要指定具体类。

抽象工厂允许客户使用一个抽象接口来创建一组相关产品,而不需要关心实际生产的具体产品是什么。通过这样的方式,客户就从所有的特定具体产品解耦。

抽象工厂为创建的产品家族提供了一个抽象,其子类定义如何生产这些产品。要使用抽象工厂,首先要实例化一个工厂,并传入一些针对抽象类型编写的代码。

抽象工厂的方法经常实现为工厂方法。抽象工厂的工作是定义一个接口,用来创建一组产品。该接口的每个方法负责创建一个具体的产品,我们实现抽象工厂的子类就是提供这些实现。因此在抽象工厂中,用工厂方法来实现生产方法是非常自然的方式。

// 1. 在(工厂方法的)抽象父类中
class PizzaStore {
    orderPizza(type){
        // 第一层工厂方法:调用工厂方法,实例化具体对象
        const pizza = createPizza(type)
        // 第二层抽象工厂:会传入第一层的具体工厂,即以对象组合的方式复用
        pizza.prepare()

        pizza.bake()
        pizza.cut()
        pizza.box()

        return pizza
    }
    createPizza(type)  // 第一层工厂方法,由子类实现
}

// 2. 在(工厂方法的)具体工厂中
class NYPizzaStore extends PizzaStore {
    // 实现第一层的工厂方法【~~继承~~】
    createPizza(type) {
        let pizza = null

        // 第二层:实例化一个(抽象工厂的)具体工厂,其父类是一组接口(产品家族)【~~组合~~】
        const ingredientFacory = new NYIngredientFacory()        
        
        // 第二层:在实例化具体类时,把工厂传参进去
        if (type === 'cheese') {
            pizza = new CheesePizza(ingredientFacory)
            pizza.setName('New York Style Cheese Pizza')
        } else if (type === 'veggie') {
            pizza = new VeggiePizza(ingredientFacory)
            pizza.setName('New York Style Veggie Pizza')
        } else if (type === 'clam') {
            pizza = new ClamPizza(ingredientFacory)
            pizza.setName('New York Style Clam Pizza')
        } else if (type === 'pepperoni') {
            pizza = new PepperoniPizza(ingredientFacory)
            pizza.setName('New York Style Pepperoni Pizza')
        }
        return pizza
    }
}
// 3. 在具体类的代码里
class CheesePizza extends Pizza {

    constructor(ingredientFacory) {
        // 获取参数,设置本地工厂【~~组合~~】
        this.ingredientFacory = ingredientFacory
    }

    // 工厂方法:实现父类中的抽象方法【~~继承~~】
    prepare() {
        // 用组合来的本地工厂,生产具体的原料
        dough = this.ingredientFacory.createDough()
        sauce = this.ingredientFacory.createSauce()
        cheese = this.ingredientFacory.createCheese()
        clam = this.ingredientFacory.createClam()
    }
}
// 最终的使用:和工厂方法的代码一样
nyStore = new NYPizzaStore()
pizza = nyStore.orderPizza('cheese')

思考:所以工厂方法和抽象工厂的最本质的区别,到底工厂方法的接口个数,还是复用的手段(继承 or 组合)?应该是后者,也就是说抽象工厂的核心是“对象组合”。那么从产品需求上来讲,是先确认一个类别(第一层工厂方法是用继承),再确认另一个类别(第二层是组合+工厂方法(里面调一组接口))。

所以,抽象工厂的核心代码,如下:

const nyIngredientFacory = new NYIngredientFacory()  // 实例化抽象工厂的具体工厂
const pizza = new CheesePizza(nyIngredientFacory)    // 实例化具体产品

// 在具体产品的内部:接收参数,进行组合,用具体工厂生产
class CheesePizza extends Pizza {
    constructor(ingredientFacory) {
        this.ingredientFacory = ingredientFacory
    }
    prepare() {
        dough = this.ingredientFacory.createDough()
        sauce = this.ingredientFacory.createSauce()
        cheese = this.ingredientFacory.createCheese()
        clam = this.ingredientFacory.createClam()
    }
}

// 这里,抽象工厂是用“原料工厂”区分了不同的 style
// 之前,工厂方法是直接用“子类”区分了不同的 style

3.3 对比工厂方法

工厂模式
创建对象
说明

工厂方法

通过类继承

对象创建被委托给了子类 子类实现工厂方法来创建对象

抽象工厂

通过对象组合

对象创建是在工厂接口暴露的方法中

工厂模式
接口
说明

工厂方法

简单

只创建一个产品,因此只需要一个方法

抽象工厂

大而全

需创建多个产品(产品家族),因此需要大而全的接口

适合接口明确且稳定

当要扩展相关产品时(比如添加新产品)就要修改接口

工厂模式
意图

工厂方法

允许一个类延迟实例化到其子类(使用子类来创建对象)

抽象工厂

创建相关对象家族,不必依赖于其具体类

4. 写在最后

模式会让设计更灵活,但未必会更小。

通常情况,设计会从使用工厂方法开始。当设计者发现需要更大的灵活性时,便会向其它创建型模式(抽象工厂、建造者、原型)演化,因为它们三个比工厂方法更灵活,但也更复杂。当在设计标准之间进行权衡的时候,了解多个模式可以有更多的选择余地。

Last updated