工厂方法(factory method)模式,也称作虚拟构造者(virtual constructor)模式。

1 意图

工厂方法在超类中提供创建对象的接口,但是允许子类修改将创建的对象类型。
02 创建型模式1:工厂方法 - 图1

2 问题

假设要创建一个物流管理应用。第一版的应用只能用卡车进行运输,所以大量代码在Trunk类中。应用变得流行之后,每天收到很多海运订单。该如何修改代码?大部分代码是与Trunk类耦合的,增加Ship到应用中需要修改大量代码。而且,如果以后要增加新的运输类型,还得进行一次这样的大量代码修改。
结果,代码会变得很烂,充斥着根据运输对象类型切换应用行为的条件判断。
02 创建型模式1:工厂方法 - 图2

3 解决方案

工厂方法模式用特殊的工厂方法调用,取代直接的对象构建(new)。对象还是用new创建的,但是是在工厂方法中调用new的。工厂方法返回的对象通常称作产品(product)
02 创建型模式1:工厂方法 - 图3
初看起来,这个改动没什么用:只是把对构造函数的调用换了个地方。然而,可以在工厂子类中重写工厂方法,修改方法创建的产品类型。
当然,有个限制:子类可以返回不同的产品类型,但是所有产品必须有公共的基类或者接口。而基类的工厂方法应该声明将返回这个公共的基类或者接口。
02 创建型模式1:工厂方法 - 图4
本例中

  • 产品(TrunkShip)要实现的公共接口是Transport,这个接口含有方法deliver()
  • 工厂基类Logistics含有工厂方法createTransport()
    • 工厂子类RoadLogistics的工厂方法返回Trunk类实例
    • 工厂子类SeaLogistics的工厂方法返回Ship子类
  • 使用工厂方法的客户端代码,不知道各种子类返回的实际产品的差别。客户端把所有产品当成抽象的Transport,知道它有deliver()方法,但是不知道如何实现运输的细节。

    4 结构

    02 创建型模式1:工厂方法 - 图5
  1. 产品(Product):创建者(工厂)及其子类(子工厂)可以生产的所有对象,都必须实现的接口。
  2. 具体产品(Concrete Product):产品接口的不同实现。
  3. 创建者(Creator):声明将返回新产品对象的工厂方法。工厂方法的返回类型,必须与产品接口匹配。
    • 可以将工厂方法声明为抽象的,强制所有子类实现自有版本的工厂方法。
    • 当然,工厂方法也可以返回某些默认的产品类型。
    • 注意:虽然名字是创建者,但是创建产品不是其主要职责。创建者类通常含有一些与产品相关的核心业务逻辑。工厂方法让这些逻辑与具体的产品类型解耦。打个比方:大的软件开发公司可以有一个程序员培训部门,但从总体上说,公司的主要功能是写代码,而不是生产程序员。
  4. 具体创建者(Concrete Creators):重写基础工厂方法,返回不同的产品类型。工厂方法不一定总是创建新的对象,也可以返回缓存、对象池,或者其他来源中已有的对象。

    5 伪代码

    02 创建型模式1:工厂方法 - 图6
    1. // 创建者类声明返回产品类的工厂方法,通常由创建者的子类实现工厂方法。
    2. class Dialog
    3. // 创建者可以为工厂方法提供某些默认实现。
    4. abstract method createButton():Button
    5. // 注意:创建者的主要职责不是创建产品。创建者通常含有某些与产品相关的核心业务逻辑。
    6. // 子类返回不同类型的产品,就可以间接地修改这些业务逻辑。
    7. method render()
    8. // 调用工厂方法创建一个产品对象
    9. Button okButton = createButton()
    10. // 然后使用产品
    11. okButton.onClick(closeDialog)
    12. okButton.render()
    13. // 具体创建者重写工厂方法,修改返回的产品类型。
    14. class WindowsDialog extends Dialog
    15. method createButton():Button
    16. return new WindowsButton()
    17. class WebDialog extends Dialog
    18. method createButton():Button
    19. return new HTMLButton()
    20. // 产品接口声明所有具体产品必须实现的操作
    21. interface Button
    22. method render()
    23. method onClick(f)
    24. // 具体产品提供产品接口的各种实现
    25. class WindowsButton implements Button
    26. method render(a, b)
    27. method onClick(f)
    28. class HTMLButton implements Button
    29. method render(a, b)
    30. method onClick(f)
    31. class Application
    32. field dialog: Dialog
    33. // 应用根据当前配置或者环境变量设置,选择创建者类型
    34. method initialize()
    35. config = readApplicationConfigFile()
    36. if (config.OS == "Windows") then
    37. dialog = new WindowsDialog()
    38. else if (config.OS == "Web") then
    39. dialog = new WebDialog()
    40. else
    41. throw new Exception("Error! Unknown operating system.")
    42. // 客户端代码通过创建者基类接口工作,可以使用任何创建者子类。
    43. method main() is
    44. this.initialize()
    45. dialog.render()

    6 应用

  • 事先不知道代码需要的对象的精确类型和依赖时,可以使用工厂方法。
    • 工厂方法分离了产品构建代码和使用代码的代码,从而可以更容易地扩展产品构建代码,独立于产品的其他部分。
    • 比如说,增加新产品类型时,只需要创建新的创建者子类,重写其工厂方法。
  • 需要以扩展内部组件的方式向用户提供库或者框架时,可以使用工厂方法。
    • 继承可能是扩展库或者框架默认行为的最简单方法,但是,框架如何识别应该使用哪个子类来代替标准组件?本模式将框架中构建组件的代码缩减到单个工厂方法中,除了扩展组件本身之外,任何人可以重写这个方法。
    • 一个例子:要写一个使用开源UI框架的应用,需要一个圆形按钮,但是框架只提供方形按钮。你将标准的Button类扩展成RoundButton类,然而,你如何告诉UIFramework类使用新的RoundButton来代替默认的Button
      • 为实现这个目标,需要用UIWithRoundButtons类扩展UIFramework类,重写createButton()方法:基类中,这个方法返回Button对象;而子类中,这个方法返回RoundButton对象。
      • 然后用UIWithRoundButtons代替UIFramework
  • 需要重用已有对象,而不是每次都新建对象,从而节省系统资源时,可以使用工厂方法。
    • 处理大的、资源占用多的对象(如数据库连接、文件系统、网络资源)时,经常需要这样做。
      1. 首先,需要创建对象池来跟踪所有已经创建的对象
      2. 请求获取对象时,程序应该在池中找空闲对象,返回给用户
      3. 如果找不到空闲对象,则应该创建新的对象
      • 上述处理需要大量代码,应该放到单个位置,避免代码重复。
        • 可能最明显和方便的位置是对象的构造函数。然而,从定义上来说,构造函数应该返回新的对象,而不能返回已有对象。
        • 所以,需要一个规范的方法,可以创建新对象,也可以返回已有对象。这就是工厂方法啦。

          7 如何实现

  1. 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
  2. 在创建类中添加空的工厂方法,其返回类型必须是通用的产品接口。
  3. 在创建者代码中找到对产品构造函数的所有引用,将它们依次替换为对工厂方法的调用;然后将创建产品的代码移入工厂方法。可能需要在工厂方法中添加参数来控制返回的产品类型。
    工厂方法的代码看上去可能非常糟糕:其中可能会有复杂的 switch分支,用于选择各种具体的产品类。但是不要担心,我们很快就会修复这个问题。
  4. 为每种产品编写一个创建者子类,在子类中重写工厂方法,将基类工厂方法中的创建代码移动到子类的工厂方法中。
  5. 如果产品类型太多,那么为每个产品创建子类并无太大必要,这时可以在子类中复用基类的控制参数。例如, 设想有以下一些层次结构的类。
    • 基类 Mail及其子类 AirMailGroundMail
    • Transport及其子类 Plane, TrunkTrain
    • AirMail仅使用 Plain对象
    • GroundMail则会同时使用 TrunkTrain对象。
    • 可以编写新的子类 (例如 TrainMail ) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给 GroundMail类传递一个参数, 用于控制其希望获得的产品。
  6. 如果代码经过上述移动后,基础工厂方法中已经没有任何代码,则可以将其转变为抽象类。 如果基础工厂方法中还有其他语句,你可以将其设置为该方法的默认行为。

    8 优缺点

  • 可以避免创建者和具体产品之间的紧密耦合
  • 单一职责原则:将产品创建代码放在程序的单一位置,让代码更容易维护
  • 开闭原则:无需更改现有客户端代码,就可以在程序中引入新的产品类型
  • 工厂方法模式需要引入许多新的子类,代码可能会变得更复杂。最好的情况是,将本模式引入创建者类的现有层次结构中。

    9 与其他模式的关系

  • 许多设计工作初期使用工厂方法模式,随后演化成使用抽象工厂模式原型模式生成器模式

  • 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式。
  • 可以同时使用工厂方法和迭代器模式,让子类集合返回不同类型的迭代器,让迭代器与集合匹配。
  • 原型模式不基于继承,没有继承的缺点。然而,原型需要对被复制对象进行复杂的初始化(如设置对象的各个字段)。工厂方法基于继承,不需要初始化步骤(简单地使用new就可以了)。
  • 工厂方法模式是模板方法模式的一种特殊形式,可以作为大型模板方法中的一个步骤。

    10 代码示例

    02 创建型模式1:工厂方法 - 图7

    10.1 产品接口

    1. package main
    2. type iGun interface {
    3. setName(name string)
    4. setPower(power int)
    5. getName() string
    6. getPower() int
    7. }

    10.2 具体产品

    10.2.1 默认产品

    1. package main
    2. type gun struct {
    3. name string
    4. power int
    5. }
    6. func (g *gun) setName(name string) {
    7. g.name = name
    8. }
    9. func (g *gun) getName() string {
    10. return g.name
    11. }
    12. func (g *gun) setPower(power int) {
    13. g.power = power
    14. }
    15. func (g *gun) getPower() int {
    16. return g.power
    17. }

    10.2.2 AK47

    1. package main
    2. type ak47 struct {
    3. gun
    4. }
    5. func newAk47() iGun {
    6. return &ak47{
    7. gun: gun{
    8. name: "AK47 gun",
    9. power: 4,
    10. },
    11. }
    12. }

    10.2.3 musket

    1. package main
    2. type musket struct {
    3. gun
    4. }
    5. func newMusket() iGun {
    6. return &musket{
    7. gun: gun{
    8. name: "Musket gun",
    9. power: 1,
    10. },
    11. }
    12. }

    10.3 工厂

    1. package main
    2. import "fmt"
    3. func getGun(gunType string) (iGun, error) {
    4. if gunType == "ak47" {
    5. return newAk47(), nil
    6. }
    7. if gunType == "musket" {
    8. return newMusket(), nil
    9. }
    10. return nil, fmt.Errorf("Wrong gun type passed")
    11. }

    10.4 客户端

    1. package main
    2. import "fmt"
    3. func main() {
    4. ak47, _ := getGun("ak47")
    5. musket, _ := getGun("musket")
    6. printDetails(ak47)
    7. printDetails(musket)
    8. }
    9. func printDetails(g iGun) {
    10. fmt.Printf("Gun: %s", g.getName())
    11. fmt.Println()
    12. fmt.Printf("Power: %d", g.getPower())
    13. fmt.Println()
    14. }