本文分享自华为云社区《【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           // 服务IDType     ServiceType      // 服务类型Status   ServiceStatus    // 服务状态Endpoint network.Endpoint // 服务EndpointRegion   *Region          // 服务所属regionPriority int              // 服务优先级,范围0~100,值越低,优先级越高Load     int              // 服务负载,负载越高表示服务处理的业务压力越大
}// demo/service/registry/model/region.go
// Region 值对象,每个服务都唯一属于一个Region
type Region struct {Id      stringName    stringCountry string
}// demo/network/endpoint.go
// Endpoint 值对象,其中ip和port属性为不可变,如果需要变更,需要整对象替换
type Endpoint struct {ip   stringport 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 = serviceTypereturn s
}func (s *serviceProfileBuild) WithStatus(status ServiceStatus) *serviceProfileBuild {s.profile.Status = statusreturn 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 = priorityreturn s
}func (s *serviceProfileBuild) WithLoad(load int) *serviceProfileBuild {s.profile.Load = loadreturn 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 = idreturn f
}func (f *fluentServiceProfileBuilder) WithType(svcType ServiceType) statusBuilder {f.profile.Type = svcTypereturn f
}func (f *fluentServiceProfileBuilder) WithStatus(status ServiceStatus) endpointBuilder {f.profile.Status = statusreturn 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 = priorityreturn f
}func (f *fluentServiceProfileBuilder) WithLoad(load int) endBuilder {f.profile.Load = loadreturn 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. 【Go实现】实践GoF的23种设计模式:命令模式

    上一篇:[Go实现]实践GoF的23种设计模式:代理模式 简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Patter ...

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

    本文分享自华为云社区<实践GoF的23种设计模式:SOLID原则(上)>,作者:元闰子. 前言 从1995年GoF提出23种设计模式到现在,25年过去了,设计模式依旧是软件领域的热门话题. ...

  3. 23种设计模式----------建造者模式

    建造者模式:像是模板方法模式的升级.也叫生成器模式.将一个复杂对象的构造与它的表示分离,使得同样的构建过程可以创建不同的表示.其实就是创建一大类的东西, 但是具体又有些差异. 在建造者模式中,一般有四 ...

  4. 23种设计模式——建造者模式

    为什么会出现建造者模式 需要创建一个复杂对象的时候,这个对象通过一定的步骤组合而成,并且步骤一步都不能少. 流程 玩家(客户)告诉拳头(指挥者)我想要一个什么样的英雄,拳头告诉手下的程序员去做一个这样 ...

  5. 实践GoF的23的设计模式:SOLID原则(下)

    本文分享自华为云社区<实践GoF的23的设计模式:SOLID原则(下)>,作者: 雷电与骤雨. 在<实践GoF的23种设计模式:SOLID原则(上)>中,主要讲了SOLID原则 ...

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

    摘抄:http://c.biancheng.net/view/1320.html 1.什么是GoF(摘抄自百度词条) <Design Patterns: Elements of Reusable ...

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

    设计模式有两种分类方法,即根据模式的目的来分和根据模式的作用范围来分. 根据目的来分 根据模式是用来完成什么工作来划分,这种方式可分为创建型模式.结构性模式和行为模式3种. 创建型模式:用于描述&qu ...

  8. c++ 23种设计模式_使用Go实现GoF的23种设计模式(三)

    前言 上一篇文章<使用Go实现GoF的23种设计模式(二)>中,我们介绍了结构型模式(Structural Pattern)中的组合模式.适配器模式和桥接模式.本文将会介绍完剩下的几种结构 ...

  9. GoF的23种设计模式速记

    设计模式这东西平时也不怎么用,最近打算好好看一看这部分内容,但是GoF的23种设计模式在不熟悉的情况下,想要记住有点困难.所以想着自己画一个图,用于简单快速的记忆,同时也分享出来.让这些名词儿能在脑袋 ...

最新文章

  1. 图片提取文字功能很神奇?Java几行代码搞定它!
  2. 用C++ 和OpenCV 实现视频目标检测(YOLOv4模型)
  3. JS 监控页面刷新,关闭 事件的方法(转载)
  4. python中str isupper_python pandas Series.str.isupper用法及代码示例
  5. python简单代码编写-Python | 编写一个简单的Excel处理脚本
  6. 垃圾回收机制之复制算法
  7. Java多线程的几种写法
  8. myEclipse背景控制插件方案 内附使用说明
  9. 头条搜索发布2020年十大流行语:逆行者、集美、后浪位列前三
  10. 转:Gulp使用指南
  11. Kubernetes 小白学习笔记(20)--kubernetes的运维-管理Node
  12. elasticsearch实践之代码结构设计
  13. PMP培训第一次听课笔记(第1-3章)
  14. (文献研读)ContainerCloudSim:云数据中心中容器建模和仿真的环境
  15. tensorflow常用数据函数总结(tf.tile()、tf.expand_dims())
  16. matlab自学入门
  17. android中加载Gif图片
  18. python virtualenv迁移,迁移virtualenv环境
  19. [渝粤教育] 山东大学 2021秋中国武术文化(艺术英语) 参考 资料
  20. NetSuite ERP的优势是什么?

热门文章

  1. 会议及作用篇--项目管理(三)
  2. Apache配置及应用
  3. 迷你计算机工作站,这到底是什么 迄今最mini的工作站即将发售
  4. HTML列表的简单使用以及在我们网页编程中的单位你了解多少??CSS中的字体样式你又了解多少,进来康康!!HTML、CSS(三)
  5. 如何在Fragment碎片中展示数据
  6. 爱情保卫战经典语录全集
  7. 台湾地震,微软遭罪。
  8. 婴幼儿办理护照的过程及注意事项(原创)
  9. 计算机专业河南单招,河南单招计算机专业专科学校排名
  10. C71500(BFe30-1-1)镍白铜锻件 带材