Go語言設(shè)計模式之結(jié)構(gòu)型模式
一、組合模式(Composite Pattern)
1.1、簡述
在面向?qū)ο缶幊讨?,有兩個常見的對象設(shè)計方法,組合和繼承,兩者都可以解決代碼復用的問題,但是使用后者時容易出現(xiàn)繼承層次過深,對象關(guān)系過于復雜的副作用,從而導致代碼的可維護性變差。因此,一個經(jīng)典的面向?qū)ο笤O(shè)計原則是:組合優(yōu)于繼承。
我們都知道,組合所表示的語義為“has-a”,也就是部分和整體的關(guān)系,最經(jīng)典的組合模式描述如下:
將對象組合成樹形結(jié)構(gòu)以表示“部分-整體”的層次結(jié)構(gòu),使得用戶對單個對象和組合對象的使用具有一致性。
Go語言天然就支持了組合模式,而且從它不支持繼承關(guān)系的特點來看,Go也奉行了組合優(yōu)于繼承的原則,鼓勵大家在進行程序設(shè)計時多采用組合的方法。Go實現(xiàn)組合模式的方式有兩種,分別是直接組合(Direct Composition)和嵌入組合(Embedding Composition),下面我們一起探討這兩種不同的實現(xiàn)方法。
1.2、Go實現(xiàn)
直接組合(Direct Composition)的實現(xiàn)方式類似于Java/C++,就是將一個對象作為另一個對象的成員屬性。
一個典型的實現(xiàn)如《使用Go實現(xiàn)GoF的23種設(shè)計模式(一)》中所舉的例子,一個Message結(jié)構(gòu)體,由Header和Body所組成。那么Message就是一個整體,而Header和Body則為消息的組成部分。
type Message struct { Header *Header Body *Body }
現(xiàn)在,我們來看一個稍微復雜一點的例子,同樣考慮上一篇文章中所描述的插件架構(gòu)風格的消息處理系統(tǒng)。前面我們用抽象工廠模式解決了插件加載的問題,通常,每個插件都會有一個生命周期,常見的就是啟動狀態(tài)和停止狀態(tài),現(xiàn)在我們使用組合模式來解決插件的啟動和停止問題。
首先給Plugin接口添加幾個生命周期相關(guān)的方法:
package plugin ... // 插件運行狀態(tài) type Status uint8 const ( Stopped Status = iota Started ) type Plugin interface { // 啟動插件 Start() // 停止插件 Stop() // 返回插件當前的運行狀態(tài) Status() Status } // Input、Filter、Output三類插件接口的定義跟上一篇文章類似 // 這里使用Message結(jié)構(gòu)體替代了原來的string,使得語義更清晰 type Input interface { Plugin Receive() *msg.Message } type Filter interface { Plugin Process(msg *msg.Message) *msg.Message } type Output interface { Plugin Send(msg *msg.Message) }
對于插件化的消息處理系統(tǒng)而言,一切皆是插件,因此我們將Pipeine也設(shè)計成一個插件,實現(xiàn)Plugin接口:
package pipeline ... // 一個Pipeline由input、filter、output三個Plugin組成 type Pipeline struct { status plugin.Status input plugin.Input filter plugin.Filter output plugin.Output } func (p *Pipeline) Exec() { msg := p.input.Receive() msg = p.filter.Process(msg) p.output.Send(msg) } // 啟動的順序 output -> filter -> input func (p *Pipeline) Start() { p.output.Start() p.filter.Start() p.input.Start() p.status = plugin.Started fmt.Println("Hello input plugin started.") } // 停止的順序 input -> filter -> output func (p *Pipeline) Stop() { p.input.Stop() p.filter.Stop() p.output.Stop() p.status = plugin.Stopped fmt.Println("Hello input plugin stopped.") } func (p *Pipeline) Status() plugin.Status { return p.status }
一個Pipeline由Input、Filter、Output三類插件組成,形成了“部分-整體”的關(guān)系,而且它們都實現(xiàn)了Plugin接口,這就是一個典型的組合模式的實現(xiàn)。Client無需顯式地啟動和停止Input、Filter和Output插件,在調(diào)用Pipeline對象的Start和Stop方法時,Pipeline就已經(jīng)幫你按順序完成對應(yīng)插件的啟動和停止。
相比于上一篇文章,在本文中實現(xiàn)Input、Filter、Output三類插件時,需要多實現(xiàn)3個生命周期的方法。還是以上一篇文章中的HelloInput、UpperFilter和ConsoleOutput作為例子,具體實現(xiàn)如下:
package plugin ... type HelloInput struct { status Status } func (h *HelloInput) Receive() *msg.Message { // 如果插件未啟動,則返回nil if h.status != Started { fmt.Println("Hello input plugin is not running, input nothing.") return nil } return msg.Builder(). WithHeaderItem("content", "text"). WithBodyItem("Hello World"). Build() } func (h *HelloInput) Start() { h.status = Started fmt.Println("Hello input plugin started.") } func (h *HelloInput) Stop() { h.status = Stopped fmt.Println("Hello input plugin stopped.") } func (h *HelloInput) Status() Status { return h.status } package plugin ... type UpperFilter struct { status Status } func (u *UpperFilter) Process(msg *msg.Message) *msg.Message { if u.status != Started { fmt.Println("Upper filter plugin is not running, filter nothing.") return msg } for i, val := range msg.Body.Items { msg.Body.Items[i] = strings.ToUpper(val) } return msg } func (u *UpperFilter) Start() { u.status = Started fmt.Println("Upper filter plugin started.") } func (u *UpperFilter) Stop() { u.status = Stopped fmt.Println("Upper filter plugin stopped.") } func (u *UpperFilter) Status() Status { return u.status } package plugin ... type ConsoleOutput struct { status Status } func (c *ConsoleOutput) Send(msg *msg.Message) { if c.status != Started { fmt.Println("Console output is not running, output nothing.") return } fmt.Printf("Output:\n\tHeader:%+v, Body:%+v\n", msg.Header.Items, msg.Body.Items) } func (c *ConsoleOutput) Start() { c.status = Started fmt.Println("Console output plugin started.") } func (c *ConsoleOutput) Stop() { c.status = Stopped fmt.Println("Console output plugin stopped.") } func (c *ConsoleOutput) Status() Status { return c.status }
測試代碼如下:
package test ... func TestPipeline(t *testing.T) { p := pipeline.Of(pipeline.DefaultConfig()) p.Start() p.Exec() p.Stop() } // 運行結(jié)果 === RUN TestPipeline Console output plugin started. Upper filter plugin started. Hello input plugin started. Pipeline started. Output: Header:map[content:text], Body:[HELLO WORLD] Hello input plugin stopped. Upper filter plugin stopped. Console output plugin stopped. Hello input plugin stopped. --- PASS: TestPipeline (0.00s) PASS
組合模式的另一種實現(xiàn),嵌入組合(Embedding Composition),其實就是利用了Go語言的匿名成員特性,本質(zhì)上跟直接組合是一致的。
還是以Message結(jié)構(gòu)體為例,如果采用嵌入組合,則看起來像是這樣:
type Message struct { Header Body } // 使用時,Message可以引用Header和Body的成員屬性,例如: msg := &Message{} msg.SrcAddr = "192.168.0.1"
二、適配器模式(Adapter Pattern)
2.1、簡述
適配器模式是最常用的結(jié)構(gòu)型模式之一,它讓原本因為接口不匹配而無法一起工作的兩個對象能夠一起工作。在現(xiàn)實生活中,適配器模式也是處處可見,比如電源插頭轉(zhuǎn)換器,可以讓英式的插頭工作在中式的插座上。適配器模式所做的就是將一個接口Adaptee,通過適配器Adapter轉(zhuǎn)換成Client所期望的另一個接口Target來使用,實現(xiàn)原理也很簡單,就是Adapter通過實現(xiàn)Target接口,并在對應(yīng)的方法中調(diào)用Adaptee的接口實現(xiàn)。
一個典型的應(yīng)用場景是,系統(tǒng)中一個老的接口已經(jīng)過時即將廢棄,但因為歷史包袱沒法立即將老接口全部替換為新接口,這時可以新增一個適配器,將老的接口適配成新的接口來使用。適配器模式很好的踐行了面向?qū)ο笤O(shè)計原則里的開閉原則(open/closed principle),新增一個接口時也無需修改老接口,只需多加一個適配層即可。
2.2、Go實現(xiàn)
繼續(xù)考慮上一節(jié)的消息處理系統(tǒng)例子,目前為止,系統(tǒng)的輸入都源自于HelloInput,現(xiàn)在假設(shè)需要給系統(tǒng)新增從Kafka消息隊列中接收數(shù)據(jù)的功能,其中Kafka消費者的接口如下:
package kafka ... type Records struct { Items []string } type Consumer interface { Poll() Records }
由于當前Pipeline的設(shè)計是通過plugin.Input接口來進行數(shù)據(jù)接收,因此kafka.Consumer并不能直接集成到系統(tǒng)中。
怎么辦?使用適配器模式!
為了能讓Pipeline能夠使用kafka.Consumer接口,我們需要定義一個適配器如下:
package plugin ... type KafkaInput struct { status Status consumer kafka.Consumer } func (k *KafkaInput) Receive() *msg.Message { records := k.consumer.Poll() if k.status != Started { fmt.Println("Kafka input plugin is not running, input nothing.") return nil } return msg.Builder(). WithHeaderItem("content", "text"). WithBodyItems(records.Items). Build() } // 在輸入插件映射關(guān)系中加入kafka,用于通過反射創(chuàng)建input對象 func init() { inputNames["hello"] = reflect.TypeOf(HelloInput{}) inputNames["kafka"] = reflect.TypeOf(KafkaInput{}) } ...
因為Go語言并沒有構(gòu)造函數(shù),如果按照上一篇文章中的抽象工廠模式來創(chuàng)建KafkaInput,那么得到的實例中的consumer成員因為沒有被初始化而會是nil。因此,需要給Plugin接口新增一個Init方法,用于定義插件的一些初始化操作,并在工廠返回實例前調(diào)用。
package plugin ... type Plugin interface { Start() Stop() Status() Status // 新增初始化方法,在插件工廠返回實例前調(diào)用 Init() } // 修改后的插件工廠實現(xiàn)如下 func (i *InputFactory) Create(conf Config) Plugin { t, _ := inputNames[conf.Name] p := reflect.New(t).Interface().(Plugin) // 返回插件實例前調(diào)用Init函數(shù),完成相關(guān)初始化方法 p.Init() return p } // KakkaInput的Init函數(shù)實現(xiàn) func (k *KafkaInput) Init() { k.consumer = &kafka.MockConsumer{} }
上述代碼中的kafka.MockConsumer為我們模式Kafka消費者的一個實現(xiàn),代碼如下:
package kafka ... type MockConsumer struct {} func (m *MockConsumer) Poll() *Records { records := &Records{} records.Items = append(records.Items, "i am mock consumer.") return records }
測試代碼如下:
package test ... func TestKafkaInputPipeline(t *testing.T) { config := pipeline.Config{ Name: "pipeline2", Input: plugin.Config{ PluginType: plugin.InputType, Name: "kafka", }, Filter: plugin.Config{ PluginType: plugin.FilterType, Name: "upper", }, Output: plugin.Config{ PluginType: plugin.OutputType, Name: "console", }, } p := pipeline.Of(config) p.Start() p.Exec() p.Stop() } // 運行結(jié)果 === RUN TestKafkaInputPipeline Console output plugin started. Upper filter plugin started. Kafka input plugin started. Pipeline started. Output: Header:map[content:kafka], Body:[I AM MOCK CONSUMER.] Kafka input plugin stopped. Upper filter plugin stopped. Console output plugin stopped. Pipeline stopped. --- PASS: TestKafkaInputPipeline (0.00s) PASS
三、橋接模式(Bridge Pattern)
3.1、簡述
橋接模式主要用于將抽象部分和實現(xiàn)部分進行解耦,使得它們能夠各自往獨立的方向變化。它解決了在模塊有多種變化方向的情況下,用繼承所導致的類爆炸問題。舉一個例子,一個產(chǎn)品有形狀和顏色兩個特征(變化方向),其中形狀分為方形和圓形,顏色分為紅色和藍色。如果采用繼承的設(shè)計方案,那么就需要新增4個產(chǎn)品子類:方形紅色、圓形紅色、方形藍色、圓形紅色。如果形狀總共有m種變化,顏色有n種變化,那么就需要新增m*n個產(chǎn)品子類!現(xiàn)在我們使用橋接模式進行優(yōu)化,將形狀和顏色分別設(shè)計為一個抽象接口獨立出來,這樣需要新增2個形狀子類:方形和圓形,以及2個顏色子類:紅色和藍色。同樣,如果形狀總共有m種變化,顏色有n種變化,總共只需要新增m+n個子類!
上述例子中,我們通過將形狀和顏色抽象為一個接口,使產(chǎn)品不再依賴于具體的形狀和顏色細節(jié),從而達到了解耦的目的。橋接模式本質(zhì)上就是面向接口編程,可以給系統(tǒng)帶來很好的靈活性和可擴展性。如果一個對象存在多個變化的方向,而且每個變化方向都需要擴展,那么使用橋接模式進行設(shè)計那是再合適不過了。
3.2、Go實現(xiàn)
回到消息處理系統(tǒng)的例子,一個Pipeline對象主要由Input、Filter、Output三類插件組成(3個特征),因為是插件化的系統(tǒng),不可避免的就要求支持多種Input、Filter、Output的實現(xiàn),并能夠靈活組合(有多個變化的方向)。顯然,Pipeline就非常適合使用橋接模式進行設(shè)計,實際上我們也這么做了。我們將Input、Filter、Output分別設(shè)計成一個抽象的接口,它們按照各自的方向去擴展。Pipeline只依賴的這3個抽象接口,并不感知具體實現(xiàn)的細節(jié)。
package plugin ... type Input interface { Plugin Receive() *msg.Message } type Filter interface { Plugin Process(msg *msg.Message) *msg.Message } type Output interface { Plugin Send(msg *msg.Message) } package pipeline ... // 一個Pipeline由input、filter、output三個Plugin組成 type Pipeline struct { status plugin.Status input plugin.Input filter plugin.Filter output plugin.Output } // 通過抽象接口來使用,看不到底層的實現(xiàn)細節(jié) func (p *Pipeline) Exec() { msg := p.input.Receive() msg = p.filter.Process(msg) p.output.Send(msg) }
測試代碼如下:
package test ... func TestPipeline(t *testing.T) { p := pipeline.Of(pipeline.DefaultConfig()) p.Start() p.Exec() p.Stop() } // 運行結(jié)果 === RUN TestPipeline Console output plugin started. Upper filter plugin started. Hello input plugin started. Pipeline started. Output: Header:map[content:text], Body:[HELLO WORLD] Hello input plugin stopped. Upper filter plugin stopped. Console output plugin stopped. Pipeline stopped. --- PASS: TestPipeline (0.00s) PASS
四、總結(jié)
本文主要介紹了結(jié)構(gòu)型模式中的組合模式、適配器模式和橋接模式。組合模式主要解決代碼復用的問題,相比于繼承關(guān)系,組合模式可以避免繼承層次過深導致的代碼復雜問題,因此面向?qū)ο笤O(shè)計領(lǐng)域流傳著組合優(yōu)于繼承的原則,而Go語言的設(shè)計也很好實踐了該原則;適配器模式可以看作是兩個不兼容接口之間的橋梁,可以將一個接口轉(zhuǎn)換成Client所希望的另外一個接口,解決了模塊之間因為接口不兼容而無法一起工作的問題;橋接模式將模塊的抽象部分和實現(xiàn)部分進行分離,讓它們能夠往各自的方向擴展,從而達到解耦的目的。
以上就是Go語言設(shè)計模式之結(jié)構(gòu)型模式的詳細內(nèi)容,更多關(guān)于Go結(jié)構(gòu)型模式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go引入自建包名報錯:package?XXX?is?not?in?std解決辦法
這篇文章主要給大家介紹了go引入自建包名報錯:package?XXX?is?not?in?std的解決辦法,這是在寫測試引入包名的時候遇到的錯誤提示,文中將解決辦法介紹的非常詳細,需要的朋友可以參考下2023-12-12安裝GoLang環(huán)境和開發(fā)工具的圖文教程
Go是一門由Google開發(fā)的編程語言,GoLand的安裝非常簡單,本文主要介紹了安裝GoLang環(huán)境和開發(fā)工具的圖文教程,具有一定的參考價值,感興趣的可以了解一下2023-09-09Golang之sync.Pool對象池對象重用機制總結(jié)
這篇文章主要對Golang的sync.Pool對象池對象重用機制做了一個總結(jié),文中有相關(guān)的代碼示例和圖解,具有一定的參考價值,需要的朋友可以參考下2023-07-07Golang利用compress/flate包來壓縮和解壓數(shù)據(jù)
在處理需要高效存儲和快速傳輸?shù)臄?shù)據(jù)時,數(shù)據(jù)壓縮成為了一項不可或缺的技術(shù),Go語言的compress/flate包為我們提供了對DEFLATE壓縮格式的原生支持,本文將深入探討compress/flate包的使用方法,揭示如何利用它來壓縮和解壓數(shù)據(jù),并提供實際的代碼示例,需要的朋友可以參考下2024-08-08Golang應(yīng)用執(zhí)行Shell命令實戰(zhàn)
本文主要介紹了Golang應(yīng)用執(zhí)行Shell命令實戰(zhàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-03-03使用Go語言實現(xiàn)跨域資源共享(CORS)設(shè)置
在Web開發(fā)中,跨域資源共享(CORS)是一種重要的安全機制,它允許許多資源在一個網(wǎng)頁上被另一個來源的網(wǎng)頁所訪問,然而,出于安全考慮,瀏覽器默認禁止這種跨域訪問,為了解決這個問題,我們可以使用Go語言來設(shè)置CORS,需要的朋友可以參考下2024-06-06