每次使用 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 使用场景
当创建逻辑比较复杂,是一个大工程的时候,就考虑用工厂模式来封装对象的创建过程,将对象的创建和使用相分离。
那么,“创建逻辑比较复杂”是指哪些情况呢?有以下两种:
代码中存在 if-else
分支判断(比如规则配置解析)动态地根据不同类型来创建不同的对象,此时就考虑使用工厂模式,将这一大坨 if-else
创建对象的代码抽离出来,放到工厂类中。
当每个对象的创建逻辑都比较简单时,推荐使用简单工厂
当每个对象的创建逻辑都比较复杂时,推荐使用工厂方法
尽管不需要根据不同的类型来创建不同的对象,但是单个对象本身的创建过程比较复杂(比如还要组合其它类对象,做各种初始化操作),此时也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。因为单个对象本身的创建逻辑就比较复杂,所以建议使用工厂方法模式。
除了上面提到的这两种情况外,如果创建对象的逻辑并不复杂,那可以直接通过 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. 写在最后
模式会让设计更灵活,但未必会更小。
通常情况,设计会从使用工厂方法开始。当设计者发现需要更大的灵活性时,便会向其它创建型模式(抽象工厂、建造者、原型)演化,因为它们三个比工厂方法更灵活,但也更复杂。当在设计标准之间进行权衡的时候,了解多个模式可以有更多的选择余地。