本文分享自华为云社区《【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 结构

实践GoF的23种设计模式:建造者模式

代码完结

示例

在简略的分布式使用系统(示例代码工程)中,咱们界说了服务注册中心,供给服务注册、去注册、更新、 发现等功用。要完结这些功用,服务注册中心就有必要保存服务的信息,咱们把这些信息放在了 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 结构体嵌套的层次不多,可是从上述直接实例化的代码来看,的确存在对运用者不友好代码可读性较差的缺陷。比方,运用者有必要先对 EndpointRegion 进行实例化,这实际上是将 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 个要害点:

  1. ServiceProfile 界说一个 Builder 目标 serviceProfileBuild,通常咱们将它规划为包内可见,来限制客户端的滥用。
  2. 把需求构建的 ServiceProfile 作为 Builder 目标 serviceProfileBuild 的成员特点,用来存储构建进程中的状况。
  3. 为 Builder 目标 serviceProfileBuild 界说用来构建 ServiceProfile 的一系列办法,上述代码中咱们运用了 WithXXX 的风格。
  4. 在构建办法中回来 Builder 目标指针自身,也即接收者指针,用来支撑链式调用,提升客户端代码的简练性。
  5. 为 Builder 目标界说 Build() 办法,回来构建好的 ServiceProfile 实例,在链式调用的最终调用。
  6. 界说一个实例化 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()

当运用制作者形式来进行目标创立时,运用者不再需求知道目标详细的完结细节(这儿体现为无须预先实例化 EndpointRegion 目标),代码可读性、简练性也更好了。

扩展

Functional Options 形式

进一步思考,其实前文说到的制作者完结办法,还有 2 个待改善点:

  1. 咱们额定新增了一个 Builder 目标,假如能够把 Builder 目标省掉掉,同时又能防止长长的入参列表就更好了。
  2. 熟悉 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 个要害点:

  1. 界说 Functional Option 类型 ServiceProfileOption,本质上是一个入参为构建目标 ServiceProfile指针类型。(留意有必要是指针类型,值类型无法到达修正目的)
  2. 界说构建 ServiceProfile 的工厂办法,以 ServiceProfileOption 的可变参数作为入参。函数的可变参数就意味着能够不传参,因而一些有必要赋值的特点建议还是界说对应的函数入参。
  3. 可为特定的特点供给默认值,这种做法在 为装备目标赋值的场景 比较常见。
  4. 在工厂办法中,经过 for 循环使用 ServiceProfileOption 完结构建目标的赋值。
  5. 界说一系列的构建办法,以需求构建的特点作为入参,回来 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 个要害点,大部分与传统的制作者形式相似:

  1. ServiceProfile 界说一个 Builder 目标 fluentServiceProfileBuilder
  2. 把需求构建的 ServiceProfile 规划为 Builder 目标 fluentServiceProfileBuilder 的成员特点。
  3. 界说一系列构建特点的 Fluent 接口,经过办法的回来值操控特点的构建次序,这是完结 Fluent API 的要害。比方 WithId 办法的回来值是 typeBuilder 类型,表明紧随其后的便是 WithType 办法。
  4. 界说一个 Fluent 接口(这儿是 endBuilder)回来完结构建的 ServiceProfile,在最终调用链的最终调用。
  5. 为 Builder 界说一系列构建办法,也即完结要害点 3 中界说的 Fluent 接口,并在构建办法中回来 Builder 目标指针自身。
  6. 界说一个实例化 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

点击关注,第一时间了解华为云新鲜技术~