工厂方法(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 Dialogmethod createButton():Buttonreturn new WindowsButton()class WebDialog extends Dialogmethod createButton():Buttonreturn new HTMLButton()// 产品接口声明所有具体产品必须实现的操作interface Buttonmethod render()method onClick(f)// 具体产品提供产品接口的各种实现class WindowsButton implements Buttonmethod render(a, b)method onClick(f)class HTMLButton implements Buttonmethod render(a, b)method onClick(f)class Applicationfield dialog: Dialog// 应用根据当前配置或者环境变量设置,选择创建者类型method initialize()config = readApplicationConfigFile()if (config.OS == "Windows") thendialog = new WindowsDialog()else if (config.OS == "Web") thendialog = new WebDialog()elsethrow new Exception("Error! Unknown operating system.")// 客户端代码通过创建者基类接口工作,可以使用任何创建者子类。method main() isthis.initialize()dialog.render()
6 应用
- 事先不知道代码需要的对象的精确类型和依赖时,可以使用工厂方法。
- 工厂方法分离了产品构建代码和使用代码的代码,从而可以更容易地扩展产品构建代码,独立于产品的其他部分。
- 比如说,增加新产品类型时,只需要创建新的创建者子类,重写其工厂方法。
- 需要以扩展内部组件的方式向用户提供库或者框架时,可以使用工厂方法。
- 继承可能是扩展库或者框架默认行为的最简单方法,但是,框架如何识别应该使用哪个子类来代替标准组件?本模式将框架中构建组件的代码缩减到单个工厂方法中,除了扩展组件本身之外,任何人可以重写这个方法。
- 一个例子:要写一个使用开源UI框架的应用,需要一个圆形按钮,但是框架只提供方形按钮。你将标准的
Button类扩展成RoundButton类,然而,你如何告诉UIFramework类使用新的RoundButton来代替默认的Button?- 为实现这个目标,需要用
UIWithRoundButtons类扩展UIFramework类,重写createButton()方法:基类中,这个方法返回Button对象;而子类中,这个方法返回RoundButton对象。 - 然后用
UIWithRoundButtons代替UIFramework。
- 为实现这个目标,需要用
- 需要重用已有对象,而不是每次都新建对象,从而节省系统资源时,可以使用工厂方法。
- 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
- 在创建类中添加空的工厂方法,其返回类型必须是通用的产品接口。
- 在创建者代码中找到对产品构造函数的所有引用,将它们依次替换为对工厂方法的调用;然后将创建产品的代码移入工厂方法。可能需要在工厂方法中添加参数来控制返回的产品类型。
工厂方法的代码看上去可能非常糟糕:其中可能会有复杂的switch分支,用于选择各种具体的产品类。但是不要担心,我们很快就会修复这个问题。 - 为每种产品编写一个创建者子类,在子类中重写工厂方法,将基类工厂方法中的创建代码移动到子类的工厂方法中。
- 如果产品类型太多,那么为每个产品创建子类并无太大必要,这时可以在子类中复用基类的控制参数。例如, 设想有以下一些层次结构的类。
- 基类
Mail及其子类AirMail和GroundMail Transport及其子类Plane,Trunk和TrainAirMail仅使用Plain对象- 而
GroundMail则会同时使用Trunk和Train对象。 - 可以编写新的子类 (例如
TrainMail) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给GroundMail类传递一个参数, 用于控制其希望获得的产品。
- 基类
- 如果代码经过上述移动后,基础工厂方法中已经没有任何代码,则可以将其转变为抽象类。 如果基础工厂方法中还有其他语句,你可以将其设置为该方法的默认行为。
8 优缺点
- 可以避免创建者和具体产品之间的紧密耦合
- 单一职责原则:将产品创建代码放在程序的单一位置,让代码更容易维护
- 开闭原则:无需更改现有客户端代码,就可以在程序中引入新的产品类型
工厂方法模式需要引入许多新的子类,代码可能会变得更复杂。最好的情况是,将本模式引入创建者类的现有层次结构中。
9 与其他模式的关系
- 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式。
- 可以同时使用工厂方法和迭代器模式,让子类集合返回不同类型的迭代器,让迭代器与集合匹配。
- 原型模式不基于继承,没有继承的缺点。然而,原型需要对被复制对象进行复杂的初始化(如设置对象的各个字段)。工厂方法基于继承,不需要初始化步骤(简单地使用
new就可以了)。 - 工厂方法模式是模板方法模式的一种特殊形式,可以作为大型模板方法中的一个步骤。
10 代码示例
10.1 产品接口
package maintype iGun interface {setName(name string)setPower(power int)getName() stringgetPower() int}
10.2 具体产品
10.2.1 默认产品
package maintype gun struct {name stringpower 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 maintype ak47 struct {gun}func newAk47() iGun {return &ak47{gun: gun{name: "AK47 gun",power: 4,},}}
10.2.3 musket
package maintype musket struct {gun}func newMusket() iGun {return &musket{gun: gun{name: "Musket gun",power: 1,},}}
10.3 工厂
package mainimport "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 mainimport "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()}
