0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

为什么要设计模式

马哥Linux运维 来源:云原生CTO 作者:charts 2022-06-14 11:08 次阅读

GoRustPythonIstiocontainerdCoreDNSEnvoyetcdFluentdHarborHelmJaegerKubernetesOpenPolicyAgentPrometheusRookTiKVTUFVitessArgoBuildpacksCloudEventsCNIContourCortexCRI-OFalcoFluxgRPCKubeEdgeLinkerdNATSNotaryOpenTracingOperatorFrameworkSPIFFESPIREThanos


Go 中的构建器模式

Operator 条件更新上应用 Go 风格的构建器模式的实际示例

建议我们在某个“框架”内进行编码,即遵循一定的设计模式,这些模式是有效的、可复制的、被广泛认可的、更容易理解和应用的。

为什么要设计模式

为了不那么抽象,我们从实践中的一个例子开始。

通常,我们定义一个struct,然后在使用它时对其进行初始化。

typeAstruct{
namestring
}
a:=A{name:“abc”}

这是常见的用法,但不适用于A复杂的场景

  • 多层嵌套字段
  • 超过 5 个字段
  • 不同的字段需要不同的默认值
  • 多个可选字段
  • 以上四种的组合

例如在Kubernetes operator开发中,我们调用SetStatusAndCondition来更新资源信息,其中不仅包含了metav1的基本信息。条件,如状态,原因,观察生成等,但也传递回调函数,如OnSuccessOnError。围绕ConditionAndStatus,我们可以添加其他逻辑,比如发送事件、处理不同状态(成功或失败)的逻辑,等等,然后定义一个类似如下的结构。

typeConditionAndStatusstruct{
Conditionmetav1.Condition
EventTypestring//eventtype
Recorderrecord.EventRecorder,//K8seventsrecorder
Forcebool,//isForceupdate
OnErrorfunc,//errhandler
OnSuccesfunc,//successhandler
}

它可以通过通过new初始化这个ConditionAndStatus来工作,但是当有超过5个字段并且其中两个是嵌套的时候,它是累赘和复杂的,这是对非调用者友好的,并且在代码可读性上很差。除非conditioneventRecorder被实例化,否则调用者不能实例化ConditionAndStatus。调用者需要知道所有的实现细节,例如,他们应该知道在错误处理中更新条件时传递onSucc方法,即使只有nil。此外,不同的用户在不同的地方执行初始化时,每次都需要传入相同的onSucconErr

那么我们该如何优化这段代码呢?

Factory 模式

应用Factory模式可能是我们想到的第一个想法,但它不适用于这种情况。

5e4ce432-eb1e-11ec-ba43-dac502259ad0.png

通过Factory模式封装一些创建方法。

//Createnofalse,nodefaulthandlers
func(cConditionAndStatus)Create(condmetav1.Condition,eventTypestring,recorderrecord.EventRecorder)ConditionAndStatus{
returncreate(cond,eventType,recorder,false,nil,nil)
}

//Createnodefaulthandlers
func(cConditionAndStatus)Create(condmetav1.Condition,eventTypestring,recorderrecord.EventRecorder,forcebool)ConditionAndStatus{
returncreate(cond,eventType,recorder,force,nil,nil)
}

//...morecreatefunctions

func(cConditionAndStatus)Create(condmetav1.Condition,eventTypestring,recorderrecord.EventRecorder,forcebool,onErrfunc,onSuccfunc)ConditionAndStatus{
returnConditionAndStatus{
condtion:cond,
eventType:eventType,
recorder:recorder,
force:force,
onErr:onErr,
onSucces:onSucc,
}
}

api应该易于使用且不易误用——来自Josh Bloch

然而,Factory模式实现的api并不是那么方便。

显然,create不是一个选项,因为它需要提供所有参数,传入的参数越多,操作就越困难。此外,当多个参数为同一类型时,很容易出错。

尽管其他Factory方法可以通过提供一些默认值来减少传入的参数来降低复杂性,但它们仍然缺乏灵活性。添加参数后,需要修改所有create *方法。

Builder模式

