【go实现】实践gof的23种设计模式:建造者模式-4008云顶国际网站
上一篇:
简单的分布式应用系统(示例代码工程):
简述
在程序设计中,我们会经常遇到一些复杂的对象,其中有很多成员属性,甚至嵌套着多个复杂的对象。这种情况下,创建这个复杂对象就会变得很繁琐。对于 c /java 而言,最常见的表现就是构造函数有着长长的参数列表:
myobject obj = new myobject(param1, param2, param3, param4, param5, param6, ...)
对于 go 语言来说,最常见的表现就是多层的嵌套实例化:
obj := &myobject{
field1: &field1 {
param1: ¶m1 {
val: 0,
},
param2: ¶m2 {
val: 1,
},
...
},
field2: &field2 {
param3: ¶m3 {
val: 2,
},
...
},
...
}
上述的对象创建方法有两个明显的缺点:(1)对使用者不友好,使用者在创建对象时需要知道的细节太多;(2)代码可读性很差。
针对这种对象成员较多,创建对象逻辑较为繁琐的场景,非常适合使用建造者模式来进行优化。
建造者模式的作用有如下几个:1、封装复杂对象的创建过程,使对象使用者不感知复杂的创建逻辑。
2、可以一步步按照顺序对成员进行赋值,或者创建嵌套对象,并最终完成目标对象的创建。
3、对多个对象复用同样的对象创建逻辑。
其中,第1和第2点比较常用,下面对建造者模式的实现也主要是针对这两点进行示例。
uml 结构
代码实现
示例
在(示例代码工程)中,我们定义了服务注册中心,提供服务注册、去注册、更新、 发现等功能。要实现这些功能,服务注册中心就必须保存服务的信息,我们把这些信息放在了 serviceprofile
这个数据结构上,定义如下:
// demo/service/registry/model/service_profile.go
// serviceprofile 服务档案,其中服务id唯一标识一个服务实例,一种服务类型可以有多个服务实例
type serviceprofile struct {
id string // 服务id
type servicetype // 服务类型
status servicestatus // 服务状态
endpoint network.endpoint // 服务endpoint
region *region // 服务所属region
priority int // 服务优先级,范围0~100,值越低,优先级越高
load int // 服务负载,负载越高表示服务处理的业务压力越大
}
// demo/service/registry/model/region.go
// region 值对象,每个服务都唯一属于一个region
type region struct {
id string
name string
country string
}
// demo/network/endpoint.go
// endpoint 值对象,其中ip和port属性为不可变,如果需要变更,需要整对象替换
type endpoint struct {
ip string
port int
}
实现
如果按照直接实例化方式应该是这样的:
// 多层的嵌套实例化
profile := &serviceprofile{
id: "service1",
type: "order",
status: normal,
endpoint: network.endpointof("192.168.0.1", 8080),
region: ®ion{ // 需要知道对象的实现细节
id: "region1",
name: "beijing",
country: "china",
},
priority: 1,
load: 100,
}
虽然 serviceprofile
结构体嵌套的层次不多,但是从上述直接实例化的代码来看,确实存在对使用者不友好和代码可读性较差的缺点。比如,使用者必须先对 endpoint
和 region
进行实例化,这实际上是将 serviceprofile
的实现细节暴露给使用者了。
下面我们引入建造者模式对代码进行优化重构:
// demo/service/registry/model/service_profile.go
// 关键点1: 为serviceprofile定义一个builder对象
type serviceprofilebuild struct {
// 关键点2: 将serviceprofile作为builder的成员属性
profile *serviceprofile
}
// 关键点3: 定义构建serviceprofile的方法
func (s *serviceprofilebuild) withid(id string) *serviceprofilebuild {
s.profile.id = id
// 关键点4: 返回builder接收者指针,支持链式调用
return s
}
func (s *serviceprofilebuild) withtype(servicetype servicetype) *serviceprofilebuild {
s.profile.type = servicetype
return s
}
func (s *serviceprofilebuild) withstatus(status servicestatus) *serviceprofilebuild {
s.profile.status = status
return s
}
func (s *serviceprofilebuild) withendpoint(ip string, port int) *serviceprofilebuild {
s.profile.endpoint = network.endpointof(ip, port)
return s
}
func (s *serviceprofilebuild) withregion(regionid, regionname, regioncountry) *serviceprofilebuild {
s.profile.region = ®ion{id: regionid, name: regionname, country: regioncountry}
return s
}
func (s *serviceprofilebuild) withpriority(priority int) *serviceprofilebuild {
s.profile.priority = priority
return s
}
func (s *serviceprofilebuild) withload(load int) *serviceprofilebuild {
s.profile.load = load
return s
}
// 关键点5: 定义build方法,在链式调用的最后调用,返回构建好的serviceprofile
func (s *serviceprofilebuild) build() *serviceprofile {
return s.profile
}
// 关键点6: 定义一个实例化builder对象的工厂方法
func newserviceprofilebuilder() *serviceprofilebuild {
return &serviceprofilebuild{profile: &serviceprofile{}}
}
实现建造者模式有 6 个关键点:
- 为
serviceprofile
定义一个 builder 对象serviceprofilebuild
,通常我们将它设计为包内可见,来限制客户端的滥用。 - 把需要构建的
serviceprofile
作为 builder 对象serviceprofilebuild
的成员属性,用来存储构建过程中的状态。 - 为 builder 对象
serviceprofilebuild
定义用来构建serviceprofile
的一系列方法,上述代码中我们使用了withxxx
的风格。 - 在构建方法中返回 builder 对象指针本身,也即接收者指针,用来支持链式调用,提升客户端代码的简洁性。
- 为 builder 对象定义 build() 方法,返回构建好的
serviceprofile
实例,在链式调用的最后调用。 - 定义一个实例化 builder 对象的工厂方法
newserviceprofilebuilder()
。
那么,使用建造者模式实例化逻辑是这样的:
// 建造者模式的实例化方法
profile := newserviceprofilebuilder().
withid("service1").
withtype("order").
withstatus(normal).
withendpoint("192.168.0.1", 8080).
withregion("region1", "beijing", "china").
withpriority(1).
withload(100).
build()
当使用建造者模式来进行对象创建时,使用者不再需要知道对象具体的实现细节(这里体现为无须预先实例化 endpoint
和 region
对象),代码可读性、简洁性也更好了。
扩展
functional options 模式
进一步思考,其实前文提到的建造者实现方式,还有 2 个待改进点:
- 我们额外新增了一个 builder 对象,如果能够把 builder 对象省略掉,同时又能避免长长的入参列表就更好了。
- 熟悉 java 的同学应该能够感觉出来,这种实现具有很强的“java 风格”。并非说这种风格不好,而是在 go 中理应有更具“go 风格”的建造者模式实现。
针对这两点,我们可以通过 functional options 模式 来优化。functional options 模式也是用来构建对象的,这里我们也把它看成是建造者模式的一种扩展。它利用了 go 语言中函数作为一等公民的特点,结合函数的可变参数,达到了优化上述 2 个改进点的目的。
使用 functional options 模式的实现是这样的:
// demo/service/registry/model/service_profile_functional_options.go
// 关键点1: 定义构建serviceprofile的functional option,以*serviceprofile作为入参的函数
type serviceprofileoption func(profile *serviceprofile)
// 关键点2: 定义实例化serviceprofile的工厂方法,使用serviceprofileoption作为可变入参
func newserviceprofile(svcid string, svctype servicetype, options ...serviceprofileoption) *serviceprofile {
// 关键点3: 可为特定的字段提供默认值
profile := &serviceprofile{
id: svcid,
type: svctype,
status: normal,
endpoint: network.endpointof("192.168.0.1", 80),
region: ®ion{id: "region1", name: "beijing", country: "china"},
priority: 1,
load: 100,
}
// 关键点4: 通过serviceprofileoption来修改字段
for _, option := range options {
option(profile)
}
return profile
}
// 关键点5: 定义一系列构建serviceprofile的方法,在serviceprofileoption实现构建逻辑,并返回serviceprofileoption
func status(status servicestatus) serviceprofileoption {
return func(profile *serviceprofile) {
profile.status = status
}
}
func endpoint(ip string, port int) serviceprofileoption {
return func(profile *serviceprofile) {
profile.endpoint = network.endpointof(ip, port)
}
}
func svcregion(svcid, svcname, svccountry string) serviceprofileoption {
return func(profile *serviceprofile) {
profile.region = ®ion{
id: svcid,
name: svcname,
country: svccountry,
}
}
}
func priority(priority int) serviceprofileoption {
return func(profile *serviceprofile) {
profile.priority = priority
}
}
func load(load int) serviceprofileoption {
return func(profile *serviceprofile) {
profile.load = load
}
}
实现 functional options 模式有 5 个关键点:
- 定义 functional option 类型
serviceprofileoption
,本质上是一个入参为构建对象serviceprofile
的指针类型。(注意必须是指针类型,值类型无法达到修改目的) - 定义构建
serviceprofile
的工厂方法,以serviceprofileoption
的可变参数作为入参。函数的可变参数就意味着可以不传参,因此一些必须赋值的属性建议还是定义对应的函数入参。 - 可为特定的属性提供默认值,这种做法在 为配置对象赋值的场景 比较常见。
- 在工厂方法中,通过
for
循环利用serviceprofileoption
完成构建对象的赋值。 - 定义一系列的构建方法,以需要构建的属性作为入参,返回
serviceprofileoption
对象,并在serviceprofileoption
中实现属性赋值。
functional options 模式 的实例化逻辑是这样的:
// functional options 模式的实例化逻辑
profile := newserviceprofile("service1", "order",
status(normal),
endpoint("192.168.0.1", 8080),
svcregion("region1", "beijing", "china"),
priority(1),
load(100))
相比于传统的建造者模式,functional options 模式的使用方式明显更加的简洁,也更具“go 风格”了。
fluent api 模式
前文中,不管是传统的建造者模式,还是 functional options 模式,我们都没有限定属性的构建顺序,比如:
// 传统建造者模式不限定属性的构建顺序
profile := newserviceprofilebuilder().
withpriority(1). // 先构建priority也完全没问题
withid("service1").
...
// functional options 模式也不限定属性的构建顺序
profile := newserviceprofile("service1", "order",
priority(1), // 先构建priority也完全没问题
status(normal),
...
但是在一些特定的场景,对象的属性是要求有一定的构建顺序的,如果违反了顺序,可能会导致一些隐藏的错误。
当然,我们可以与使用者的约定好属性构建的顺序,但这种约定是不可靠的,你很难保证使用者会一直遵守该约定。所以,更好的方法应该是通过接口的设计来解决问题, fluent api 模式 诞生了。
下面,我们使用 fluent api 模式进行实现:
// demo/service/registry/model/service_profile_fluent_api.go
type (
// 关键点1: 为serviceprofile定义一个builder对象
fluentserviceprofilebuilder struct {
// 关键点2: 将serviceprofile作为builder的成员属性
profile *serviceprofile
}
// 关键点3: 定义一系列构建属性的fluent接口,通过方法的返回值控制属性的构建顺序
idbuilder interface {
withid(id string) typebuilder
}
typebuilder interface {
withtype(svctype servicetype) statusbuilder
}
statusbuilder interface {
withstatus(status servicestatus) endpointbuilder
}
endpointbuilder interface {
withendpoint(ip string, port int) regionbuilder
}
regionbuilder interface {
withregion(regionid, regionname, regioncountry string) prioritybuilder
}
prioritybuilder interface {
withpriority(priority int) loadbuilder
}
loadbuilder interface {
withload(load int) endbuilder
}
// 关键点4: 定义一个fluent接口返回完成构建的serviceprofile,在最后调用链的最后调用
endbuilder interface {
build() *serviceprofile
}
)
// 关键点5: 为builder定义一系列构建方法,也即实现关键点3中定义的fluent接口
func (f *fluentserviceprofilebuilder) withid(id string) typebuilder {
f.profile.id = id
return f
}
func (f *fluentserviceprofilebuilder) withtype(svctype servicetype) statusbuilder {
f.profile.type = svctype
return f
}
func (f *fluentserviceprofilebuilder) withstatus(status servicestatus) endpointbuilder {
f.profile.status = status
return f
}
func (f *fluentserviceprofilebuilder) withendpoint(ip string, port int) regionbuilder {
f.profile.endpoint = network.endpointof(ip, port)
return f
}
func (f *fluentserviceprofilebuilder) withregion(regionid, regionname, regioncountry string) prioritybuilder {
f.profile.region = ®ion{
id: regionid,
name: regionname,
country: regioncountry,
}
return f
}
func (f *fluentserviceprofilebuilder) withpriority(priority int) loadbuilder {
f.profile.priority = priority
return f
}
func (f *fluentserviceprofilebuilder) withload(load int) endbuilder {
f.profile.load = load
return f
}
func (f *fluentserviceprofilebuilder) build() *serviceprofile {
return f.profile
}
// 关键点6: 定义一个实例化builder对象的工厂方法
func newfluentserviceprofilebuilder() idbuilder {
return &fluentserviceprofilebuilder{profile: &serviceprofile{}}
}
实现 fluent api 模式有 6 个关键点,大部分与传统的建造者模式类似:
- 为
serviceprofile
定义一个 builder 对象fluentserviceprofilebuilder
。 - 把需要构建的
serviceprofile
设计为 builder 对象fluentserviceprofilebuilder
的成员属性。 - 定义一系列构建属性的 fluent 接口,通过方法的返回值控制属性的构建顺序,这是实现 fluent api 的关键。比如
withid
方法的返回值是typebuilder
类型,表示紧随其后的就是withtype
方法。 - 定义一个 fluent 接口(这里是
endbuilder
)返回完成构建的serviceprofile
,在最后调用链的最后调用。 - 为 builder 定义一系列构建方法,也即实现关键点 3 中定义的 fluent 接口,并在构建方法中返回 builder 对象指针本身。
- 定义一个实例化 builder 对象的工厂方法
newfluentserviceprofilebuilder()
,返回第一个 fluent 接口,这里是idbuilder
,表示首先构建的是id
属性。
fluent api 的使用与传统的建造者实现使用类似,但是它限定了方法调用的顺序。如果顺序不对,在编译期就报错了,这样就能提前把问题暴露在编译器,减少了不必要的错误使用。
// fluent api的使用方法
profile := newfluentserviceprofilebuilder().
withid("service1").
withtype("order").
withstatus(normal).
withendpoint("192.168.0.1", 8080).
withregion("region1", "beijing", "china").
withpriority(1).
withload(100).
build()
// 如果方法调用不按照预定的顺序,编译器就会报错
profile := newfluentserviceprofilebuilder().
withtype("order").
withid("service1").
withstatus(normal).
withendpoint("192.168.0.1", 8080).
withregion("region1", "beijing", "china").
withpriority(1).
withload(100).
build()
// 上述代码片段把withtype和withid的调用顺序调换了,编译器会报如下错误
// newfluentserviceprofilebuilder().withtype undefined (type idbuilder has no field or method withtype)
典型应用场景
建造者模式主要应用在实例化复杂对象的场景,常见的有:
- 配置对象。比如创建 http server 时需要多个配置项,这种场景通过 functional options 模式就能够很优雅地实现配置功能。
- sql 语句对象。一些 orm 框架在构造 sql 语句时也经常会用到 builder 模式。比如 框架中构建一个 sql 对象是这样的:
builder.insert().into("table1").select().from("table2").toboundsql()
- 复杂的 dto 对象。
- …
优缺点
优点
1、将复杂的构建逻辑从业务逻辑中分离出来,遵循了单一职责原则。
2、可以将复杂对象的构建过程拆分成多个步骤,提升了代码的可读性,并且可以控制属性构建的顺序。
3、对于有多种构建方式的场景,可以将 builder 设计为一个接口来提升可扩展性。
4、go 语言中,利用 functional options 模式可以更为简洁优雅地完成复杂对象的构建。
缺点
1、传统的建造者模式需要新增一个 builder 对象来完成对象的构造,fluent api 模式下甚至还要额外增加多个 fluent 接口,一定程度上让代码更加复杂了。
与其他模式的关联
抽象工厂模式和建造者模式类似,两者都是用来构建复杂的对象,但前者的侧重点是构建对象/产品族,后者的侧重点是对象的分步构建过程。
参考
[1] , 元闰子
[2] , gof
[3] , 酷壳 coolshell
[4] , ori roth
[5] , xorm
[6] ,
更多文章请关注微信公众号:元闰子的邀请
- 点赞
- 收藏
- 关注作者
评论(0)