建造者模式

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

实际上,建造者模式的原理和代码实现非常简单,掌握起来并不难,难点在于应用场景。

下面通过一些示例来看看建造者模式。

示例

现在有一个需求要创建一个网络请求对象,既然是网络请求对象那么就会有一些参数。如下表:

字段 解释 是否为必填 默认值
from 源地址 “”
to 目标地址 “”
host 本机地址 “”
expires 过期时间 0
method sip 请求方法 “”
userAgent 代理名称 sip
body 请求体 “”

根据以上表格来写 Go 语言代码:

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
type Request struct {
from *FromHeader
to *ToHeader
host string
transport string
expires *Expires
method RequestMethod
userAgent *UserAgentHeader
body string
}

func NewRequest(from *FromHeader, to *ToHeader, expires *Expires, method RequestMethod, ua *UserAgentHeader,host,transport,body string) *Request {

// 各种字段的校验逻辑
r := new(Request)
if from != nil {
r.from = from
}
if to != nil {
r.to = to
}
// 省略其他字段的校验逻辑

return r
}

现在有一个 NewRequest 函数来创建 Reqeust 对象,该对象有 5 个必选项再加上 2 个可选项,该函数就有入参 7 个选项。但如果后续可配置项逐渐增多的话,按照现在的设计思路,该函数的参数列表就变得很长,代码在可读性和易用性上都会变得很差。特别是调用该函数的时候,容易搞错各参数的顺序,传递进错误的参数值,导致 bug。

那换一种思路,修改 NewRequest 函数的参数列表,只把必选参数当作入参,可选参数把字段的可导出性设置成 public。这样可以强制创建实例的时候把必要参数填写进去:

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
type Request struct {
from *FromHeader
to *ToHeader
host string
transport string
expires *Expires
method RequestMethod
UserAgent *UserAgentHeader
Body string
}

func NewRequest(from *FromHeader, to *ToHeader, expires *Expires, method RequestMethod,host,transport string) *Request {

// 各种字段的校验逻辑
r := new(Request)
if from != nil {
r.from = from
}
if to != nil {
r.to = to
}
// 省略其他字段的校验逻辑

return r
}

经过一次改造之后,没有了冗长的函数调用和参数,代码在可读性和易用性提高了很多:

1
2
3
4
5
6
7
// 使用时
//...省略各种参数
r := NewRequest()
r.body = body
r.userAgent = ua

sendRequest(r)

普通对象创建函数的痛点

至此,我们仍然没有用到建造者模式,通过 NewRequest 函数设置必填项,通过字段的可访问性去配置可选配置项,就能实现我们的设计需求。如果我们把问题的难度再加大点,比如,还需要解决下面这三个问题,那现在的设计思路就不能满足了。

  • 如果该对象的必选配置项有很多的话,那该函数又会出现参数列表很长的问题;
  • 假设配置项有依赖、约束关系。比如 body 可选项不接受 JSON 文本以外的数据,而 body 字段又只能通过字段赋值的方式去修改,校验的逻辑不知道写在哪里了;
  • 假设该对象是一个不可变对象,也就是该对象的所有字段都是在包外无法访问的,那当前方案可选字段也就无法实现这个需求了。

为了解决以上问题,建造者模式就派上用场了。

首先,我们先创建 XXBuilder 类,比如这里就是 RequestBuilder 类,并且通过 Set()方法设置建造者类的变量值,然后在使用 build()方法真正的创建对象之前,做集中的校验,校验通过之后才会返回 Request 对象。

除此之外,把之前用于创建对象的 NewRequest()方法给删除掉,这样我们想要获取到 Request 对象的话只能先创建 RequstBuilder 类后调用 build 方法才能获取 Reqeust 对象。具体的代码如下:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
type RequestBuilder struct {
from *FromHeader
to *ToHeader
host string
transport string
expires *Expires
method RequestMethod
userAgent *UserAgentHeader
body string
}

func NewRequestBuilder() *RequestBuilder {
// 设置默认值
userAgent := UserAgentHeader("GoSIP")
rb := &RequestBuilder{
host: "localhost",
body: "",
userAgent: &userAgent,
}
return rb
}

func (rb *RequestBuilder) SetHost(host string) *RequestBuilder {
if host == "" {
rb.host = "localhost"
} else {
rb.host = host
}

return rb
}

func (rb *RequestBuilder) SetBody(body string) *RequestBuilder {
rb.body = body

return rb
}

func (rb *RequestBuilder) SetFrom(address *Address) *RequestBuilder {
if address == nil {
rb.from = nil
} else {
address = address.Clone()
if address.Uri.Host() == "" {
address.Uri.SetHost(rb.host)
}
rb.from = &FromHeader{
DisplayName: address.DisplayName,
Address: address.Uri,
Params: address.Params,
}
}
return rb
}

// 省略其他属性

// build方法
func (rb *RequestBuilder) Build() (Request, error) {

if rb.from == nil {
return nil, fmt.Errorf("empty 'From' header")
}
if rb.to == nil {
return nil, fmt.Errorf("empty 'To' header")
}

hdrs := make([]Header, 0)

hdrs = append(hdrs, rb.cseq, rb.from, rb.to, rb.callID)

if rb.expires != nil {
hdrs = append(hdrs, rb.expires)
}
if rb.contentType != nil {
hdrs = append(hdrs, rb.contentType)
}
if rb.accept != nil {
hdrs = append(hdrs, rb.accept)
}
if rb.userAgent != nil {
hdrs = append(hdrs, rb.userAgent)
}

for _, header := range rb.generic {
hdrs = append(hdrs, header)
}

// basic request
req := NewRequest("", rb.method, rb.recipient, sipVersion, hdrs, "", nil)
req.SetBody(rb.body, true)

return req, nil
}

和工厂模式有何区别?

建造者模式是让建造者类来负责对象的创建工作。[[工厂模式]]是由工厂类来负责对象创建的工作。那它们之间有什么区别呢?

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

网上有一个经典的例子很好地解释了两者的区别。

顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。

实际上,我们也不要太学院派,非得把工厂模式、建造者模式分得那么清楚,我们需要知道的是,每个模式为什么这么设计,能解决什么问题。

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