Builder模式是一种设计模式,旨在为面向对象编程中的各种对象创建问题提供灵活的解决方案

来自https://en.wikipedia.org/wiki/Builder_pattern。

构建器模式为灵活简化复杂对象的创建铺平了道路,同时也隐藏了嵌入式类型的一些初始化细节,大大提高了可读性。

Builder接口

builder接口是两种builder模式实现之一,buildxxx用接口实现各个字段的方法,Builder通过多态性确定具体的builder。请参阅下面的 UML 流程图。

5e5c6420-eb1e-11ec-ba43-dac502259ad0.png

让我们“翻新”以前的ConditionAndStatus.

typeConditionAndStatusBuilderinterface{
SetCondtion(condmetav1.Condition)ConditionAndStatusBuilder
SetEventType(evnetTypestring)ConditionAndStatusBuilder
SetRecorder(recorderrecord.EventRecorder)ConditionAndStatusBuilder
SetForce(forcebool)ConditionAndStatusBuilder
SetOnErr(onErrfunc())ConditionAndStatusBuilder
SetOnSuccess(onSuccfunc())ConditionAndStatusBuilder
Build()ConditionAndStatus
}

typeDefaultBuilderstruct{
conditionmetav1.Condition
eventTypestring//eventtype
recorderrecord.EventRecorder,//K8seventsrecorder
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}

func(b*DefaultBuilder)SetCondtion(condmetav1.Condition)DefaultBuilder{
b.condition=cond
returnb
}
//...moresetfuncs

func(b*DefaultBuilder)Build()ConditionAndStatus{
//setsomedefaultvalues
b.force=true
returnConditionAndStatus{
condition:b.condtion,
//...
}
}

要创建ConditionAndStatus,您可以使用注册方法组成所有构建器,然后通过getByName获得特定的构建器。

不难得出结论,该模式与Factory模式非常相似,因为每个构建器仍然需要创建所有字段或提供默认值。但它确实向前迈出了一步。

  • 当字段确定时,它可以灵活地添加新的生成器,而不需要修改旧的生成器。
  • 它可以控制创建不同字段的顺序。如果字段是相互依赖的,它可以隐藏细节并防止调用者犯错误。

然而,它与Factory模式有相同的缺点:一旦添加了字段(在Builder接口中添加方法),就需要修改所有构建器。

Pipeline建设者

另一种构建器模式是管道构建器,它更常见。通过上面的接口builder,你会发现多builder的设计是多余的,而让调用者控制相关字段的分配更合理:唯一的一个builder管理所有字段初始化,并通过返回builder本身来构建管道在每一步中,最后都组装成我们想要的。

5e8f8990-eb1e-11ec-ba43-dac502259ad0.png

通用调用代码的格式为obj.Withxxx().Withyyy().Withzzz().build(). 更改ConditionAndStatus如下。

typeBuilderstruct{
conditionmetav1.Condition
eventTypestring//eventtype
recorderrecord.EventRecorder,//K8seventsrecorder
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}

func(b*Builder)WithCondition(condmetav1.Condition)Builder{
b.condition=cond
returnb
}
//...moreWithxxxfuncs

func(b*Builder)Build()ConditionAndStatus{
//setsomedefaultvalues
b.force=true
returnConditionAndStatus{
condition:b.condtion,
//...
}
}

Pipeline builder巧妙地避免了添加新字段带来的麻烦。只有一个builder,它可以通过添加With*方法轻松处理字段添加。

它对现有的调用者绝对更友好。如果参数是可选的,则不需要修改其他调用者的代码。而你只有通过添加新的调用者并With*在调用时插入方法来完成它;但是,当需要新参数而没有默认值时,则需要修改所有调用者的代码。

当然,没有一种模式是没有缺陷的,管道构建器也不是。

  • Withxxx()一旦要构建许多字段,堆积的方法会给调用者带来麻烦并降低可读性。
  • 无法控制字段的初始化顺序。如果存在依赖关系,则需要出色的错误控制和文档来避免错误。
  • 代码不是 Go 风格,而是更多 Java 风格。

可选的构建器模式

