1 意图

生成器模式是一种创建型设计模式,使你能够分步骤创建复杂对象,允许你使用相同的创建代码生成不同类型和形式的对象。

builder-zh.png

2 问题

假设有一个复杂对象,对其进行构造时需要对诸多成员变量和嵌套对象进行繁杂的初始化工作。这些初始化代码通常深藏于一个包含众多参数的,让人基本看不懂的构造函数中;甚至还有更糟糕的情况,那就是这些代码散落在客户端的多个位置。

problem1.png

例如,来思考如何创建一个房屋(House)对象。建造简单的房屋,需要四面墙和地板,要安装房门和窗户,然后要建造屋顶。然而,如果需要更宽敞、更明亮的房屋,要有院子和其他设施(暖气、排水和供电设备等),又该怎么办?

最简单的方法是扩展房屋基类,创建一系列涵盖所有参数组合的子类。最终你将面对相当数量的子类。任何新增的参数(例如门廊类型)都会让这个层次结构更加复杂。

另一种方法则无需生成子类。可以在房屋基类中创建一个包括所有可能参数的超级构造函数,用它来控制房屋对象。这种方法可以避免生成子类,但是会造成另外一个问题:这些参数不是每次都要全部用上。

problem2.png

通常情况下,绝大部分参数都没有用到,这使得对构造函数的调用十分不简洁。例如,只有很少的房子有游泳池,因此与游泳池相关的参数十之八九是用不到的。

3 解决方案

生成器模式建议将对象构造代码从产品类中抽取出来,放到一个名为生成器的独立对象中。

solution1.png

生成器模式会将对象构造过程划分成一组步骤,比如buildWallsbuildDoor等。每次创建对象时,都需要通过生成器对象执行一系列步骤。重点在于:无需调用所有步骤,只需要调用创建特定对象所需要的步骤。

需要创建不同形式的产品时,其中一些构造步骤可能需要不同的实现。例如,木屋的房门需要使用木头制造,而城堡的房门必须使用石头制造。

这种情况下,可以创建多个不同的生成器,用不同方式实现一组相同的创建步骤。然后在创建过程中使用这些生成器(例如按顺序调用多个构造步骤)来生成不同类型的对象。例如:

  • 第一个建造者使用木头和玻璃制造房屋,得到一栋普通房屋。
  • 第二个建造者使用石头和钢铁,得到一座城堡。
  • 第三个建造者使用黄金和钻石,得到一座宫殿。

客户端代码必须能够通过通用接口与建造者交互,才可以得到需要的房屋。

3.1 主管

可以进一步将创建产品的一系列生成器步骤调用,抽取成为单独的主管类。主管类可以定义创建步骤的执行次序,而生成器则提供这些步骤的实现。

builder-comic-2-zh.png

不一定需要主管类:客户端代码可以直接以特定次序调用创建步骤。不过,主管类中可以放入各种例行构造流程,以便在程序中反复使用。

4 生成器模式结构

structure.png

  1. 生成器(builder) 接口声明所有类型生成器中通用的产品构造步骤。
  2. 具体生成器(concrete builders) 提供构造过程的不同实现,也可以构造不遵循通用接口的产品。
  3. 产品(products) 是最终生成的对象。由不同生成器构造的产品无需属于同一类层次结构或者接口。
  4. 主管(director) 类定义调用构造步骤的顺序,这样就可以创建和复用特定的产品配置。
  5. 客户端(client) 必须将某个生成器对象与主管类关联。
    • 一般情况下,只需要通过主管类构造函数的参数进行一次关联即可。此后主管类就能使用生成器对象完成后续所有的构造任务。
    • 也可以在使用主管类生产产品时,每次都使用不同的生成器。

5 伪代码

example-zh.png

汽车是一个复杂对象,有数百种不同的制造方法。我们没有在汽车类中塞入一个巨型构造函数,而是将汽车组装代码抽取到单独的汽车生成器类中,这个类有一组方法,可以用来配置汽车的各种部件。

