本文分享自华为云社区《【Go完结】实践GoF的23种规划形式:制作者形式》,作者: 元闰子。
简述
在程序规划中,咱们会常常遇到一些杂乱的目标,其间有许多成员特点,乃至嵌套着多个杂乱的目标。这种情况下,创立这个杂乱目标就会变得很繁琐。关于 C++/Java 而言,最常见的体现便是结构函数有着长长的参数列表:
MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)
关于 Go 语言来说,最常见的体现便是多层的嵌套实例化:
obj := &MyObject{
Field1: &Field1 {
Param1: &Param1 {
Val: 0,
},
Param2: &Param2 {
Val: 1,
},
...
},
Field2: &Field2 {
Param3: &Param3 {
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: &Region{ // 需求知道目标的完结细节
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 = &Region{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: &Region{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 = &Region{
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 = &Region{
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 形式。比方 xorm 结构中构建一个 SQL 目标是这样的:
builder.Insert().Into("table1").Select().From("table2").ToBoundSQL()
- 杂乱的 DTO 目标。
- …
优缺陷
优点
1、将杂乱的构建逻辑从业务逻辑中分离出来,遵循了单一职责准则。 2、能够将杂乱目标的构建进程拆分红多个步骤,提升了代码的可读性,而且能够操控特点构建的次序。 3、关于有多种构建办法的场景,能够将 Builder 规划为一个接口来提升可扩展性。 4、Go 语言中,使用 Functional Options 形式能够更为简练高雅地完结杂乱目标的构建。
缺陷
1、传统的制作者形式需求新增一个 Builder 目标来完结目标的结构,Fluent API 形式下乃至还要额定添加多个 Fluent 接口,必定程度上让代码愈加杂乱了。
与其他形式的关联
抽象工厂形式和制作者形式相似,两者都是用来构建杂乱的目标,但前者的侧重点是构建目标/产品族,后者的侧重点是目标的分步构建进程。
参阅
[1]【Go完结】实践GoF的23种规划形式:SOLID准则, 元闰子
[2] Design Patterns, Chapter 3. Creational Patterns, GoF
[3] GO 编程形式:FUNCTIONAL OPTIONS, 酷壳 CoolShell
[4] Fluent API: Practice and Theory, Ori Roth
[5] XORM BUILDER, xorm
[6] 生成器形式, refactoringguru.cn
点击关注,第一时间了解华为云新鲜技术~