目錄
- 一、組合模式(Composite Pattern)
- 二、適配器模式(Adapter Pattern)
- 三、橋接模式(Bridge Pattern)
- 四、總結
一、組合模式(Composite Pattern)

1.1、簡述
在面向對象編程中,有兩個常見的對象設計方法,組合和繼承,兩者都可以解決代碼復用的問題,但是使用后者時容易出現繼承層次過深,對象關系過于復雜的副作用,從而導致代碼的可維護性變差。因此,一個經典的面向對象設計原則是:組合優于繼承。
我們都知道,組合所表示的語義為“has-a”,也就是部分和整體的關系,最經典的組合模式描述如下:
將對象組合成樹形結構以表示“部分-整體”的層次結構,使得用戶對單個對象和組合對象的使用具有一致性。
Go語言天然就支持了組合模式,而且從它不支持繼承關系的特點來看,Go也奉行了組合優于繼承的原則,鼓勵大家在進行程序設計時多采用組合的方法。Go實現組合模式的方式有兩種,分別是直接組合(Direct Composition)和嵌入組合(Embedding Composition),下面我們一起探討這兩種不同的實現方法。
1.2、Go實現
直接組合(Direct Composition)的實現方式類似于Java/C++,就是將一個對象作為另一個對象的成員屬性。
一個典型的實現如《使用Go實現GoF的23種設計模式(一)》中所舉的例子,一個Message結構體,由Header和Body所組成。那么Message就是一個整體,而Header和Body則為消息的組成部分。
type Message struct {
Header *Header
Body *Body
}
現在,我們來看一個稍微復雜一點的例子,同樣考慮上一篇文章中所描述的插件架構風格的消息處理系統。前面我們用抽象工廠模式解決了插件加載的問題,通常,每個插件都會有一個生命周期,常見的就是啟動狀態和停止狀態,現在我們使用組合模式來解決插件的啟動和停止問題。
首先給Plugin接口添加幾個生命周期相關的方法:
package plugin
...
// 插件運行狀態
type Status uint8
const (
Stopped Status = iota
Started
)
type Plugin interface {
// 啟動插件
Start()
// 停止插件
Stop()
// 返回插件當前的運行狀態
Status() Status
}
// Input、Filter、Output三類插件接口的定義跟上一篇文章類似
// 這里使用Message結構體替代了原來的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)
}
對于插件化的消息處理系統而言,一切皆是插件,因此我們將Pipeine也設計成一個插件,實現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三類插件組成,形成了“部分-整體”的關系,而且它們都實現了Plugin接口,這就是一個典型的組合模式的實現。Client無需顯式地啟動和停止Input、Filter和Output插件,在調用Pipeline對象的Start和Stop方法時,Pipeline就已經幫你按順序完成對應插件的啟動和停止。
相比于上一篇文章,在本文中實現Input、Filter、Output三類插件時,需要多實現3個生命周期的方法。還是以上一篇文章中的HelloInput、UpperFilter和ConsoleOutput作為例子,具體實現如下:
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()
}
// 運行結果
=== 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
組合模式的另一種實現,嵌入組合(Embedding Composition),其實就是利用了Go語言的匿名成員特性,本質上跟直接組合是一致的。
還是以Message結構體為例,如果采用嵌入組合,則看起來像是這樣:
type Message struct {
Header
Body
}
// 使用時,Message可以引用Header和Body的成員屬性,例如:
msg := Message{}
msg.SrcAddr = "192.168.0.1"
二、適配器模式(Adapter Pattern)

