- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
本文分享自华为云社区《【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点比较常用,下面对建造者模式的实现也主要是针对这两点进行示例。
在简单的分布式应用系统(示例代码工程)中,我们定义了服务注册中心,提供服务注册、去注册、更新、 发现等功能。要实现这些功能,服务注册中心就必须保存服务的信息,我们把这些信息放在了 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
的成员属性,用来存储构建过程中的状态。serviceProfileBuild
定义用来构建 ServiceProfile
的一系列方法,上述代码中我们使用了 WithXXX
的风格。ServiceProfile
实例,在链式调用的最后调用。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
对象),代码可读性、简洁性也更好了。
进一步思考,其实前文提到的建造者实现方式,还有 2 个待改进点:
针对这两点,我们可以通过 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 个关键点:
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 风格”了。
前文中,不管是传统的建造者模式,还是 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
的成员属性。WithId
方法的返回值是 typeBuilder
类型,表示紧随其后的就是 WithType
方法。endBuilder
)返回完成构建的 ServiceProfile
,在最后调用链的最后调用。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)
建造者模式主要应用在实例化复杂对象的场景,常见的有:
builder.Insert().Into("table1").Select().From("table2").ToBoundSQL()
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
本文分享自华为云社区《大模型LLM之分布式训练》,作者: 码上开花_Lancer。 随着语言模型参数量和所需训练数据量的急速增长,单个机器上有限的资源已无法满足大语言模型训练的要求。需要设计分布式训
本文分享自华为云社区《五大基础算法--动态规划法》,作者: 大金(内蒙的)。 一、基本概念 动态规划法,和分治法极其相似。区别就是,在求解子问题时,会保存该子问题的解,后面的子问题求解时,可以直接拿来
pip install scp pip install pexpect 测试代码: import os import stat import paramiko # 用于调用scp命令 def s
我目前正在实现“ token ”REST 服务。 token 只是一个字符串,由一些参数构建而成,然后经过哈希处理并在一定时间后过期。 我想在我的 REST 服务中有一个可以验证 token 的端点,
打开软删除后,我在客户端上添加一条记录,推送,删除添加的记录推送,然后尝试使用与初始记录相同的主键添加新记录(然后推送),我得到一个异常(exception)。 EntityDomainManager
打开软删除后,我在客户端上添加一条记录,推送,删除添加的记录推送,然后尝试使用与初始记录相同的主键添加新记录(然后推送),我得到一个异常(exception)。 EntityDomainManager
我有一个应用程序,每 x 秒接收一次天气信息。我想将此数据保存到 XML 文件中。 我应该为每个天气通知创建一个新的 XML 文件,还是将每个通知附加到同一个 XML 文件中?我不确定 XML 标准的
我猜我们大多数人都必须在某个时候处理这个问题,所以我想我会问这个问题。 当您的 BLL 中有很多集合并且您发现自己一遍又一遍地编写相同的旧内联(匿名)谓词时,显然有必要进行封装,但实现封装的最佳方
我有一些 c# 代码已经运行了一段时间了..我不得不说,虽然我了解 OO 原则的基础知识,但显然有不止一种方法可以给猫剥皮(尽管我讨厌那个短语!)。 因此,我有一个基本抽象类作为基本数据服务类,如下所
我设计了一个 SQL 数据库系统(使用 Postgre),我有一个问题,即创建一个关系/引用的常见做法是什么,这种关系/引用即使在引用的对象被删除时也能持续存在。 比如有一个UserORM,还有Act
我们的目标是搜索用户输入的字符串并计算在其中找到多少元音。不幸的是我被困在这里,有什么帮助吗? def numVowels(s): vowels= "AEIOUaeiou" if s
我有一个适用于我的“items”int 数组的旋转函数。下面的代码完成了它,除了我不必要地传输值。我正在努力实现“就地”轮换。我的意思是 ptrs 会递增或递减,而不是从数组中获取值。我需要通过这种方
我有一个 json 存储在我的应用程序文档文件夹中,我需要在我的所有 View 中使用它。我正在加载 json 并将其添加到每个 View 中的 NSMutableArray。但现在我了解到,我可以将
我用 C++ 开始了一个项目。这种语言的内存管理对我来说是新的。 我过去常常使用 new () 创建对象,然后传递指针,虽然它可以工作,但调试起来很痛苦,人们看到代码时会用有趣的眼神看着我。我为它没有
已结束。 这个问题是 off-topic .它目前不接受答案。 想要改进这个问题? Update the question所以它是on-topic堆栈溢出。 关闭 10 年前。 Improve thi
保持类松散耦合是编写易于理解、修改和调试的代码的一个重要方面——我明白这一点。然而,作为一个新手,几乎任何时候我都会超越我所苦苦挣扎的最简单的例子。 我或多或少地了解如何将字符串、整数和简单数据类型封
我发现我需要编写大量重复代码,因为我无法从其他 Controller 调用函数。例如,这里新闻提要内容在我的代码中重复,我对一个 Controller 做一些特定的事情,然后需要像这样加载我的新闻提要
假设需要一种数字数据类型,其允许值在指定范围内。更具体地说,假设要定义一个整数类型,其最小值为0,最大值为5000。这种情况在很多情况下都会出现,例如在对数据库数据类型,XSD数据类型进行建模时。 在
假设我想循环整个数组来访问每个元素。使用 for 循环、for...in 循环或 for...of 循环是 JavaScript 开发人员的标准做法吗? 例如: var myArray = ["app
我有一个旧的 SL4/ria 应用程序,我希望用 Breeze 取代它。我有一个关于内存使用和缓存的问题。我的应用程序加载工作列表(一个典型的用户可以访问大约 1,000 个这些工作)。此外,还有很多
我是一名优秀的程序员,十分优秀!