创建型 - 3. 建造者模式

150 阅读2分钟

Builder 模式,中文翻译为建造者模式或者构建者模式,也有人叫它生成器模式。

1. 为什么需要建造者模式?

在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。有些对象的构造比较复杂,包含很多个必选和可选参数。如果将这些参数全部放到构造函数中,那构造函数的参数列表肯定特别长,影响代码的可读性和易用性。解决方法:通过构造函数设置必填项,通过 set() 方法设置可选配置项。应对复杂的场景,还可能存在如下三个问题:

  • 如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了。
  • 除此之外,假设配置项之间有一定的依赖关系,例如,用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了。
  • 如果我们希望对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在类中暴露 set() 方法。
const (
   // default value
   defaultMaxTotal = 8
   defaultMaxIdle  = 8
   defaultMinIdle  = 2
)

type ResourcePoolConfig struct {
   name     string // required
   maxTotal int    // optional
   maxIdle  int    // optional
   minIdle  int    // optional
}

func NewResourcePoolConfig(name string) ResourcePoolConfig {
   if name == "" {
      panic("empty name")
   }
   return ResourcePoolConfig{
      name:     name,
      maxTotal: defaultMaxTotal,
      maxIdle:  defaultMaxIdle,
      minIdle:  defaultMinIdle,
   }
}

func (r *ResourcePoolConfig) SetMaxTotal(maxTotal int) {
   if maxTotal <= 0 {
      panic("")
   }
   r.maxTotal = maxTotal
}

func (r *ResourcePoolConfig) SetMaxIdle(maxIdle int) {
   if maxIdle <= 0 {
      panic("")
   }
   r.maxIdle = maxIdle
}

func (r *ResourcePoolConfig) SetMinIdle(minIdle int) {
   if minIdle < 0 {
      panic("")
   }
   r.minIdle = minIdle
}


// 客户端使用
func TestNewResourcePoolConfig(t *testing.T) {
   config := NewResourcePoolConfig("test")
   config.SetMaxTotal(10)
   config.SetMaxIdle(8)
   config.SetMinIdle(2)
}

2. 建造者模式的实现

可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。

func NewResourcePoolConfigByBuilder(builder *ConfigBuilder) ResourcePoolConfig {
   return ResourcePoolConfig{
      name:     builder.name,
      maxTotal: builder.maxTotal,
      maxIdle:  builder.maxIdle,
      minIdle:  builder.minIdle,
   }
}

type ConfigBuilder struct {
   name     string
   maxTotal int
   maxIdle  int
   minIdle  int
}

func NewConfigBuilder() *ConfigBuilder {
   return &ConfigBuilder{
      maxTotal: defaultMaxTotal,
      maxIdle:  defaultMaxIdle,
      minIdle:  defaultMinIdle,
   }
}

func (c *ConfigBuilder) SetName(name string) *ConfigBuilder {
   if name == "" {
      panic("")
   }
   c.name = name
   return c
}

func (c *ConfigBuilder) SetMaxTotal(maxTotal int) *ConfigBuilder {
   if maxTotal <= 0 {
      panic("")
   }
   c.maxTotal = maxTotal
   return c
}

func (c *ConfigBuilder) SetMaxIdle(maxIdle int) *ConfigBuilder {
   if maxIdle <= 0 {
      panic("")
   }
   c.maxIdle = maxIdle
   return c
}

func (c *ConfigBuilder) SetMinIdle(minIdle int) *ConfigBuilder {
   if minIdle < 0 {
      panic("")
   }
   c.minIdle = minIdle
   return c
}

func (c *ConfigBuilder) Build() *ConfigBuilder {
   // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
   if c.name == "" {
      panic("")
   }
   if c.maxIdle > c.maxTotal {
      panic("")
   }
   if c.minIdle > c.maxIdle || c.minIdle > c.maxTotal {
      panic("")
   }
   return c
}

// 客户端使用
func TestNewConfigBuilder(t *testing.T) {
   builder := NewConfigBuilder().
      SetName("test").
      SetMaxTotal(10).
      SetMaxIdle(8).
      SetMinIdle(2).
      Build()

   config := NewResourcePoolConfigByBuilder(builder)
   t.Log(config)
}


// optional program
type option func(c *ConfigBuilder)

func WithName(name string) option {
   return func(c *ConfigBuilder) {
      if name == "" {
         panic("")
      }
      c.name = name
   }
}

func WithMaxTotal(maxTotal int) option {
   return func(c *ConfigBuilder) {
      if maxTotal <= 0 {
         panic("")
      }
      c.maxTotal = maxTotal
   }
}

func WithMaxIdle(maxIdle int) option {
   return func(c *ConfigBuilder) {
      if maxIdle <= 0 {
         panic("")
      }
      c.maxIdle = maxIdle
   }
}

func WithMinIdle(minIdle int) option {
   return func(c *ConfigBuilder) {
      if minIdle < 0 {
         panic("")
      }
      c.minIdle = minIdle
   }
}

func NewBuilder(opts ...option) *ConfigBuilder {
   builder := NewConfigBuilder()
   for _, opt := range opts {
      opt(builder)
   }
   return builder.Build()
}

// 客户端使用
func TestNewConfigWithOption(t *testing.T) {
   opts := []option{
      WithName("test"),
      WithMaxTotal(10),
      WithMaxIdle(8),
      WithMinIdle(2),
   }
   builder := NewBuilder(opts...)
   config := NewResourcePoolConfigByBuilder(builder)
   t.Log(config)
}

使用建造者模式创建对象,还能避免对象存在无效状态。使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。

type Rectangle struct{
    width int
    height int
}

r := Rectange{} // invalid
r.width = 10    // invalid
r.height = 2    // valid

如果并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。那直接暴露 set() 方法来设置类的成员变量值是完全没问题的。而且,使用建造者模式来构建对象,代码实际上是有点重复的,类中的成员变量,要在 Builder 类中重新再定义一遍。

3. 建造者模式与工厂模式的区别

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

需要知道的是,每个模式为什么这么设计,能解决什么问题。只有了解了这些最本质的东西,我们才能不生搬硬套,才能灵活应用,甚至可以混用各种模式创造出新的模式,来解决特定场景的问题。

4. 使用建造者模式的其他场景

  • 生成一些很相似的产品,它们在制造过程相似且仅有细节上的差异,此时可使用生成器模式。参考上述代码,可以在生成其他类似的ConfigBuilder,它们包含的参数是一样的,但参数的默认值和校验规则不相同。