簡介
GoF 對橋接模式(Bridge Pattern)的定義如下:
Decouple an abstraction from its implementation so that the two can vary independently.
也即,將抽象部分和實(shí)現(xiàn)部分進(jìn)行解耦,使得它們能夠各自往獨(dú)立的方向變化。
橋接模式解決了在模塊有多種變化方向的情況下,用繼承所導(dǎo)致的類爆炸問題。
舉個(gè)例子,一個(gè)產(chǎn)品有形狀和顏色兩個(gè)特征(變化方向),其中形狀分為方形和圓形,顏色分為紅色和藍(lán)色。如果采用繼承的設(shè)計(jì)方案,那么就需要新增4個(gè)產(chǎn)品子類:方形紅色、圓形紅色、方形藍(lán)色、圓形紅色。如果形狀總共有 m 種變化,顏色有 n 種變化,那么就需要新增 m * n 個(gè)產(chǎn)品子類!
現(xiàn)在我們使用橋接模式進(jìn)行優(yōu)化,將形狀和顏色分別設(shè)計(jì)為抽象接口獨(dú)立出來,這樣需要新增 2 個(gè)形狀子類:方形和圓形,以及 2 個(gè)顏色子類:紅色和藍(lán)色。同樣,如果形狀總共有 m 種變化,顏色有 n 種變化,總共只需要新增 m + n 個(gè)子類!
上述例子中,我們通過將形狀和顏色抽象為一個(gè)接口,使產(chǎn)品不再依賴于具體的形狀和顏色細(xì)節(jié),從而達(dá)到了解耦的目的。橋接模式本質(zhì)上就是面向接口編程,可以給系統(tǒng)帶來很好的靈活性和可擴(kuò)展性。如果一個(gè)對象存在多個(gè)變化的方向,而且每個(gè)變化方向都需要擴(kuò)展,那么使用橋接模式進(jìn)行設(shè)計(jì)那是再合適不過了。
當(dāng)然,Go 語言從語言特性本身就把繼承剔除,但橋接模式中分離變化、面向接口編程的思想仍然值得學(xué)習(xí)。
UML 結(jié)構(gòu)
場景上下文
在簡單的分布式應(yīng)用系統(tǒng)(示例代碼工程)中,我們設(shè)計(jì)了一個(gè) Monitor 監(jiān)控系統(tǒng)模塊,它可以看成是一個(gè)簡單的 ETL 系統(tǒng),負(fù)責(zé)對監(jiān)控?cái)?shù)據(jù)進(jìn)行采集、處理、輸出。監(jiān)控?cái)?shù)據(jù)來源于在線商場服務(wù)集群各個(gè)服務(wù),當(dāng)前通過消息隊(duì)列模塊 Mq 傳遞到監(jiān)控系統(tǒng),經(jīng)處理后,存儲(chǔ)到數(shù)據(jù)庫模塊 Db 上。
我們假設(shè)未來要上線一個(gè)不支持對接消息隊(duì)列的服務(wù)、結(jié)果數(shù)據(jù)也需要存儲(chǔ)到 ClickHouse 以供后續(xù)分析,為了應(yīng)對未來多變的需求,我們有必要將監(jiān)控系統(tǒng)設(shè)計(jì)得足夠的可擴(kuò)展。
于是,整個(gè)模塊被設(shè)計(jì)為插件化風(fēng)格的架構(gòu),Pipeline是數(shù)據(jù)處理的流水線,其中包含了Input、Filter和Output三類插件,Input負(fù)責(zé)從各類數(shù)據(jù)源中獲取監(jiān)控?cái)?shù)據(jù),F(xiàn)ilter負(fù)責(zé)數(shù)據(jù)處理,Output負(fù)責(zé)將處理后的數(shù)據(jù)輸出。
上述設(shè)計(jì)中,我們抽象出Input、Filter和Output三類插件,它們各種往獨(dú)立的方向變化,最后在Pipeline上進(jìn)行靈活組合,這使用橋接模式正合適。
代碼實(shí)現(xiàn)
//關(guān)鍵點(diǎn)1:明確產(chǎn)品的變化點(diǎn),這里是input、filter和output三類插件,它們各自變化 //demo/monitor/input/input_plugin.go packageinput //關(guān)鍵點(diǎn)2:將產(chǎn)品的變化點(diǎn)抽象成接口,這里是input.Plugin,filter.Plugin和output.Plugin //Plugin輸入插件 typePlugininterface{ plugin.Plugin Input()(*plugin.Event,error) } //關(guān)鍵點(diǎn)3:實(shí)現(xiàn)產(chǎn)品變化點(diǎn)的接口,這里是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增加時(shí)間戳 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) } //?關(guān)鍵點(diǎn)4:定義產(chǎn)品的接口或者實(shí)現(xiàn),通過組合的方式把變化點(diǎn)橋接起來。 //?demo/monitor/pipeline/pipeline_plugin.go //?Plugin?pipeline由input、filter、output三種插件組成,定義了一個(gè)數(shù)據(jù)處理流程 //?數(shù)據(jù)流向?yàn)?input?->filter->output //如果是接口,可以通過定義Setter方法達(dá)到聚合的目的。 typePlugininterface{ plugin.Plugin SetInput(inputinput.Plugin) SetFilter(filterfilter.Plugin) SetOutput(outputoutput.Plugin) } //如果是結(jié)構(gòu)體,直接把變化點(diǎn)作為成員變量來達(dá)到聚合的目的。 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實(shí)現(xiàn),每次運(yùn)行時(shí)新啟一個(gè)goroutine typeSimplePipelinestruct{ pipelineTemplate }
在本系統(tǒng)中,我們通過配置文件來靈活組合插件,利用反射來實(shí)現(xiàn)插件的實(shí)例化,實(shí)例化的實(shí)現(xiàn)使用了抽象工廠模式,詳細(xì)的實(shí)現(xiàn)方法可參考【Go實(shí)現(xiàn)】實(shí)踐GoF的23種設(shè)計(jì)模式:抽象工廠模式。
總結(jié)實(shí)現(xiàn)橋接模式的幾個(gè)關(guān)鍵點(diǎn):
明確產(chǎn)品的變化點(diǎn),這里是 input、filter 和 output 三類插件,它們各自變化。
將產(chǎn)品的變化點(diǎn)抽象成接口,這里是input.Plugin,filter.Plugin和output.Plugin。
實(shí)現(xiàn)產(chǎn)品變化點(diǎn)的接口,這里是SocketInput,AddTimestampFilter和MemoryDbOutput。
定義產(chǎn)品的接口或者實(shí)現(xiàn),通過組合的方式把變化點(diǎn)橋接起來。這里是pipeline.Plugin通過Setter方法將input.Plugin,filter.Plugin和output.Plugin三個(gè)抽象接口橋接了起來。后面即可實(shí)現(xiàn)各類 input、filter 和 output 的靈活組合了。
擴(kuò)展
TiDB 中的橋接模式
TiDB是一款出色的分布式關(guān)系型數(shù)據(jù)庫,它對外提供了一套插件框架,方便用戶進(jìn)行功能擴(kuò)展。TiDB 的插件框架的設(shè)計(jì),也運(yùn)用到了橋接模式的思想。
如上圖所示,每個(gè)Plugin都包含Validate、OnInit、OnShutdown、OnFlush四個(gè)待用戶實(shí)現(xiàn)的接口,它們可以按照各自的方向去變化,然后靈活組合在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 在實(shí)現(xiàn)插件框架時(shí),使用函數(shù)式編程的方式來定義 OnXXX 接口,更具有 Go 風(fēng)格。
典型應(yīng)用場景
從多個(gè)維度上對系統(tǒng)/類/結(jié)構(gòu)體進(jìn)行擴(kuò)展,如插件化架構(gòu)。
在運(yùn)行時(shí)切換不同的實(shí)現(xiàn),如插件化架構(gòu)。
用于構(gòu)建與平臺(tái)無關(guān)的程序適配層。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
可實(shí)現(xiàn)抽象不分與實(shí)現(xiàn)解耦,變化實(shí)現(xiàn)時(shí),客戶端無須修改代碼,符合開閉原則。
每個(gè)分離的變化點(diǎn)都可以專注于自身的演進(jìn),符合單一職責(zé)原則。
缺點(diǎn)
過度的抽象(過度設(shè)計(jì))會(huì)使得接口膨脹,導(dǎo)致系統(tǒng)復(fù)雜性變大,難以維護(hù)。
與其他模式的關(guān)聯(lián)
橋接模式通常與抽象工廠模式搭配使用,比如,在本文例子中,可以通過抽象工廠模式對各個(gè) Plugin 完成實(shí)例化,詳情見【Go實(shí)現(xiàn)】實(shí)踐GoF的23種設(shè)計(jì)模式:抽象工廠模式。
文章配圖
可以在用Keynote畫出手繪風(fēng)格的配圖中找到文章的繪圖方法。
審核編輯:劉清
-
UML
+關(guān)注
關(guān)注
0文章
122瀏覽量
30861 -
數(shù)據(jù)處理
+關(guān)注
關(guān)注
0文章
599瀏覽量
28568 -
go語言
+關(guān)注
關(guān)注
1文章
158瀏覽量
9049
原文標(biāo)題:【Go實(shí)現(xiàn)】實(shí)踐GoF的23種設(shè)計(jì)模式:橋接模式
文章出處:【微信號(hào):yuanrunzi,微信公眾號(hào):元閏子的邀請】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論