客户端可以直接调用生成器,生成与众不同、精心调校的汽车,也可以将组装工作委托给主管类,让主管类使用生成器制造最受欢迎的几种汽车。

每辆汽车都需要使用手册。手册会介绍汽车的每一项功能,因此,可以复用现有制造汽车的流程来编写手册:把制造部件的过程,换成对部件的描述就可以了。

最后需要获取制造结果。汽车和手册存在关联,但却是完全不同的东西。

  1. // 只有当产品较为复杂且需要详细配置时,使用生成器模式才有意义。下面的两个
  2. // 产品尽管没有同样的接口,但却相互关联。
  3. class Car is
  4. // 一辆汽车可能配备有 GPS 设备、行车电脑和几个座位。不同型号的汽车(
  5. // 运动型轿车、SUV 和敞篷车)可能会安装或启用不同的功能。
  6. class Manual is
  7. // 用户使用手册应该根据汽车配置进行编制,并介绍汽车的所有功能。
  8. // 生成器接口声明了创建产品对象不同部件的方法。
  9. interface Builder is
  10. method reset()
  11. method setSeats(...)
  12. method setEngine(...)
  13. method setTripComputer(...)
  14. method setGPS(...)
  15. // 具体生成器类将遵循生成器接口并提供生成步骤的具体实现。你的程序中可能会
  16. // 有多个以不同方式实现的生成器变体。
  17. class CarBuilder implements Builder is
  18. private field car:Car
  19. // 一个新的生成器实例必须包含一个在后续组装过程中使用的空产品对象。
  20. constructor CarBuilder() is
  21. this.reset()
  22. // reset(重置)方法可清除正在生成的对象。
  23. method reset() is
  24. this.car = new Car()
  25. // 所有生成步骤都会与同一个产品实例进行交互。
  26. method setSeats(...) is
  27. // 设置汽车座位的数量。
  28. method setEngine(...) is
  29. // 安装指定的引擎。
  30. method setTripComputer(...) is
  31. // 安装行车电脑。
  32. method setGPS(...) is
  33. // 安装全球定位系统。
  34. // 具体生成器需要自行提供获取结果的方法。这是因为不同类型的生成器可能
  35. // 会创建不遵循相同接口的、完全不同的产品。所以也就无法在生成器接口中
  36. // 声明这些方法(至少在静态类型的编程语言中是这样的)。
  37. //
  38. // 通常在生成器实例将结果返回给客户端后,它们应该做好生成另一个产品的
  39. // 准备。因此生成器实例通常会在 `getProduct(获取产品)`方法主体末尾
  40. // 调用重置方法。但是该行为并不是必需的,你也可让生成器等待客户端明确
  41. // 调用重置方法后再去处理之前的结果。
  42. method getProduct():Car is
  43. product = this.car
  44. this.reset()
  45. return product
  46. // 生成器与其他创建型模式的不同之处在于:它让你能创建不遵循相同接口的产品。
  47. class CarManualBuilder implements Builder is
  48. private field manual:Manual
  49. constructor CarManualBuilder() is
  50. this.reset()
  51. method reset() is
  52. this.manual = new Manual()
  53. method setSeats(...) is
  54. // 添加关于汽车座椅功能的文档。
  55. method setEngine(...) is
  56. // 添加关于引擎的介绍。
  57. method setTripComputer(...) is
  58. // 添加关于行车电脑的介绍。
  59. method setGPS(...) is
  60. // 添加关于 GPS 的介绍。
  61. method getProduct():Manual is
  62. // 返回使用手册并重置生成器。
  63. // 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
  64. // 会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并
  65. // 不是必需的。
  66. class Director is
  67. private field builder:Builder
  68. // 主管可同由客户端代码传递给自身的任何生成器实例进行交互。客户端可通
  69. // 过这种方式改变最新组装完毕的产品的最终类型。
  70. method setBuilder(builder:Builder)
  71. this.builder = builder
  72. // 主管可使用同样的生成步骤创建多个产品变体。
  73. method constructSportsCar(builder: Builder) is
  74. builder.reset()
  75. builder.setSeats(2)
  76. builder.setEngine(new SportEngine())
  77. builder.setTripComputer(true)
  78. builder.setGPS(true)
  79. method constructSUV(builder: Builder) is
  80. // ...
  81. // 客户端代码会创建生成器对象并将其传递给主管,然后执行构造过程。最终结果
  82. // 将需要从生成器对象中获取。
  83. class Application is
  84. method makeCar() is
  85. director = new Director()
  86. CarBuilder builder = new CarBuilder()
  87. director.constructSportsCar(builder)
  88. Car car = builder.getProduct()
  89. CarManualBuilder builder = new CarManualBuilder()
  90. director.constructSportsCar(builder)
  91. // 最终产品通常需要从生成器对象中获取,因为主管不知晓具体生成器和
  92. // 产品的存在,也不会对其产生依赖。
  93. Manual manual = builder.getProduct()

