浅析建造者模式
0. 前言
建造者模式是创建型设计模式的一种。本篇文章将介绍什么是建造者模式,以及什么时候用建造者模式,同时给出 Kubernetes:kubectl
中类似建造者模式的示例以加深理解。
1. 建造者模式
1.1 从工厂函数说起
试想构建房子类,其属性如下:
type house struct {
window int
door int
bed int
desk int
deskLamp int
}
其中,door
, window
和 bed
是必须配置,desk
和 deskLamp
是可选配置,且 desk
和 deskLamp
是配套配置。
通过工厂函数创建 house
对象:
func NewHouse(window, door, bed, desk, deskLamp int) *house {
return &house{
window: window,
door: door,
bed: bed,
desk: desk,
deskLamp: deskLamp,
}
}
这里有个问题在于 desk
和 deskLamp
是可选配置。通过 NewHouse
创建对象需要指定 desk
和 deskLamp
:
house := NewHouse(2, 1, 1, 0, 0)
这对调用者来说不必要。
继续,使用 set
结合工厂函数构造 house
对象:
func NewHouse(window, door, bed int) *house {
return &house{
window: window,
door: door,
bed: bed,
}
}
func (h *house) SetDesk(desk int) {
h.desk = desk
}
func (h *house) SetDeskLamp(deskLamp int) {
h.deskLamp = deskLamp
}
创建 house
对象:
house := NewHouse(2, 1, 1)
// 使用 set 设置 desk 和 deskLamp
house.SetDesk(1)
house.SetDeskLamp(1)
看起来还不错。
不过 desk
和 deskLamp
要配套出现,这里并没有检查配套的逻辑。并且,window
, door
和 bed
需要检测,如果传入的是 0 或 负数,应该要报错。
结合这两点,继续构建 house
对象。构建有两种思路:思路一,在构造完的 house
对象上添加 validation
方法校验属性。思路二,在工厂函数内校验必配属性,新建方法检查 desk
和 deskLamp
是否配套出现。
这两种思路虽然能实现校验功能,但是都有瑕疵。
思路一,在构造完对象后才验证,如果对象忘了调用 validation
,那这个对象就是个不安全的对象。
思路二,校验分开了,对象的属性应该放在一起校验,试想如果参数过多,且相互有依赖关系,那又得新增方法判断,麻烦且容易出错。
并且,对于调用方来说,构造过程暴露太多了。工厂函数的优势在于调用方无感知,如果暴露太多 set 方法,并且由调用方来调用验证方法验证对象属性。那工厂函数的优势将大打折扣。
1.2 工厂函数到建造者的优雅过渡
如何适配上述场景,使得调用方无感知呢?
试拆分上述代码如下:
func NewHouse() *house {
return &house{}
}
func (h *house) SetRequisite(window, door, bed int) *house {
h.window = window
h.door = door
h.bed = bed
return h
}
func (h *house) SetDesk(desk int) *house {
h.desk = desk
return h
}
func (h *house) SetDeskLamp(deskLamp int) *house {
h.deskLamp = deskLamp
return h
}
func (h *house) Validation() (*house, error) {
if h.window <= 0 || h.door <= 0 || h.bed <= 0 {
return nil, errors.New("invalid [window|door|bed]")
}
if h.desk < 0 || h.deskLamp < 0 {
return nil, errors.New("invalid [desk|deskLamp]")
}
if !(h.desk > 0 && h.deskLamp > 0) {
return nil, errors.New("need desk and deskLamp at same time")
}
return h, nil
}
创建 house
对象:
house, _ := NewHouse().SetRequisite(2, 1, 1).SetDesk(1).SetDeskLamp(1).Validation()
嗯,看起来清晰了不少。不过我们细细分析下逻辑的话还是会发现那么一点怪异的点。这一点在于,house
对象是 set
的主体,这在逻辑上好像不通。
是的,我们需要引入一个新对象叫 Builder
来创建 house
,而不是让 house
自己创建自己。
改造代码如下:
示例 1.1
type Builder struct {
house
}
func NewBuilder() *Builder {
return &Builder{}
}
func (b *Builder) SetRequisite(window, door, bed int) *Builder {
b.window = window
b.door = door
b.bed = bed
return b
}
func (b *Builder) SetDesk(desk int) *Builder {
b.desk = desk
return b
}
func (b *Builder) SetDeskLamp(deskLamp int) *Builder {
b.deskLamp = deskLamp
return b
}
func (b *Builder) build() (*house, error) {
if b.window <= 0 || b.door <= 0 || b.bed <= 0 {
return nil, errors.New("invalid [window|door|bed]")
}
if b.desk < 0 || b.deskLamp < 0 {
return nil, errors.New("invalid [desk|deskLamp]")
}
if !(b.desk > 0 && b.deskLamp > 0) {
return nil, errors.New("need desk and deskLamp at same time")
}
return &b.house, nil
}
这里做了几点改动:
1)新建 Builder
对象,通过 Builder
对象创建 house
。并且,将 house
作为 Builder
的属性,house
是 Builder
造的,作为属性挺合理的。
2)重命名 Validation
为 build
,之所以这么命名是想说明 build
是创建的最后一步,结束 build
之后即可获得 house
对象。
对于调用方,创建对象就变成了:
house, _ := NewBuilder().SetRequisite(2, 1, 1).SetDesk(1).SetDeskLamp(1).build()
这里 desk
和 deskLamp
是配套使用的,如果不需要的话。创建对象就变成:
house, _ := NewBuilder().SetRequisite(2, 1, 1).build()
要留意这种结构,它是顺序不一致的。
如果顺序一致的情况,即创建的流程都是一样的。那么可以将 build
抽象为接口,使用不同的接口创建产品,且创建的产品流程是一样的,可以用封装将这一过程封装起来。
举例,使用两个 Builder
创建房子。villaBuilder
先建十个门,再建五十个窗,最后放五十把椅子。residenceBuilder
负责建两个门,两个窗,以及五把椅子。代码如下:
type Builder interface {
createDoor() Builder
createWindow() Builder
createChair() Builder
build() (*house, error)
}
type villaBuilder struct {
house
}
type residenceBuilder struct {
house
}
type house struct {
door int
window int
chair int
}
func (vb *villaBuilder) createDoor() Builder {
vb.door = 10
return vb
}
func (vb *villaBuilder) createWindow() Builder {
vb.window = 50
return vb
}
func (vb *villaBuilder) createChair() Builder {
vb.chair = 50
return vb
}
func (vb *villaBuilder) validation() error {
return nil
}
func (vb *villaBuilder) build() (*house, error) {
// validate property of object houseBuilder, skip...
err := vb.validation()
vb.createDoor()
vb.createWindow()
vb.createChair()
return &vb.house, err
}
func (rb *residenceBuilder) createDoor() Builder {
rb.door = 2
return rb
}
func (rb *residenceBuilder) createWindow() Builder {
rb.window = 2
return rb
}
func (rb *residenceBuilder) createChair() Builder {
rb.chair = 1
return rb
}
func (rb *residenceBuilder) validation() error {
return nil
}
func (rb *residenceBuilder) build() (*house, error) {
// validate property of object carBuilder, skip...
err := rb.validation()
rb.createDoor()
rb.createWindow()
rb.createChair()
return &rb.house, err
}
func NewBuilder(typ string) Builder {
switch typ {
case "villa":
return &villaBuilder{}
case "residence":
return &residenceBuilder{}
default:
return nil
}
}
最后,通过不同类型的 Builder 创建房子:
house, err := NewBuilder("villa").build()
可以看到,通过 Builder
的 build
方法实现了创建过程的封装,对于调用方来说相当友好。
继续往下分析,刚才的参数是固定的。如果要用户可配,而不是内定的参数。怎么做呢?
重新改造代码如下:
type villaBuilder struct {
house
window int
door int
chair int
}
func (hb *villaBuilder) createDoor(door int) Builder {
hb.house.door = door
return hb
}
func (hb *villaBuilder) createWindow(window int) Builder {
hb.house.window = window
return hb
}
func (hb *villaBuilder) createChair(chair int) Builder {
hb.house.chair = chair
return hb
}
func (hb *villaBuilder) build() (*house, error) {
// validate property of object villaBuilder, skip...
err := hb.validate()
hb.createDoor(hb.door)
hb.createWindwo(hb.window)
hb.createChair(hb.chair)
return hb.car, err
}
func NewBuilder(typ string) Builder {
switch typ {
case "villa":
return &villaBuilder{}
case "residence":
return &residenceBuilder{}
default:
return nil
}
}
调用方创建 house
:
house, err := NewBuilder("house", 2, 2, 2).build()
这里最大的改变在于 Builder
对象中新增可配置属性 window
, door
和 chair
。通过 Builer
内的属性将参数传给内嵌产品对象,实现有序创建。
参数可配带来的问题在于,可以整合 villaBuilder
和 residenceBuilder
为一个 Builder
。通过该 Builder
实现根据不同配置创建 house
。
那就蜕化为前面的 示例 1.1 的实现了。
试想,这时候在新增冰箱和饮料两个属性,且这两个属性是可选的,配套的。那么怎么创建 house
和 car
呢?
同样的道理,将可选项赋值给 Builder
中的属性。代码如下:
示例 1.2
type villaBuilder struct {
house
window int
door int
chair int
icer int
drink int
}
func (vb *villaBuilder) createIcer() Builder {
vb.house.icer = vb.icer
return vb
}
func (vb *villaBuilder) createDrink() Builder {
vb.house.drink = vb.drink
return vb
}
func (vb *villaBuilder) setIcer(icer int) Builder {
vb.icer = icer
return vb
}
func (hb villaBuilder) setDrink(drink int) Builder {
vb.drink = drink
return vb
}
调用方创建过程为:
house, err := NewBuilder("house", 2, 2, 2).setIcer(1).setDrink(1).build()
调用方一直在和 Builder
打交道。可选配置传递给 Builder
,最后通过 build
创建出 house
,做到了表达和实现分离。
1.3 建造者模式
讲到这里基本也差不多了,在建造者模式中还有个 Director
对象作为更上层的封装。
从上面代码示例中,Builder
负责整体的顺序创建,可以把这块逻辑向上提给 Director
,Builder
只关心部件的创建,而不需要关心整体。做到逻辑的进一步拆分。代码示例如下:
type Director struct {
builder Builder
}
func (d *Director) createHouse() (*house, error) {
if err := d.builder.validation(); err != nil {
return nil, err
}
d.builder.createDoor()
d.builder.createWindwo()
d.builder.createChair()
return *hb.house, nil
}
调用方只需要创建 Builder
和 Director
而不需要关心实现细节。
画建造者模式的 UML 图,最后感受下:
1.4 建造者模式在 Kubernetes:kubectl 的应用
在 kubectl
上找到了建造者模式的应用,虽然不是“完全体”,不过没有关系。代码如下:
// https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/get/get.go
r := f.NewBuilder().
Unstructured().
NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces).
FilenameParam(o.ExplicitNamespace, &o.FilenameOptions).
LabelSelectorParam(o.LabelSelector).
FieldSelectorParam(o.FieldSelector).
Subresource(o.Subresource).
RequestChunksOf(chunkSize).
ResourceTypeOrNameArgs(true, args...).
ContinueOnError().
Latest().
Flatten().
TransformRequests(o.transformRequests).
Do()
这段代码是不是和我们的示例 1.2 非常像。通过 factory
的 NewBuilder
创建 Builder
,接着通过一系列建造者方法构造 Builder
,最后构建完成的 Builder
调用 Do
方法创建 resouce.Result
对象。
2. 小结
从上述分析可以做个建造者模式的小结:
1) 建造者模式是表达和实现分离,对于调用方来说不需要关注细节实现。
2) 建造者模式其内部对象建造顺序是稳定的,实现是复杂的。摘录《设计模式之美》的一段话表明什么时候该用建造者模式:
顾客走进一家餐馆点餐,我们利用工厂模式,根据顾客不同的选择,制作不同的食物,如比萨、汉堡和沙拉等。对于比萨,顾客又有各种配料可以选择,如奶酪、西红柿和培根等。我们通过建造者模式,根据顾客选择的不同配料,制作不同口味的比萨。
3) 建造者模式建造的对象是可用的,安全的。