亦称:生成器模式,builder

产品的构造需要繁杂的参数或者步骤,建造者模式建议将对象构造代码从产品类中抽取出来,并将这些步骤放在一个名为建造者的独立对象中。

将一个复杂对象的构建过程与对象最终的表示分离,使得同样的构建过程可以创建出不同的表示。

主要用于对象建造过程比较复杂的情况,“表示"可以理解为对象的属性。

介绍

角色组成:

  • builder接口
    • 声明在所有类型的生成器中通用的产品建造步骤。
  • 具体builder
    • 提供建造过程的具体实现。如果各个builder生成的产品不属于同一个产品接口,则builder应提供getResult方法返回该生成器创建的产品。
  • 产品Product
    • 最终生成的产品对象,可以不实现同一个产品接口。
  • 主管director
    • 按顺序执行建造步骤。如果各个产品实现了相同的产品接口,可以在director中统一获取建造结果(即产品接口的实例),不再在具体builder中获取。
  • 客户端client
    • 将某个builder对象与director关联,此后director调用builder对象的方法完成后续的建造任务。

建造者模式

Builder的作用是建造产品,但是建造产品的过程太复杂,所以Builder中有多个BuildPartX()用于创建产品的部分元素,通过GetResult()获取最终的对象。

BuildPartX()方法数量较多,不能直接让客户端去调用创建。所以有一个Director,它会利用Builder中的众多BuildPartX()方法,将产品组装起来。 并通过Builder的GetResult()方法获取到组装好的产品返回给客户端,客户端无需知道组装的细节。

当需要创建不同形式的产品时,其中的一些建造步骤可能需要不同的实现。

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

可以将用于创建产品的一系列建造步骤调用抽取成为单独的director类。 director类定义创建步骤的执行顺序, 而builder则提供这些步骤的实现。

对于客户端代码来说,director类完全隐藏了产品建造细节。 客户端只需要将一个builder与director类关联,然后使用director来建造产品, 就能从builder处获得建造结果了。

适用场景

  • 构造函数参数众多
    • 建造者模式可以分步骤生成对象,允许你仅使用必须的步骤。应用该模式后,再也不需要将几十个参数塞进构造函数里了。
  • 希望使用类似的代码创建不同形式的产品
    • 如果需要创建的各种形式的产品,它们的制造过程相似且仅有细节上的差异,可使用建造者模式。

实现步骤

  1. 抽象通用建造步骤,确保它们可以创建所有形式的产品,在builder接口中定义这些步骤
  2. 为每个形式的产品建造过程创建对应的具体builder类,实现builder接口中的步骤
  3. 每个具体的builder类应包含获取结果产品的方法。注意,不能在builder接口中声明该方法,因为不同生成器创建的产品可能没有公共接口,除非它们有公共接口。
  4. 创建director类,调用builder上的建造步骤
  5. 所有产品都实现同一接口时,客户端可以通过director获取构造结果。否则,客户端应该通过builder获取构造结果。

基本builder接口中定义了所有可能的制造步骤,具体builder将实现这些步骤来制造特定形式的产品。 同时, director类将负责管理制造步骤的执行顺序。

优缺点

优点

  • 可以分步创建对象,暂缓创建步骤或递归运行创建步骤。
  • 生成不同形式的产品时,可以复用相同的建造代码。
  • 单一职责原则。可以将复杂的建造代码从产品的业务逻辑中分离出来。

缺点

  • 需要新增多个类,代码整体复杂程度会有所增加。

与其它模式的关系

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

例子

假设要创建一个资源池,需要设置资源名称(name)、最大总资源数量(maxTotal)、最大空闲资源数量(maxIdle)、最小空闲资源数量(minIdle)等。 其中name必填,maxTotal、maxIdle、minIdle非必填,但是填了一个其它两个也需要填,而且数据值有限制,如不能等于0,数据大小有限制,如maxIdle不能大于maxTotal。

有这样几个方案:

  • 使用一个构造函数,所有判断都在构造函数里做。但是一旦输入参数个数增多、校验逻辑复杂,函数就会变得不好维护。需求有变化时,构造函数也要改,不满足开放封闭原则
  • 如果对外提供Set方法,因为数据有限制,容易漏掉部分配置。而且有时资源是不可变对象,就不能暴露set方法
  • 使用建造者模式。建造者将输入数据准备好,调用产品的创造函数,并返回最终的产品。后面有规则上的变动,只需修改Builder即可。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ResourceProduct 产品接口
type ResourceProduct interface {
	Show()
}

// RedisResourceProduct 具体产品
type RedisResourceProduct struct {
	params ResourceParam
}

func (r *RedisResourceProduct) Show() {
	fmt.Println(r.params)
}
1
2
3
4
5
6
7
8
9
// ResourceBuilder 创建者接口
type ResourceBuilder interface {
	setName(name string) ResourceBuilder
	setMaxTotal(maxTotal int64) ResourceBuilder
	setMaxIdle(maxIdle int64) ResourceBuilder
	setMinIdle(minIdle int64) ResourceBuilder
	getError() error
	build() ResourceProduct
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
type ResourceParam struct {
	name     string
	maxTotal int64
	maxIdle  int64
	minIdle  int64
}


// RedisResourceBuilder 实际的建造者
type RedisResourceBuilder struct {
  param ResourceParam
  err   error
}

func (r *RedisResourceBuilder) setName(name string) ResourceBuilder {
  // 参数校验
  r.param.name = name
  return r
}

func (r *RedisResourceBuilder) setMaxTotal(maxTotal int64) ResourceBuilder {
  // 参数校验
  r.param.maxTotal = maxTotal
  return r
}

func (r *RedisResourceBuilder) setMaxIdle(maxIdle int64) ResourceBuilder {
  // 参数校验
  r.param.maxIdle = maxIdle
  return r
}

func (r *RedisResourceBuilder) setMinIdle(minIdle int64) ResourceBuilder {
  // 参数校验
  r.param.minIdle = minIdle
  return r
}

func (r *RedisResourceBuilder) getError() error {
  return r.err
}

func (r *RedisResourceBuilder) build() ResourceProduct {
  // 一堆参数校验
  if r.param.minIdle > r.param.maxIdle {
    r.err = errors.New("param error")
    return nil
  }
  p := &RedisResourceProduct{params: r.param}
  return p
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

// Director 指挥者
type Director struct {
}

// construct 指挥者的建造方法入参是builder的接口,依赖倒置原则
// 高层模块不要依赖底层模块。高层模块和底层模块应该通过抽象来互相依赖。
func (d *Director) construct(builder ResourceBuilder) ResourceProduct {
	p := builder.setName("xx").setMinIdle(10).build()

	err := builder.getError()
	if err != nil {
		return nil
	}
	return p
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

/*
代码使用RedisResourceBuilder对Product的参数做检查,使Product只需要关注自身的业务逻辑,比如Show。
Director负责组装,使调用方无需知道建造的细节。
*/

func main() {
	build := &RedisResourceBuilder{}
	director := &Director{}

	product := director.construct(build)
	if product == nil {
		return
	}
	product.Show()
}

总结

如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合set()方法来解决。

但是,如果存在下面情况中的任意一种,就要考虑使用建造者模式了。

  • 把类的必填属性放在构造方法中,强制创建对象的时候就设置
  • 类的属性之间有一定的依赖关系或者约束条件,需要做复杂的逻辑校验
  • 希望创建的对象不可变

References

guru