简介
GoF 对桥接模式(Bridge Pattern)的定义如下:
Decouple an abstraction from its implementation so that the two can vary independently.
也即,将抽象部分和实现部分进行解耦,使得它们能够各自往独立的方向变化。
桥接模式解决了在模块有多种变化方向的情况下,用继承所导致的类爆炸问题。
举个例子,一个产品有形状和颜色两个特征(变化方向),其中形状分为方形和圆形,颜色分为红色和蓝色。如果采用继承的设计方案,那么就需要新增4个产品子类:方形红色、圆形红色、方形蓝色、圆形红色。如果形状总共有 m 种变化,颜色有 n 种变化,那么就需要新增 m * n 个产品子类!
现在我们使用桥接模式进行优化,将形状和颜色分别设计为抽象接口独立出来,这样需要新增 2 个形状子类:方形和圆形,以及 2 个颜色子类:红色和蓝色。同样,如果形状总共有 m 种变化,颜色有 n 种变化,总共只需要新增 m + n 个子类!
上述例子中,我们通过将形状和颜色抽象为一个接口,使产品不再依赖于具体的形状和颜色细节,从而达到了解耦的目的。桥接模式本质上就是面向接口编程,可以给系统带来很好的灵活性和可扩展性。如果一个对象存在多个变化的方向,而且每个变化方向都需要扩展,那么使用桥接模式进行设计那是再合适不过了。
当然,Go 语言从语言特性本身就把继承剔除,但桥接模式中分离变化、面向接口编程的思想仍然值得学习。
UML 结构
场景上下文
在简单的分布式应用系统(示例代码工程)中,我们设计了一个 Monitor 监控系统模块,它可以看成是一个简单的 ETL 系统,负责对监控数据进行采集、处理、输出。监控数据来源于在线商场服务集群各个服务,当前通过消息队列模块 Mq 传递到监控系统,经处理后,存储到数据库模块 Db 上。
我们假设未来要上线一个不支持对接消息队列的服务、结果数据也需要存储到 ClickHouse 以供后续分析,为了应对未来多变的需求,我们有必要将监控系统设计得足够的可扩展。
于是,整个模块被设计为插件化风格的架构,Pipeline是数据处理的流水线,其中包含了Input、Filter和Output三类插件,Input负责从各类数据源中获取监控数据,Filter负责数据处理,Output负责将处理后的数据输出。
上述设计中,我们抽象出Input、Filter和Output三类插件,它们各种往独立的方向变化,最后在Pipeline上进行灵活组合,这使用桥接模式正合适。
代码实现
//关键点1:明确产品的变化点,这里是input、filter和output三类插件,它们各自变化 //demo/monitor/input/input_plugin.go packageinput //关键点2:将产品的变化点抽象成接口,这里是input.Plugin,filter.Plugin和output.Plugin //Plugin输入插件 typePlugininterface{ plugin.Plugin Input()(*plugin.Event,error) } //关键点3:实现产品变化点的接口,这里是SocketInput,AddTimestampFilter和MemoryDbOutput //demo/monitor/input/socket_input.go typeSocketInputstruct{ socketnetwork.Socket endpointnetwork.Endpoint packetschan*network.Packet isUninstalluint32 } func(s*SocketInput)Input()(*plugin.Event,error){ packet,ok:=<-s.packets if !ok { return nil, plugin.ErrPluginUninstalled } event := plugin.NewEvent(packet.Payload()) event.AddHeader("peer", packet.Src().String()) return event, nil } // demo/monitor/filter/filter_plugin.go package filter // Plugin 过滤插件 type Plugin interface { plugin.Plugin Filter(event *plugin.Event) *plugin.Event } // demo/monitor/filter/add_timestamp_filter.go // AddTimestampFilter 为MonitorRecord增加时间戳 type AddTimestampFilter struct { } func (a *AddTimestampFilter) Filter(event *plugin.Event) *plugin.Event { re, ok := event.Payload().(*model.MonitorRecord) if !ok { return event } re.Timestamp = time.Now().Unix() return plugin.NewEvent(re) } // demo/monitor/output/output_plugin.go // Plugin 输出插件 type Plugin interface { plugin.Plugin Output(event *plugin.Event) error } // demo/monitor/output/memory_db_output.go type MemoryDbOutput struct { db db.Db tableName string } func (m *MemoryDbOutput) Output(event *plugin.Event) error { r, ok := event.Payload().(*model.MonitorRecord) if !ok { return fmt.Errorf("memory db output unknown event type %T", event.Payload()) } return m.db.Insert(m.tableName, r.Id, r) } // 关键点4:定义产品的接口或者实现,通过组合的方式把变化点桥接起来。 // demo/monitor/pipeline/pipeline_plugin.go // Plugin pipeline由input、filter、output三种插件组成,定义了一个数据处理流程 // 数据流向为 input ->filter->output //如果是接口,可以通过定义Setter方法达到聚合的目的。 typePlugininterface{ plugin.Plugin SetInput(inputinput.Plugin) SetFilter(filterfilter.Plugin) SetOutput(outputoutput.Plugin) } //如果是结构体,直接把变化点作为成员变量来达到聚合的目的。 typepipelineTemplatestruct{ inputinput.Plugin filterfilter.Plugin outputoutput.Plugin isCloseuint32 runfunc() } func(p*pipelineTemplate)SetInput(inputinput.Plugin){ p.input=input } func(p*pipelineTemplate)SetFilter(filterfilter.Plugin){ p.filter=filter } func(p*pipelineTemplate)SetOutput(outputoutput.Plugin){ p.output=output } //demo/monitor/pipeline/simple_pipeline.go //SimplePipeline简单Pipeline实现,每次运行时新启一个goroutine typeSimplePipelinestruct{ pipelineTemplate }
在本系统中,我们通过配置文件来灵活组合插件,利用反射来实现插件的实例化,实例化的实现使用了抽象工厂模式,详细的实现方法可参考【Go实现】实践GoF的23种设计模式:抽象工厂模式。
总结实现桥接模式的几个关键点:
明确产品的变化点,这里是 input、filter 和 output 三类插件,它们各自变化。
将产品的变化点抽象成接口,这里是input.Plugin,filter.Plugin和output.Plugin。
实现产品变化点的接口,这里是SocketInput,AddTimestampFilter和MemoryDbOutput。
定义产品的接口或者实现,通过组合的方式把变化点桥接起来。这里是pipeline.Plugin通过Setter方法将input.Plugin,filter.Plugin和output.Plugin三个抽象接口桥接了起来。后面即可实现各类 input、filter 和 output 的灵活组合了。
扩展
TiDB 中的桥接模式
TiDB是一款出色的分布式关系型数据库,它对外提供了一套插件框架,方便用户进行功能扩展。TiDB 的插件框架的设计,也运用到了桥接模式的思想。
如上图所示,每个Plugin都包含Validate、OnInit、OnShutdown、OnFlush四个待用户实现的接口,它们可以按照各自的方向去变化,然后灵活组合在Plugin中。
//PluginpresentsaTiDBplugin. typePluginstruct{ *Manifest library*gplugin.Plugin Pathstring Disableduint32 StateState } //Manifestdescribesplugininfoandhowitcandobypluginitself. typeManifeststruct{ Namestring Descriptionstring RequireVersionmap[string]uint16 Licensestring BuildTimestring //Validatedefinesthevalidatelogicforplugin. //returnserrorwillstoploadpluginprocessandTiDBstartup. Validatefunc(ctxcontext.Context,manifest*Manifest)error //OnInitdefinestheplugininitlogic. //itwillbecalledafterdomaininit. //returnerrorwillstoploadpluginprocessandTiDBstartup. OnInitfunc(ctxcontext.Context,manifest*Manifest)error //OnShutDowndefinestheplugincleanuplogic. //returnerrorwillwritelogandcontinueshutdown. OnShutdownfunc(ctxcontext.Context,manifest*Manifest)error //OnFlushdefinesflushlogicafterexecuted`flushtidbplugins`. //itwillbecalledafterOnInit. //returnerrorwillwritelogandcontinuewatchfollowingflush. OnFlushfunc(ctxcontext.Context,manifest*Manifest)error flushWatcher*flushWatcher Versionuint16 KindKind }
TiDB 在实现插件框架时,使用函数式编程的方式来定义 OnXXX 接口,更具有 Go 风格。
典型应用场景
从多个维度上对系统/类/结构体进行扩展,如插件化架构。
在运行时切换不同的实现,如插件化架构。
用于构建与平台无关的程序适配层。
优缺点
优点
可实现抽象不分与实现解耦,变化实现时,客户端无须修改代码,符合开闭原则。
每个分离的变化点都可以专注于自身的演进,符合单一职责原则。
缺点
过度的抽象(过度设计)会使得接口膨胀,导致系统复杂性变大,难以维护。
与其他模式的关联
桥接模式通常与抽象工厂模式搭配使用,比如,在本文例子中,可以通过抽象工厂模式对各个 Plugin 完成实例化,详情见【Go实现】实践GoF的23种设计模式:抽象工厂模式。
文章配图
可以在用Keynote画出手绘风格的配图中找到文章的绘图方法。
审核编辑:刘清
-
UML
+关注
关注
0文章
122浏览量
30858 -
数据处理
+关注
关注
0文章
595浏览量
28554 -
go语言
+关注
关注
1文章
158浏览量
9047
原文标题:【Go实现】实践GoF的23种设计模式:桥接模式
文章出处:【微信号:yuanrunzi,微信公众号:元闰子的邀请】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论