如果我们进一步优化管道构建器会怎样?正如Dave Cheney在他的Practical Go中提到的那样,我们应该以更多 Go 的方式尝试它。

首选 var args[]T 参数

深入挖掘,我们看到这里的大部分字段都是可选的,并且可以var args自然地定义。如有传入,申报;如果没有,请忽略它。因此,builder/factory当隐藏实现细节时,只需要一种方法来处理整个对象的创建。

让我们一步一步地把这个想法付诸实践。

将可选字段抽象到构建结构中,而不是将所有字段都放入。要将ConditionAndStatus转换为以下结构,其中配置包含所有可选字段。

typeConditionAndStatusstruct{
conditionmetav1.Condition
eventTypestring//eventtype
recorderrecord.EventRecorder,//K8seventsrecorder
configsconfigs//Optionalconfigs
}

typeconfigsstruct{
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}

对于配置,使用func选项接受一个*configs并返回自身以集成到管道中。需要使用以下方法。

typeconfigsstruct{
forcebool,//isForceupdate
onErrorfunc,//errhandler
onSuccesfunc,//successhandler
}

typeOptionfunc(*configs)

funcForceUpdate(forcebool)Option{returnfunc(c*configs){c.force=force}}

funcOnErr(onErrfunc())Option{returnfunc(c*configs){c.onErr=onErr}}

funcOnSuccess(onSuccfunc())Option{returnfunc(c*configs){c.onSuccess=onSucc}}

然后是新的create方法,包括必要字段和可选配置的初始化。因为所有可选的配置都是用func类型的返回值初始化的,所以整个配置的赋值只能用一个循环来完成。超级简洁!

funcCreate(conditionmetav1.Condition,eventTypestring,recorderrecord.EventRecorder,os...Option)error{
opts:=configs{
force:false,
onSuccess:func(){},
onError:nil,
}
//Applyalltheoptionalconfigs
for_,applyOption:=rangeos{
applyOption(&opts)
}
//checkrequiredfields

//updateconditionshere

//handleerr
ifopts.err!=nil{
returnopts.onError()
}

//eveutallycallsuccessfunc
opts.onSuccess()
}

调用方可以根据场景选择可选配置,避免误用。

setCondition(
metav1.Condition{
Type:apis.Ready,
Status:metav1.ConditionFalse,
Reason:apis.UpstreamUnavailable,
Message:fmt.Sprintf("Failedtosetresources%#v",resource),
},
"Update",
nil,
//onlyneedonErrfuncfromtheoptionalconfigs.
conditionAndStatus.ForOnErr(err),
)

Builder in Kubernetes

Kubernetes源代码的几乎每个角落都可以看到这种go风格的代码。几乎所有的结构被*配置是建立在可选的建造者模式,如PodApplyConfiguration EventApplyConfiguration和配置文档你找到包裹。这些逐层嵌套配置获得最终值与一个或多个方法类似于PodApplyConfiguration提取。

最后

设计模式是经典的,尽管不是所有的模式都能在Go中完美实现。Builder无疑是其中最杰出的一个,我们应该最大限度地利用Optional管道生成器模式来构建一些配置对象,特别是在设计公共模块时。使用灵活、遵守代码标准和扩展友好的api,可以大大减轻升级压力。

感谢你的阅读!

原文标题:Go 中的构建器模式

文章出处:【微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 源代码
    +关注

    关注

    94

    文章

    2926

    浏览量

    66061
  • 设计模式
    +关注

    关注

    0

    文章

    53

    浏览量

    8597
  • Struct
    +关注

    关注

    0

    文章

    29

    浏览量

    10817
  • 云原生
    +关注

    关注

    0

    文章

    222

    浏览量

    7843

