Go語言實現的23種設計模式之結構型模式

華為雲開發者社群發表於2021-06-21
摘要:本文主要聚焦在結構型模式(Structural Pattern)上,其主要思想是將多個物件組裝成較大的結構,並同時保持結構的靈活和高效,從程式的結構上解決模組之間的耦合問題。

本文分享自華為雲社群《快來,這裡有23種設計模式的Go語言實現(二)》,原文作者:元閏子。

本文主要聚焦在結構型模式(Structural Pattern)上,其主要思想是將多個物件組裝成較大的結構,並同時保持結構的靈活和高效,從程式的結構上解決模組之間的耦合問題。

組合模式(Composite Pattern)

Go語言實現的23種設計模式之結構型模式

簡述

在物件導向程式設計中,有兩個常見的物件設計方法,組合和繼承,兩者都可以解決程式碼複用的問題,但是使用後者時容易出現繼承層次過深,物件關係過於複雜的副作用,從而導致程式碼的可維護性變差。因此,一個經典的物件導向設計原則是:組合優於繼承。

我們都知道,組合所表示的語義為“has-a”,也就是部分和整體的關係,最經典的組合模式描述如下:

將物件組合成樹形結構以表示“部分-整體”的層次結構,使得使用者對單個物件和組合物件的使用具有一致性。

Go語言天然就支援了組合模式,而且從它不支援繼承關係的特點來看,Go也奉行了組合優於繼承的原則,鼓勵大家在進行程式設計時多采用組合的方法。Go實現組合模式的方式有兩種,分別是直接組合(Direct Composition)和嵌入組合(Embedding Composition),下面我們一起探討這兩種不同的實現方法。

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)

Go語言實現的23種設計模式之結構型模式

簡述

介面卡模式是最常用的結構型模式之一,它讓原本因為介面不匹配而無法一起工作的兩個物件能夠一起工作。在現實生活中,介面卡模式也是處處可見,比如電源插頭轉換器,可以讓英式的插頭工作在中式的插座上。介面卡模式所做的就是將一個介面Adaptee,通過介面卡Adapter轉換成Client所期望的另一個介面Target來使用,實現原理也很簡單,就是Adapter通過實現Target介面,並在對應的方法中呼叫Adaptee的介面實現。

一個典型的應用場景是,系統中一個老的介面已經過時即將廢棄,但因為歷史包袱沒法立即將老介面全部替換為新介面,這時可以新增一個介面卡,將老的介面適配成新的介面來使用。介面卡模式很好的踐行了物件導向設計原則裡的開閉原則(open/closed principle),新增一個介面時也無需修改老介面,只需多加一個適配層即可。

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)

Go語言實現的23種設計模式之結構型模式

簡述

橋接模式主要用於將抽象部分和實現部分進行解耦,使得它們能夠各自往獨立的方向變化。它解決了在模組有多種變化方向的情況下,用繼承所導致的類爆炸問題。舉一個例子,一個產品有形狀和顏色兩個特徵(變化方向),其中形狀分為方形和圓形,顏色分為紅色和藍色。如果採用繼承的設計方案,那麼就需要新增4個產品子類:方形紅色、圓形紅色、方形藍色、圓形紅色。如果形狀總共有m種變化,顏色有n種變化,那麼就需要新增m*n個產品子類!現在我們使用橋接模式進行優化,將形狀和顏色分別設計為一個抽象介面獨立出來,這樣需要新增2個形狀子類:方形和圓形,以及2個顏色子類:紅色和藍色。同樣,如果形狀總共有m種變化,顏色有n種變化,總共只需要新增m+n個子類!

Go語言實現的23種設計模式之結構型模式

上述例子中,我們通過將形狀和顏色抽象為一個介面,使產品不再依賴於具體的形狀和顏色細節,從而達到了解耦的目的。橋接模式本質上就是面向介面程式設計,可以給系統帶來很好的靈活性和可擴充套件性。如果一個物件存在多個變化的方向,而且每個變化方向都需要擴充套件,那麼使用橋接模式進行設計那是再合適不過了。

Go實現

回到訊息處理系統的例子,一個Pipeline物件主要由Input、Filter、Output三類外掛組成(3個特徵),因為是外掛化的系統,不可避免的就要求支援多種Input、Filter、Output的實現,並能夠靈活組合(有多個變化的方向)。顯然,Pipeline就非常適合使用橋接模式進行設計,實際上我們也這麼做了。我們將Input、Filter、Output分別設計成一個抽象的介面,它們按照各自的方向去擴充套件。Pipeline只依賴的這3個抽象介面,並不感知具體實現的細節。

Go語言實現的23種設計模式之結構型模式

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所希望的另外一個介面,解決了模組之間因為介面不相容而無法一起工作的問題;橋接模式將模組的抽象部分和實現部分進行分離,讓它們能夠往各自的方向擴充套件,從而達到解耦的目的。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章