工厂方法(factory method)模式,也称作虚拟构造者(virtual constructor)模式。
1 意图
工厂方法在超类中提供创建对象的接口,但是允许子类修改将创建的对象类型。
2 问题
假设要创建一个物流管理应用。第一版的应用只能用卡车进行运输,所以大量代码在Trunk
类中。应用变得流行之后,每天收到很多海运订单。该如何修改代码?大部分代码是与Trunk
类耦合的,增加Ship
到应用中需要修改大量代码。而且,如果以后要增加新的运输类型,还得进行一次这样的大量代码修改。
结果,代码会变得很烂,充斥着根据运输对象类型切换应用行为的条件判断。
3 解决方案
工厂方法模式用特殊的工厂方法调用,取代直接的对象构建(new
)。对象还是用new
创建的,但是是在工厂方法中调用new
的。工厂方法返回的对象通常称作产品(product)。
初看起来,这个改动没什么用:只是把对构造函数的调用换了个地方。然而,可以在工厂子类中重写工厂方法,修改方法创建的产品类型。
当然,有个限制:子类可以返回不同的产品类型,但是所有产品必须有公共的基类或者接口。而基类的工厂方法应该声明将返回这个公共的基类或者接口。
本例中
- 产品(
Trunk
和Ship
)要实现的公共接口是Transport
,这个接口含有方法deliver()
- 工厂基类
Logistics
含有工厂方法createTransport()
- 工厂子类
RoadLogistics
的工厂方法返回Trunk
类实例 - 工厂子类
SeaLogistics
的工厂方法返回Ship
子类
- 工厂子类
- 使用工厂方法的客户端代码,不知道各种子类返回的实际产品的差别。客户端把所有产品当成抽象的
Transport
,知道它有deliver()
方法,但是不知道如何实现运输的细节。4 结构
产品(Product)
:创建者(工厂)及其子类(子工厂)可以生产的所有对象,都必须实现的接口。具体产品(Concrete Product)
:产品接口的不同实现。创建者(Creator)
:声明将返回新产品对象的工厂方法。工厂方法的返回类型,必须与产品接口匹配。- 可以将工厂方法声明为抽象的,强制所有子类实现自有版本的工厂方法。
- 当然,工厂方法也可以返回某些默认的产品类型。
- 注意:虽然名字是创建者,但是创建产品不是其主要职责。创建者类通常含有一些与产品相关的核心业务逻辑。工厂方法让这些逻辑与具体的产品类型解耦。打个比方:大的软件开发公司可以有一个程序员培训部门,但从总体上说,公司的主要功能是写代码,而不是生产程序员。
- 可以将工厂方法声明为抽象的,强制所有子类实现自有版本的工厂方法。
具体创建者(Concrete Creators)
:重写基础工厂方法,返回不同的产品类型。工厂方法不一定总是创建新的对象,也可以返回缓存、对象池,或者其他来源中已有的对象。
5 伪代码
// 创建者类声明返回产品类的工厂方法,通常由创建者的子类实现工厂方法。
class Dialog
// 创建者可以为工厂方法提供某些默认实现。
abstract method createButton():Button
// 注意:创建者的主要职责不是创建产品。创建者通常含有某些与产品相关的核心业务逻辑。
// 子类返回不同类型的产品,就可以间接地修改这些业务逻辑。
method render()
// 调用工厂方法创建一个产品对象
Button okButton = createButton()
// 然后使用产品
okButton.onClick(closeDialog)
okButton.render()
// 具体创建者重写工厂方法,修改返回的产品类型。
class WindowsDialog extends Dialog
method createButton():Button
return new WindowsButton()
class WebDialog extends Dialog
method createButton():Button
return new HTMLButton()
// 产品接口声明所有具体产品必须实现的操作
interface Button
method render()
method onClick(f)
// 具体产品提供产品接口的各种实现
class WindowsButton implements Button
method render(a, b)
method onClick(f)
class HTMLButton implements Button
method render(a, b)
method onClick(f)
class Application
field dialog: Dialog
// 应用根据当前配置或者环境变量设置,选择创建者类型
method initialize()
config = readApplicationConfigFile()
if (config.OS == "Windows") then
dialog = new WindowsDialog()
else if (config.OS == "Web") then
dialog = new WebDialog()
else
throw new Exception("Error! Unknown operating system.")
// 客户端代码通过创建者基类接口工作,可以使用任何创建者子类。
method main() is
this.initialize()
dialog.render()
6 应用
- 事先不知道代码需要的对象的精确类型和依赖时,可以使用工厂方法。
- 工厂方法分离了产品构建代码和使用代码的代码,从而可以更容易地扩展产品构建代码,独立于产品的其他部分。
- 比如说,增加新产品类型时,只需要创建新的创建者子类,重写其工厂方法。
- 需要以扩展内部组件的方式向用户提供库或者框架时,可以使用工厂方法。
- 继承可能是扩展库或者框架默认行为的最简单方法,但是,框架如何识别应该使用哪个子类来代替标准组件?本模式将框架中构建组件的代码缩减到单个工厂方法中,除了扩展组件本身之外,任何人可以重写这个方法。
- 一个例子:要写一个使用开源UI框架的应用,需要一个圆形按钮,但是框架只提供方形按钮。你将标准的
Button
类扩展成RoundButton
类,然而,你如何告诉UIFramework
类使用新的RoundButton
来代替默认的Button
?- 为实现这个目标,需要用
UIWithRoundButtons
类扩展UIFramework
类,重写createButton()
方法:基类中,这个方法返回Button
对象;而子类中,这个方法返回RoundButton
对象。 - 然后用
UIWithRoundButtons
代替UIFramework
。
- 为实现这个目标,需要用
- 需要重用已有对象,而不是每次都新建对象,从而节省系统资源时,可以使用工厂方法。
- 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
- 在创建类中添加空的工厂方法,其返回类型必须是通用的产品接口。
- 在创建者代码中找到对产品构造函数的所有引用,将它们依次替换为对工厂方法的调用;然后将创建产品的代码移入工厂方法。可能需要在工厂方法中添加参数来控制返回的产品类型。
工厂方法的代码看上去可能非常糟糕:其中可能会有复杂的switch
分支,用于选择各种具体的产品类。但是不要担心,我们很快就会修复这个问题。 - 为每种产品编写一个创建者子类,在子类中重写工厂方法,将基类工厂方法中的创建代码移动到子类的工厂方法中。
- 如果产品类型太多,那么为每个产品创建子类并无太大必要,这时可以在子类中复用基类的控制参数。例如, 设想有以下一些层次结构的类。
- 基类
Mail
及其子类AirMail
和GroundMail
Transport
及其子类Plane
,Trunk
和Train
AirMail
仅使用Plain
对象- 而
GroundMail
则会同时使用Trunk
和Train
对象。 - 可以编写新的子类 (例如
TrainMail
) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给GroundMail
类传递一个参数, 用于控制其希望获得的产品。
- 基类
- 如果代码经过上述移动后,基础工厂方法中已经没有任何代码,则可以将其转变为抽象类。 如果基础工厂方法中还有其他语句,你可以将其设置为该方法的默认行为。
8 优缺点
- 可以避免创建者和具体产品之间的紧密耦合
- 单一职责原则:将产品创建代码放在程序的单一位置,让代码更容易维护
- 开闭原则:无需更改现有客户端代码,就可以在程序中引入新的产品类型
工厂方法模式需要引入许多新的子类,代码可能会变得更复杂。最好的情况是,将本模式引入创建者类的现有层次结构中。
9 与其他模式的关系
- 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式。
- 可以同时使用工厂方法和迭代器模式,让子类集合返回不同类型的迭代器,让迭代器与集合匹配。
- 原型模式不基于继承,没有继承的缺点。然而,原型需要对被复制对象进行复杂的初始化(如设置对象的各个字段)。工厂方法基于继承,不需要初始化步骤(简单地使用
new
就可以了)。 - 工厂方法模式是模板方法模式的一种特殊形式,可以作为大型模板方法中的一个步骤。
10 代码示例
10.1 产品接口
package main
type iGun interface {
setName(name string)
setPower(power int)
getName() string
getPower() int
}
10.2 具体产品
10.2.1 默认产品
package main
type gun struct {
name string
power int
}
func (g *gun) setName(name string) {
g.name = name
}
func (g *gun) getName() string {
return g.name
}
func (g *gun) setPower(power int) {
g.power = power
}
func (g *gun) getPower() int {
return g.power
}
10.2.2 AK47
package main
type ak47 struct {
gun
}
func newAk47() iGun {
return &ak47{
gun: gun{
name: "AK47 gun",
power: 4,
},
}
}
10.2.3 musket
package main
type musket struct {
gun
}
func newMusket() iGun {
return &musket{
gun: gun{
name: "Musket gun",
power: 1,
},
}
}
10.3 工厂
package main
import "fmt"
func getGun(gunType string) (iGun, error) {
if gunType == "ak47" {
return newAk47(), nil
}
if gunType == "musket" {
return newMusket(), nil
}
return nil, fmt.Errorf("Wrong gun type passed")
}
10.4 客户端
package main
import "fmt"
func main() {
ak47, _ := getGun("ak47")
musket, _ := getGun("musket")
printDetails(ak47)
printDetails(musket)
}
func printDetails(g iGun) {
fmt.Printf("Gun: %s", g.getName())
fmt.Println()
fmt.Printf("Power: %d", g.getPower())
fmt.Println()
}