6 应用场景

  • 生成器模式可以避免“重叠构造函数”的出现
    假设构造函数有十个可选参数,则调用该函数会非常不方便。因此,需要重载这个构造函数,新建几个只有较少参数的简化版。但这个构造函数仍然需要调用主构造函数,传递一些默认值来代替省略掉的参数。java class Pizza { Pizza(int size) { ... } Pizza(int size, boolean cheese) { ... } Pizza(int size, boolean cheese, boolean pepperoni) { ... }
    而生成器模式让你可以分步骤生成对象,而且允许你仅使用必须的步骤。应用该模式后,再也不需要将几十个参数塞进构造函数里了。
  • 希望使用代码创建不同形式的产品(例如石头或者木头房屋)时,可以使用生成器模式。
    • 如果需要创建各种形式的产品,其制造过程相似,仅有细节上的差异,可以使用生成器模式。
    • 基本生成器接口定义了所有可能的制造步骤,具体生成器将实现这些步骤来制造特定形式的产品。同时,主管类将负责管理制造步骤的顺序。
  • 使用生成器构造组合,或者其他复杂对象。
    • 生成器模式让你能分步骤构造产品。可以延迟执行某些步骤,而不会影响最终产品。甚至可以递归调用这些步骤,这在创建对象树时非常方便。
    • 生成器在执行制造步骤时,不能对外发布未完成的产品,必须避免客户端代码获取到不完整结果对象的情况。

7 实现方法

  1. 清晰地定义通用步骤,确保可以制造所有形式的产品,否则将无法进一步实施该模式。
  2. 在基本生成器接口中声明这些步骤。
  3. 为每个形式的产品创建具体生成器类,实现其构造步骤。
    不要忘记实现获取构造结果对象的方法:不能在生成器接口中声明该方法,因为不同生成器构造的产品可能没有公共接口,不知道该方法返回的对象类型。但是,如果所有产品都位于单一类层次中,就可以安全地在基本接口中添加获取生成对象的方法。
  4. 考虑创建主管类,可以使用同一生成器对象来封装多种构造产品的方式。
  5. 客户端代码会同时创建生成器和主管对象。构造开始前,客户端必须将生成器对象传递给主管对象。
    • 通常情况下,客户端只需要调用主管类构造函数一次即可。主管类使用生成器对象完成后续所有制造任务。
    • 也可以在调用主管类制造方法时传入生成器对象。
  6. 只有所有产品遵循相同接口的情况下,才可以通过主管类获取构造结果。否则,客户端应当通过生成器获取构造结果。

8 优缺点

  • 可以分步创建对象,暂缓创建步骤,或者递归运行创建步骤。
  • 生成不同形式的产品时,可以复用相同的制造代码。
  • 单一职责原则:可以将复杂的构造代码从产品的业务逻辑中分离出来。
  • 该模式需要新增多个类,代码整体复杂度会增加。

9 与其他模式的关系

  • 许多设计工作初期使用工厂方法模式,随后演化成使用抽象工厂模式原型模式生成器模式
  • 生成器模式重点关注如何分步生成复杂对象,而抽象工厂专门用于生产一系列相关对象。抽象工厂会马上返回产品,生成器允许在获取产品前执行一些额外构造步骤。
  • 可以在创建复杂组合模式树时使用生成器,因为可以使构造步骤以递归的方式运行。
  • 可以结合使用生成器和桥接模式:主管类负责抽象工作,各种不同的生成器负责实现工作。
  • 抽象工厂、生成器、原型模式都可以用单例模式来实现。

10 代码示例

04 创建型模式3:生成器 - 图8

10.1 生成器接口

  1. package main
  2. type iBuilder interface {
  3. setWindowType()
  4. setDoorType()
  5. setNumFloor()
  6. getHouse() house
  7. }
  8. func getBuilder(builderType string) iBuilder {
  9. if builderType == "normal" {
  10. return &normalBuilder{}
  11. }
  12. if builderType == "igloo" {
  13. return &iglooBuilder{}
  14. }
  15. return nil
  16. }

10.2 具体生成器:普通房屋

package main

type normalBuilder struct {
    windowType string
    doorType   string
    floor      int
}

func newNormalBuilder() *normalBuilder {
    return &normalBuilder{}
}

func (b *normalBuilder) setWindowType() {
    b.windowType = "Wooden Window"
}

func (b *normalBuilder) setDoorType() {
    b.doorType = "Wooden Door"
}

func (b *normalBuilder) setNumFloor() {
    b.floor = 2
}

func (b *normalBuilder) getHouse() house {
    return house{
        doorType:   b.doorType,
        windowType: b.windowType,
        floor:      b.floor,
    }
}

10.3 具体生成器:冰屋

package main

type iglooBuilder struct {
    windowType string
    doorType   string
    floor      int
}

func newIglooBuilder() *iglooBuilder {
    return &iglooBuilder{}
}

func (b *iglooBuilder) setWindowType() {
    b.windowType = "Snow Window"
}

func (b *iglooBuilder) setDoorType() {
    b.doorType = "Snow Door"
}

func (b *iglooBuilder) setNumFloor() {
    b.floor = 1
}

func (b *iglooBuilder) getHouse() house {
    return house{
        doorType:   b.doorType,
        windowType: b.windowType,
        floor:      b.floor,
    }
}

10.4 产品

package main

type house struct {
    windowType string
    doorType   string
    floor      int
}

10.5 主管

package main

type director struct {
    builder iBuilder
}

func newDirector(b iBuilder) *director {
    return &director{
        builder: b,
    }
}

func (d *director) setBuilder(b iBuilder) {
    d.builder = b
}

func (d *director) buildHouse() house {
    d.builder.setDoorType()
    d.builder.setWindowType()
    d.builder.setNumFloor()
    return d.builder.getHouse()
}

10.6 客户端

package main

import "fmt"

func main() {
    normalBuilder := getBuilder("normal")
    iglooBuilder := getBuilder("igloo")

    director := newDirector(normalBuilder)
    normalHouse := director.buildHouse()

    fmt.Printf("Normal House Door Type: %s\n", normalHouse.doorType)
    fmt.Printf("Normal House Window Type: %s\n", normalHouse.windowType)
    fmt.Printf("Normal House Num Floor: %d\n", normalHouse.floor)

    director.setBuilder(iglooBuilder)
    iglooHouse := director.buildHouse()

    fmt.Printf("\nIgloo House Door Type: %s\n", iglooHouse.doorType)
    fmt.Printf("Igloo House Window Type: %s\n", iglooHouse.windowType)
    fmt.Printf("Igloo House Num Floor: %d\n", iglooHouse.floor)

}