摘要:针对这种对象成员较多,创建对象逻辑较为繁琐的场景,非常适合使用建造者模式来进行优化。

本文分享自华为云社区《【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 个关键点:

  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()

当使用建造者模式来进行对象创建时,使用者不再需要知道对象具体的实现细节(这里体现为无须预先实例化 Endpoint 和 Region 对象),代码可读性、简洁性也更好了。

扩展

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

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

实践GoF的23种设计模式:建造者模式的更多相关文章

  1. 实践GoF的23种设计模式:装饰者模式

    摘要:装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力.理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性. 本文分享自华为云社区<[Go实现]实践GoF的2 ...

  2. 实践GoF的23种设计模式:SOLID原则(上)

    摘要:本文以我们日常开发中经常碰到的一些技术/问题/场景作为切入点,示范如何运用设计模式来完成相关的实现. 本文分享自华为云社区<实践GoF的23种设计模式:SOLID原则(上)>,作者: ...

  3. 实践GoF的23种设计模式:观察者模式

    摘要:当你需要监听某个状态的变更,且在状态变更时通知到监听者,用观察者模式吧. 本文分享自华为云社区<[Go实现]实践GoF的23种设计模式:观察者模式>,作者: 元闰子 . 简介 现在有 ...

  4. 23种设计模式--建造者模式-Builder Pattern

    一.建造模式的介绍       建造者模式就是将零件组装成一个整体,用官方一点的话来讲就是将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示.生活中比如说组装电脑,汽车等等这些都是建 ...

  5. java设计模式:概述与GoF的23种设计模式

    软件设计模式的产生背景 设计模式这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中. 1977 年,美国著名建筑大师.加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Chri ...

  6. 2.GoF 的 23 种设计模式的分类和功能

    1. 根据目的来分 根据模式是用来完成什么工作来划分,这种方式可分为创建型模式.结构型模式和行为型模式 3 种. 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”.GoF ...

  7. GoF 的 23 种设计模式的分类和功能

    1. 根据目的来分 根据模式是用来完成什么工作来划分,这种方式可分为创建型模式.结构型模式和行为型模式 3 种. 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”.GoF ...

  8. GoF的23种设计模式的功能

    GoF的23种设计模式的功能 前面说明了 GoF 的 23 种设计模式的分类,现在对各个模式的功能进行介绍. 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取 ...

  9. GoF的23种设计模式之创建型模式的特点和分类

    创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”.这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成.就像我们去商场购买商品时, ...

随机推荐

  1. 重载(Overload)和重写(Override)的区别。重载的 方法能否根据返回类型进行区分?

    方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性.重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同.参数个数不同或者二者都不同)则视 ...

  2. Java中的引用类型

    强引用(Strong) 就是我们平时使用的方式 A a = new A();强引用的对象是不会被回收的 软引用(Soft) 在jvm要内存溢出(OOM)时,会回收软引用的对象,释放更多内存 弱引用(W ...

  3. redis持久存储RDB和AOF的区别及优缺点

    1.前言 最近在项目中使用到Redis做缓存,方便多个业务进程之间共享数据.由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能, ...

  4. 5V转10.5V原理图

  5. .NET Best Practices: Architecture & Design Patterns (5 Days Training)

    .NET Best Practices: Architecture & Design Patterns (5 Days Training) .NET最佳实践:架构及设计模式 5天培训课程 课程 ...

  6. google fonts 国内使用解决方案

    由于众所周知的原因,国内使用google font库有很大的问题. 解决方案1:使用国内镜像如360网站卫士常用前端公共库CDN服务 优点:使用方便 缺点:目标用户包含国外的开发者,不清楚国外用户的加 ...

  7. java的内存泄露是如何发生的,如何避免和发现

    java的垃圾回收与内存泄露的关系:[新手可忽略不影响继续学习] 马克-to-win:上一节讲了,(i)对象被置成null.(ii)局部对象(无需置成null)当程序运行到右大括号.(iii)匿名对象 ...

  8. MySQL中MyISAM和InnoDB引擎的区别

    区别: 1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事 ...

  9. SpringCloud Function SpEL注入

    SpringCloud Function SpEL注入 漏洞分析

  10. 坐实大数据资源调度框架之王,Yarn为何这么牛

    摘要:Yarn的出现伴随着Hadoop的发展,使Hadoop从一个单一的大数据计算引擎,成为大数据的代名词. 本文分享自华为云社区<Yarn为何能坐实资源调度框架之王?>,作者: Java ...