前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >builder(建造者)模式

builder(建造者)模式

作者头像
数据小冰
发布2022-08-15 14:57:27
5560
发布2022-08-15 14:57:27
举报
文章被收录于专栏:数据小冰数据小冰

为什么需要建造者模式

在平时工作中,创建一个对象的最常用的方式是通过new来实现。因为Go语言中没有构造函数,所以一般会定义一个NewObject() *Object函数实现类似其他语言构造函数的功能。那在什么时候用建造者模式来创建对象呢?

从一个例子入手,有一个数据库连接池对象,它有如下几个字段。其中只有name是必填字段,其他都是可选字段,对于可选字段,如果用户没有设置,会给一个默认值。

代码语言:javascript
复制
type DBPool struct {
 name     string
 maxTotal uint32 // 最大连接数
 maxIdle  uint32 // 最大空闲数
 minIdle  uint32 // 最小空闲数
}

实现上述要求不难。name是必填的,所以放到构造函数中,其他是可选字段通过set方法设置,得到如下实现。

代码语言:javascript
复制
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方法构成了一个链式结构,所以可以很灵活组织产生的规则顺序。

代码语言:javascript
复制
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对象。

代码语言:javascript
复制
func main() {
 var b Builder = &concreteBuilder{}
 d := NewDirector(b)
 fmt.Println(d.construct())
}

整个创建的时序图如下,这个过程还是很简单的。先创建一个建造者,然后给建造者指定一个构建算法,建造者按照算法中的步骤完成对象的构建,最后获取最终的对象。

使用场景

建造者模式通常下面的四种情况中使用

  1. 需要生成的对象包含多个成员属性
  2. 需要生成的对象的属性相互依赖,需要指定其生产顺序
  3. 对象的创建过程独立于创建该对象的类
  4. 需要隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

在gorm源码中,可以看到有大量的链式调用,这个可以作为builder的一种扩展场景。例如在search.go文件中,有下面的链式调用。

代码语言:javascript
复制
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]

Reference

[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/

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-04,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要建造者模式
  • 建造者模式分析
  • 使用场景
  • 总结
    • Reference
    相关产品与服务
    云数据库 MySQL
    腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
    http://www.vxiaotou.com