Go 中的构建器模式
关 注 微 信 公 众 号 《 云 原 生 C T O 》 更 多 云 原 生 干 货 等 你 来 探 索
专 注 于 云原生技术
分 享
提 供 优 质 云原生开发
视 频 技 术 培 训
面试技巧
, 及 技术疑难问题
解答
云 原 生 技 术 分 享 不 仅 仅 局 限 于 Go
、 Rust
、 Python
、 Istio
、 containerd
、 CoreDNS
、 Envoy
、 etcd
、 Fluentd
、 Harbor
、 Helm
、 Jaeger
、 Kubernetes
、 Open Policy Agent
、 Prometheus
、 Rook
、 TiKV
、 TUF
、 Vitess
、 Arg
o
、 Buildpacks
、 CloudEvents
、 CNI
、 Contour
、 Cortex
、 CRI-O
、 Falco
、 Flux
、 gRPC
、 KubeEdge
、 Linkerd
、 NATS
、 Notary
、 OpenTracing
、 Operator
Framework
、 SPIFFE
、 SPIRE
和 Thanos
等
Go 中的构建器模式
在 Operator
条件更新上应用 Go
风格的构建器模式的实际示例
建议我们在某个“框架”内进行编码,即遵循一定的设计模式,这些模式是有效的、可复制的、被广泛认可的、更容易理解和应用的。
为什么要设计模式
为了不那么抽象,我们从实践中的一个例子开始。
通常,我们定义一个 struct
,然后在使用它时对其进行初始化。
type A struct { name string } a := A { name: “abc”}
这是常见的用法,但不适用于 A
复杂的场景
-
多层嵌套字段
-
超过 5 个字段
-
不同的字段需要不同的默认值
-
多个可选字段
-
以上四种的组合
例如在 Kubernetes operator
开发中,我们调用 SetStatusAndCondition
来更新资源信息,其中不仅包含了 metav1
的基本信息。条件,如状态,原因,观察生成等,但也传递回调函数,如 OnSuccess
和 OnError
。围绕 ConditionAndStatus
,我们可以添加其他逻辑,比如发送事件、处理不同状态(成功或失败)的逻辑,等等,然后定义一个类似如下的结构。:point_down:
type ConditionAndStatus struct { Condition metav1.Condition EventType string // event type Recorder record.EventRecorder, // K8s events recorder Force bool, // is Force update OnError func, // err handler OnSucces func, // success handler }
它可以通过通过 new
初始化这个 ConditionAndStatus
来工作,但是当有超过 5
个字段并且其中两个是嵌套的时候,它是累赘和复杂的,这是对非调用者友好的,并且在代码可读性上很差。除非 condition
和 eventRecorder
被实例化,否则调用者不能实例化 ConditionAndStatus
。调用者需要知道所有的实现细节,例如,他们应该知道在错误处理中更新条件时传递 onSucc
方法,即使只有 nil
。此外,不同的用户在不同的地方执行初始化时,每次都需要传入相同的 onSucc
和 onErr
。
那么我们该如何优化这段代码呢?`
Factory 模式
应用 Factory
模式可能是我们想到的第一个想法,但它不适用于这种情况。
通过 Factory
模式封装一些创建方法。
// Create no false, no default handlers func (c ConditionAndStatus) Create(cond metav1.Condition, eventType string, recorder record.EventRecorder) ConditionAndStatus{ return create(cond, eventType, recorder, false, nil, nil) } // Create no default handlers func (c ConditionAndStatus) Create(cond metav1.Condition, eventType string, recorder record.EventRecorder, force bool) ConditionAndStatus{ return create(cond, eventType, recorder, force, nil, nil) } // ... more create functions func (c ConditionAndStatus) Create(cond metav1.Condition, eventType string, recorder record.EventRecorder, force bool, onErr func, onSucc func) ConditionAndStatus{ return ConditionAndStatus { condtion: cond, eventType: eventType, recorder: recorder, force: force, onErr: onErr, onSucces: onSucc, } }
api
应该易于使用且不易误用——来自 Josh Bloch
然而, Factory
模式实现的 api
并不是那么方便。
显然, create
不是一个选项,因为它需要提供所有参数,传入的参数越多,操作就越困难。此外,当多个参数为同一类型时,很容易出错。
尽管其他 Factory
方法可以通过提供一些默认值来减少传入的参数来降低复杂性,但它们仍然缺乏灵活性。添加参数后,需要修改所有 create *
方法。
Builder模式
Builder
模式是一种设计模式,旨在为面向对象编程中的各种对象创建问题提供灵活的解决方案
来自http://en.wikipedia.org/wiki/Builder_pattern。
构建器模式为灵活简化复杂对象的创建铺平了道路,同时也隐藏了嵌入式类型的一些初始化细节,大大提高了可读性。
Builder接口
builder
接口是两种 builder
模式实现之一, buildxxx
用接口实现各个字段的方法, Builder
通过多态性确定具体的 builder
。请参阅下面的 UML
流程图。
让我们“翻新”以前的 ConditionAndStatus
.
type ConditionAndStatusBuilder interface { SetCondtion(cond metav1.Condition) ConditionAndStatusBuilder SetEventType(evnetType string) ConditionAndStatusBuilder SetRecorder(recorder record.EventRecorder) ConditionAndStatusBuilder SetForce(force bool) ConditionAndStatusBuilder SetOnErr(onErr func()) ConditionAndStatusBuilder SetOnSuccess(onSucc func()) ConditionAndStatusBuilder Build() ConditionAndStatus } type DefaultBuilder struct { condition metav1.Condition eventType string // event type recorder record.EventRecorder, // K8s events recorder force bool, // is Force update onError func, // err handler onSucces func, // success handler } func (b *DefaultBuilder) SetCondtion(cond metav1.Condition) DefaultBuilder{ b.condition = cond return b } // ... more set funcs func (b *DefaultBuilder) Build() ConditionAndStatus { // set some default values b.force = true return ConditionAndStatus { condition: b.condtion, // ... } }
要创建 ConditionAndStatus
,您可以使用注册方法组成所有构建器,然后通过 getByName
获得特定的构建器。
不难得出结论,该模式与 Factory
模式非常相似,因为每个构建器仍然需要创建所有字段或提供默认值。但它确实向前迈出了一步。
-
当字段确定时,它可以灵活地添加新的生成器,而不需要修改旧的生成器。
-
它可以控制创建不同字段的顺序。如果字段是相互依赖的,它可以隐藏细节并防止调用者犯错误。
然而,它与 Factory
模式有相同的缺点:一旦添加了字段(在 Builder
接口中添加方法),就需要修改所有构建器。
Pipeline建设者
另一种构建器模式是管道构建器,它更常见。通过上面的接口 builder
,你会发现多 builder
的设计是多余的,而让调用者控制相关字段的分配更合理:唯一的一个 builder
管理所有字段初始化,并通过返回 builder
本身来构建管道在每一步中,最后都组装成我们想要的。
通用调用代码的格式为 obj.Withxxx().Withyyy().Withzzz().build()
. 更改 ConditionAndStatus
如下。
type Builder struct { condition metav1.Condition eventType string // event type recorder record.EventRecorder, // K8s events recorder force bool, // is Force update onError func, // err handler onSucces func, // success handler } func (b *Builder) WithCondition(cond metav1.Condition) Builder{ b.condition = cond return b } // ... more Withxxx funcs func (b *Builder) Build() ConditionAndStatus { // set some default values b.force = true return ConditionAndStatus { condition: b.condtion, // ... } }
Pipeline builder
巧妙地避免了添加新字段带来的麻烦。只有一个 builder
,它可以通过添加 With*
方法轻松处理字段添加。
它对现有的调用者绝对更友好。如果参数是可选的,则不需要修改其他调用者的代码。而你只有通过添加新的调用者并 With*
在调用时插入方法来完成它;但是,当需要新参数而没有默认值时,则需要修改所有调用者的代码。
当然,没有一种模式是没有缺陷的,管道构建器也不是。
-
Withxxx()
一旦要构建许多字段,堆积的方法会给调用者带来麻烦并降低可读性。 -
无法控制字段的初始化顺序。如果存在依赖关系,则需要出色的错误控制和文档来避免错误。
-
代码不是
Go
风格,而是更多Java
风格。
可选的构建器模式
如果我们进一步优化管道构建器会怎样?正如 Dave Cheney
在他的 Practical Go
中提到的那样,我们应该以更多 Go
的方式尝试它。
首选 var args
到 []T
参数
深入挖掘,我们看到这里的大部分字段都是可选的,并且可以 var args
自然地定义。如有传入,申报;如果没有,请忽略它。因此, builder/factory
当隐藏实现细节时,只需要一种方法来处理整个对象的创建。
让我们一步一步地把这个想法付诸实践。
将可选字段抽象到构建结构中,而不是将所有字段都放入。要将 ConditionAndStatus
转换为以下结构,其中配置包含所有可选字段。
type ConditionAndStatus struct { condition metav1.Condition eventType string // event type recorder record.EventRecorder, // K8s events recorder configs configs // Optional configs } type configs struct { force bool, // is Force update onError func, // err handler onSucces func, // success handler }
对于配置,使用 func
选项接受一个 *configs
并返回自身以集成到管道中。需要使用以下方法。
type configs struct { force bool, // is Force update onError func, // err handler onSucces func, // success handler } type Option func(*configs) func ForceUpdate(force bool) Option { return func(c *configs) { c.force = force } } func OnErr(onErr func()) Option { return func(c *configs) { c.onErr = onErr } } func OnSuccess(onSucc func()) Option { return func(c *configs) { c.onSuccess = onSucc } }
然后是新的 create
方法,包括必要字段和可选配置的初始化。因为所有可选的配置都是用 func
类型的返回值初始化的,所以整个配置的赋值只能用一个循环来完成。超级简洁!
func Create(condition metav1.Condition, eventType string, recorder record.EventRecorder, os ...Option) error { opts := configs{ force: false, onSuccess: func() {}, onError: nil, } // Apply all the optional configs for _, applyOption := range os { applyOption(&opts) } // check required fields // update conditions here // handle err if opts.err != nil { return opts.onError() } // eveutally call success func opts.onSuccess() }
调用方可以根据场景选择可选配置,避免误用。
setCondition( metav1.Condition{ Type: apis.Ready, Status: metav1.ConditionFalse, Reason: apis.UpstreamUnavailable, Message: fmt.Sprintf("Failed to set resources %#v", resource), }, "Update", nil, // only need onErr func from the optional configs. conditionAndStatus.ForOnErr(err), )
Builder in Kubernetes
在 Kubernetes
源代码的几乎每个角落都可以看到这种 go
风格的代码。几乎所有的结构被*配置是建立在可选的建造者模式,如 PodApplyConfiguration EventApplyConfiguration
和配置文档你找到包裹。这些逐层嵌套配置获得最终值与一个或多个方法类似于 PodApplyConfiguration
提取。
最后
设计模式是经典的,尽管不是所有的模式都能在 Go
中完美实现。 Builder
无疑是其中最杰出的一个,我们应该最大限度地利用 Optional
管道生成器模式来构建一些配置对象,特别是在设计公共模块时。使用灵活、遵守代码标准和扩展友好的 api
,可以大大减轻升级压力。
感谢你的阅读!
- 探索 API 优先级和公平性以减轻 APIServer 的负载
- Go 中的 Kubernetes GraphQL 动态查询
- 揭开云原生数据管理的神秘面纱:操作层级
- 云原生数据库,激活数智创新之力
- 企业考虑云原生分布式数据库的三个原因
- 一组用于 Kubernetes 的现代 Grafana 仪表板
- Go 中的构建器模式
- 让我们使用 Go 实现基本的服务发现
- 云原生下一步的发展方向是什么?
- 用更云原生的方式做诊断|大规模 K8s 集群诊断利器深度解析
- 多个维度分析k8s多集群管理工具,到底哪个才真正适合你
- 使用 Kube-capacity CLI 查看 Kubernetes 资源请求、限制和利用率
- 使用 Go 在 Kubernetes 中构建自己的准入控制器
- 云原生数仓如何破解大规模集群的关联查询性能问题?
- 云原生趋势下的迁移与灾备思考
- 2022 年不容错过的六大云原生趋势!
- 使用 Prometheus 监控 Golang 应用程序
- 云原生时代下的机遇与挑战 DevOps如何破局
- 如何在云原生格局中理解Kubernetes合规性和安全框架
- 设计云原生应用程序的15条基本原则