在平时工作中,创建一个对象的最常用的方式是通过new来实现。因为Go语言中没有构造函数,所以一般会定义一个NewObject() *Object函数实现类似其他语言构造函数的功能。那在什么时候用建造者模式来创建对象呢?
从一个例子入手,有一个数据库连接池对象,它有如下几个字段。其中只有name是必填字段,其他都是可选字段,对于可选字段,如果用户没有设置,会给一个默认值。
type DBPool struct {
name string
maxTotal uint32 // 最大连接数
maxIdle uint32 // 最大空闲数
minIdle uint32 // 最小空闲数
}
实现上述要求不难。name是必填的,所以放到构造函数中,其他是可选字段通过set方法设置,得到如下实现。
const (
defaultMaxTotal = 8
defaultMaxIdle = 4
defaultMinIdle = 2
)
func NewDBPool(name string) *DBPool {
return &DBPool{
name: name,
maxTotal: defaultMaxTotal,
maxIdle: defaultMaxIdle,
minIdle: defaultMinIdle,
}
}
func (pool *DBPool) SetMaxTotal(maxTotal uint32) {
pool.maxTotal = maxTotal
}
func (pool *DBPool) SetMaxIdle(maxIdle uint32) {
pool.maxIdle = maxIdle
}
func (pool *DBPool) SetMinIdle(minIdle uint32) {
pool.minIdle = minIdle
}
使用的时候调用NewDBPool方法,传入name,就得到了一个DBPool指针类型的对象。如果对可选字段设置直接调用对应的Set方法。
上面的实现满足了此问题的需求。但有时候必填字段不止一个,有很多个,如果按照上面的解决方法,会把它们也都放入到构造函数中,强制创建对象的时候设置,这样会出现构造函数参数列表很长,如果把必填字段通过Set方法设置,又达不到必填的效果。
此外,如果字段值之间存在约束关系,比如maxIdle和minIdle的值要不能大于maxTotal。我们在哪里做这种校验逻辑呢?
为了解决上面的问题,这时候建造者模式就派上用场了。下一小节会分析建造者模式,以及它是如何解决上述问题的。
在GoF书中,对建造者模式的定义如下:
?将复杂对象的构造与其表示分离,以便同一构造过程可以创建不同的表示 ?
建造者模式主要包含4个角色,分别是Product、Builder、ConcreteBuilder和Director。Product: 表示最终构建的对象,例如上文中的DBPool ConcreteBuilder: 代表构造者抽象基类,在Go中用interface实现。它定义了Product的步骤,它的子类需要实现这些步骤,同时需要包含一个用来返回最终对象的方法getProduct. Director: 表示构造最终对象的某种算法,通过使用构造函数来调用Builder的创建方法创建对象,等创建完成后,通过getProduct方法来获取最终的对象。
根据上面建造者模式结构改造之前的代码。定义一个Builder接口,除了设置DBPool对象字段的buildXXX方法外,还定义有一个返回DBPool对象的方法getResult.我们在getResult处理逻辑中加入约束的校验,满足条件之后才会调用NewDBPool创建一个*DBPool对象,concreteBuilder是Builder接口实现对象,它的字段与DBPool是一样的,所以下面的代码concreteBuilder嵌套了DBPool对象。director会按照算法规则进行Build,最后调用Builder对象的getResul方法。整个Builder的build方法构成了一个链式结构,所以可以很灵活组织产生的规则顺序。
func NewDBPool(cb *concreteBuilder) *DBPool {
return &DBPool{
name: cb.name,
maxTotal: cb.maxTotal,
maxIdle: cb.maxIdle,
minIdle: cb.minIdle,
}
}
type Builder interface {
buildName(string) Builder
buildMaxTotal(uint32) Builder
buildMaxIdle(uint32) Builder
buildMinIdle(uint32) Builder
getResult() *DBPool
}
type concreteBuilder struct {
DBPool
}
func (c *concreteBuilder) buildName(name string) Builder {
c.name = name
return c
}
func (c *concreteBuilder) buildMaxTotal(maxTotal uint32) Builder {
c.maxTotal = maxTotal
return c
}
func (c *concreteBuilder) buildMaxIdle(maxIdle uint32) Builder {
c.maxIdle = maxIdle
return c
}
func (c *concreteBuilder) buildMinIdle(minIdle uint32) Builder {
c.minIdle = minIdle
return c
}
func (c *concreteBuilder) getResult() *DBPool {
// 加入依赖校验逻辑
if c.name == "" {
panic("name is empty")
}
if c.maxIdle > c.maxTotal {
panic("maxIdle shouldn't more than maxTotal")
}
if c.minIdle > c.maxTotal {
panic("minIdle shouldn't more than maxTotal")
}
return NewDBPool(c)
}
type director struct {
b Builder
}
func (d *director) SetBuilder(b Builder) {
d.b = b
}
func (d director) Construct() *DBPool {
return d.b.buildName("MySQL").buildMaxTotal(8).buildMaxIdle(4).buildMinIdle(2).getResult()
}
使用者通过director和concretBuilder对象,就可以创建一个DBPool对象。
func main() {
var b Builder = &concreteBuilder{}
d := NewDirector(b)
fmt.Println(d.construct())
}
整个创建的时序图如下,这个过程还是很简单的。先创建一个建造者,然后给建造者指定一个构建算法,建造者按照算法中的步骤完成对象的构建,最后获取最终的对象。
建造者模式通常下面的四种情况中使用
在gorm源码中,可以看到有大量的链式调用,这个可以作为builder的一种扩展场景。例如在search.go文件中,有下面的链式调用。
func (s *search) Where(query interface{}, values ...interface{}) *search {
...
return s
}
func (s *search) Not(query interface{}, values ...interface{}) *search {
...
return s
}
func (s *search) Or(query interface{}, values ...interface{}) *search {
...
return s
}
...
使用建造者模式可以带来的收益,一是能够分阶段、分步骤的方法更适合多次运算结果、有依赖校验的类创建场景,例如在前面数据库连接池需要校验参数设置的合理性,可以将校验逻辑放入到builder中,只有合法之后才真正创建建造者对象。二是不需要关心特定类型的建造者的具体算法实现。在一些快速复用的场景中,能起到提升编码效率的作用。从SOLID角度来说,分离创建与使用,使用者不需要知道内部实现算法,通过统一的director方法接口调用,可以自由组合出不同的对象实例。满足开闭原则,每个建造者相对独立,可以方便进行替换或新增。可以自由组合对象的创建过程。当然,建造者模式也有一些缺点,会增加代码行数,在前面的concreteBuilder对象中的属性与DBPool中存在重复,还有就是使用范围有局限,如果对象实例之间的差异性很大,不适合使用建造者模式。
Desing Patterns in Golang: Builder[1]Builder Pattern in GoLang[2]Builder Design Pattern in Golang[3]
[1]
Desing Patterns in Golang: Builder: https://blog.ralch.com/articles/design-patterns/golang-builder/
[2]
Builder Pattern in GoLang: https://golangbyexample.com/builder-pattern-golang/
[3]
Builder Design Pattern in Golang: https://smartscribs.com/builder-design-pattern-in-golang/