2.1、簡述
適配器模式是最常用的結構型模式之一,它讓原本因為接口不匹配而無法一起工作的兩個對象能夠一起工作。在現實生活中,適配器模式也是處處可見,比如電源插頭轉換器,可以讓英式的插頭工作在中式的插座上。適配器模式所做的就是將一個接口Adaptee,通過適配器Adapter轉換成Client所期望的另一個接口Target來使用,實現原理也很簡單,就是Adapter通過實現Target接口,并在對應的方法中調用Adaptee的接口實現。
一個典型的應用場景是,系統中一個老的接口已經過時即將廢棄,但因為歷史包袱沒法立即將老接口全部替換為新接口,這時可以新增一個適配器,將老的接口適配成新的接口來使用。適配器模式很好的踐行了面向對象設計原則里的開閉原則(open/closed principle),新增一個接口時也無需修改老接口,只需多加一個適配層即可。
2.2、Go實現
繼續考慮上一節的消息處理系統例子,目前為止,系統的輸入都源自于HelloInput,現在假設需要給系統新增從Kafka消息隊列中接收數據的功能,其中Kafka消費者的接口如下:
package kafka
...
type Records struct {
Items []string
}
type Consumer interface {
Poll() Records
}
由于當前Pipeline的設計是通過plugin.Input接口來進行數據接收,因此kafka.Consumer并不能直接集成到系統中。
怎么辦?使用適配器模式!
為了能讓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()
}
// 在輸入插件映射關系中加入kafka,用于通過反射創建input對象
func init() {
inputNames["hello"] = reflect.TypeOf(HelloInput{})
inputNames["kafka"] = reflect.TypeOf(KafkaInput{})
}
...
因為Go語言并沒有構造函數,如果按照上一篇文章中的抽象工廠模式來創建KafkaInput,那么得到的實例中的consumer成員因為沒有被初始化而會是nil。因此,需要給Plugin接口新增一個Init方法,用于定義插件的一些初始化操作,并在工廠返回實例前調用。
package plugin
...
type Plugin interface {
Start()
Stop()
Status() Status
// 新增初始化方法,在插件工廠返回實例前調用
Init()
}
// 修改后的插件工廠實現如下
func (i *InputFactory) Create(conf Config) Plugin {
t, _ := inputNames[conf.Name]
p := reflect.New(t).Interface().(Plugin)
// 返回插件實例前調用Init函數,完成相關初始化方法
p.Init()
return p
}
// KakkaInput的Init函數實現
func (k *KafkaInput) Init() {
k.consumer = kafka.MockConsumer{}
}
上述代碼中的kafka.MockConsumer為我們模式Kafka消費者的一個實現,代碼如下:
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()
}
// 運行結果
=== 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、簡述
橋接模式主要用于將抽象部分和實現部分進行解耦,使得它們能夠各自往獨立的方向變化。它解決了在模塊有多種變化方向的情況下,用繼承所導致的類爆炸問題。舉一個例子,一個產品有形狀和顏色兩個特征(變化方向),其中形狀分為方形和圓形,顏色分為紅色和藍色。如果采用繼承的設計方案,那么就需要新增4個產品子類:方形紅色、圓形紅色、方形藍色、圓形紅色。如果形狀總共有m種變化,顏色有n種變化,那么就需要新增m*n個產品子類!現在我們使用橋接模式進行優化,將形狀和顏色分別設計為一個抽象接口獨立出來,這樣需要新增2個形狀子類:方形和圓形,以及2個顏色子類:紅色和藍色。同樣,如果形狀總共有m種變化,顏色有n種變化,總共只需要新增m+n個子類!

上述例子中,我們通過將形狀和顏色抽象為一個接口,使產品不再依賴于具體的形狀和顏色細節,從而達到了解耦的目的。橋接模式本質上就是面向接口編程,可以給系統帶來很好的靈活性和可擴展性。如果一個對象存在多個變化的方向,而且每個變化方向都需要擴展,那么使用橋接模式進行設計那是再合適不過了。
3.2、Go實現
回到消息處理系統的例子,一個Pipeline對象主要由Input、Filter、Output三類插件組成(3個特征),因為是插件化的系統,不可避免的就要求支持多種Input、Filter、Output的實現,并能夠靈活組合(有多個變化的方向)。顯然,Pipeline就非常適合使用橋接模式進行設計,實際上我們也這么做了。我們將Input、Filter、Output分別設計成一個抽象的接口,它們按照各自的方向去擴展。Pipeline只依賴的這3個抽象接口,并不感知具體實現的細節。

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
}
// 通過抽象接口來使用,看不到底層的實現細節
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()
}
// 運行結果
=== 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
四、總結
本文主要介紹了結構型模式中的組合模式、適配器模式和橋接模式。組合模式主要解決代碼復用的問題,相比于繼承關系,組合模式可以避免繼承層次過深導致的代碼復雜問題,因此面向對象設計領域流傳著組合優于繼承的原則,而Go語言的設計也很好實踐了該原則;適配器模式可以看作是兩個不兼容接口之間的橋梁,可以將一個接口轉換成Client所希望的另外一個接口,解決了模塊之間因為接口不兼容而無法一起工作的問題;橋接模式將模塊的抽象部分和實現部分進行分離,讓它們能夠往各自的方向擴展,從而達到解耦的目的。
以上就是Go語言設計模式之結構型模式的詳細內容,更多關于Go結構型模式的資料請關注腳本之家其它相關文章!
您可能感興趣的文章:- 詳解Django的MVT設計模式
- 詳解用Go語言實現工廠模式(Golang經典編程案例)
- 如何使用django的MTV開發模式返回一個網頁
- 淺談django三種緩存模式的使用及注意點
- 淺談django開發者模式中的autoreload是如何實現的
- go語言單例模式(Singleton)實例分析