原文标题:Go 中的构建器模式

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    滚动模式和XY模式如何使用呢?各个模式有什么样的优缺点?

    示波器观察波形有三种视图模式,分别是YT模式、滚动模式、XY模式,虽然多数情况使用YT模式即可,但滚动
    的头像 发表于 10-16 09:12 1.4w次阅读

    什么是菊花链模式 星型模式

    什么是菊花链模式 星型模式 菊花链模式   菊花链模式是简化的级联模式,主要的优点是提供集中管理的扩展端口,对于
    发表于 12-05 09:00 8267次阅读

    重复采样模式,单次采样模式,自动模式与触发模式

    重复采样模式与单次采样模式  过去,最常见的示波器运行模式是重复模式。这意味着一旦示波器触发并将数据显示给用户,它将立即开始搜索下一个触发事件。这就
    发表于 10-10 15:46 1183次阅读

    单例模式分成饿汉模式和懒汉模式解析

    定义:作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式分成饿汉模式和懒汉模式
    发表于 12-11 09:49 1550次阅读

    如何使用纯模式或组件模式进行设计

    了解SelectIO体系结构的详细信息,包括使用纯模式或组件模式以及如何开始使用纯模式或组件模式进行设计。 该视频还介绍了高速SelectIO向导的本机
    的头像 发表于 11-27 06:02 1530次阅读

    电压模式与电流模式的比较

    电压模式控制这是最早的开关稳压器设计所采用的方法,而且多年来很好地满足了业界的需要。本文主要详细阐述了电压模式与电流模式的比较。
    的头像 发表于 12-02 10:45 1.8w次阅读
    电压<b class='flag-5'>模式</b>与电流<b class='flag-5'>模式</b>的比较

    设计模式(1)—什么是设计模式?设计模式的六大原则是什么?

    目录1.什么设计模式2.设计模式的发展3.设计原则6大原则3.1 开闭原则3.2 里氏转换原则3.3 依赖倒转原则3.4 接口隔离原则3.5 合成/聚合复用原则3.6 迪米特原则1.什么设计模式
    发表于 11-07 09:51 8次下载
    设计<b class='flag-5'>模式</b>(1)—什么是设计<b class='flag-5'>模式</b>?设计<b class='flag-5'>模式</b>的六大原则是什么?

    STM8 ADC转换模式-------单次模式

    STM8单片机ADC支持5种转换模式:单次模式,连续模式,带缓存的连续模式,单次扫描模式,连续扫描模式
    发表于 12-27 18:33 9次下载
    STM8 ADC转换<b class='flag-5'>模式</b>-------单次<b class='flag-5'>模式</b>

    cmd命令快速切换电源模式-平衡模式和卓越模式

    因生活需要经常切换电源模式,来回切换电源模式,这是一键快捷切换电源模式。代码视频如下:下面展示一些 内联代码片。@echo off:menuclsecho
    发表于 01-11 11:24 2次下载
    cmd命令快速切换电源<b class='flag-5'>模式</b>-平衡<b class='flag-5'>模式</b>和卓越<b class='flag-5'>模式</b>

    设计模式行为型:备忘录模式

    备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式
    的头像 发表于 06-07 11:16 586次阅读
    设计<b class='flag-5'>模式</b>行为型:备忘录<b class='flag-5'>模式</b>

    设计模式行为型:策略模式

    在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式
    的头像 发表于 06-07 11:18 429次阅读
    设计<b class='flag-5'>模式</b>行为型:策略<b class='flag-5'>模式</b>

    设计模式行为型:状态模式

    在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式
    的头像 发表于 06-07 11:20 383次阅读
    设计<b class='flag-5'>模式</b>行为型:状态<b class='flag-5'>模式</b>

    设计模式行为型:观察者模式

    定义对象之间的一种一对多依赖关系,使得每一个对象发生状态的变化时,其相关依赖对象皆得到通知并被自动更新,又称为发布-订阅模式、模型-视图模式、源-监听器模式或从属者模式
    的头像 发表于 06-07 16:56 460次阅读
    设计<b class='flag-5'>模式</b>行为型:观察者<b class='flag-5'>模式</b>

    设计模式结构性:代理模式

    在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式
    的头像 发表于 06-09 15:27 592次阅读
    设计<b class='flag-5'>模式</b>结构性:代理<b class='flag-5'>模式</b>

    设计模式创造性:建造者模式

    建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
    的头像 发表于 06-09 16:25 553次阅读
    设计<b class='flag-5'>模式</b>创造性:建造者<b class='flag-5'>模式</b>