四、GO程式設計模式:委託和反轉控制

zhaocrazy發表於2022-02-06

反轉控制IoC – Inversion of Control 是一種軟體設計的方法,其主要的思想是把控制邏輯與業務邏輯分享,不要在業務邏輯裡寫控制邏輯,這樣會讓控制邏輯依賴於業務邏輯,而是反過來,讓業務邏輯依賴控制邏輯。在《IoC/DIP其實是一種管理思想》中的那個開關和電燈的示例一樣,開關是控制邏輯,電器是業務邏輯,不要在電器中實現開關,而是把開關抽象成一種協議,讓電器都依賴之。這樣的程式設計方式可以有效的降低程式複雜度,並提升程式碼重用。

嵌入和委託

結構體嵌入

在Go語言中,我們可以很方便的把一個結構體給嵌到另一個結構體中。如下所示:

type Widget struct {
    X, Y int
}
type Label struct {
    Widget        // Embedding (delegation)
    Text   string // Aggregation
}

上面的示例中,我們把 Widget嵌入到了 Label 中,於是,我們可以這樣使用:

label := Label{Widget{10, 10}, "State:"}

label.X = 11
label.Y = 12

如果在 Label 結構體裡出現了重名,就需要解決重名,例如,如果 成員 X 重名,用 label.X表明 是自己的X ,用 label.Wedget.X 表示嵌入過來的。

有了這樣的嵌入,就可以像UI元件一樣的在結構構的設計上進行層層分解。比如,我可以新出來兩個結構體 Button 和 ListBox:

type Button struct {
    Label // Embedding (delegation)
}

type ListBox struct {
    Widget          // Embedding (delegation)
    Texts  []string // Aggregation
    Index  int      // Aggregation
}

方法重寫

然後,我們需要兩個介面 Painter 用於把元件畫出來,Clicker 用於表明點選事件:

type Painter interface {
    Paint()
}

type Clicker interface {
    Click()
}

當然,

  • 對於 Lable 來說,只有 Painter ,沒有Clicker
  • 對於 Button 和 ListBox來說,Painter 和Clicker都有。

下面是一些實現:

func (label Label) Paint() {
  fmt.Printf("%p:Label.Paint(%q)\n", &label, label.Text)
}

//因為這個介面可以通過 Label 的嵌入帶到新的結構體,
//所以,可以在 Button 中可以過載這個介面方法以
func (button Button) Paint() { // Override
    fmt.Printf("Button.Paint(%s)\n", button.Text)
}
func (button Button) Click() {
    fmt.Printf("Button.Click(%s)\n", button.Text)
}


func (listBox ListBox) Paint() {
    fmt.Printf("ListBox.Paint(%q)\n", listBox.Texts)
}
func (listBox ListBox) Click() {
    fmt.Printf("ListBox.Click(%q)\n", listBox.Texts)
}

這裡,需要重點提示一下,Button.Paint() 介面可以通過 Label 的嵌入帶到新的結構體,如果 Button.Paint() 不實現的話,會呼叫 Label.Paint() ,所以,在 Button 中宣告 Paint() 方法,相當於Override。

嵌入結構多型

通過下面的程式可以看到,整個多型是怎麼執行的。

button1 := Button{Label{Widget{10, 70}, "OK"}}
button2 := NewButton(50, 70, "Cancel")
listBox := ListBox{Widget{10, 40}, 
    []string{"AL", "AK", "AZ", "AR"}, 0}

for _, painter := range []Painter{label, listBox, button1, button2} {
    painter.Paint()
}

for _, widget := range []interface{}{label, listBox, button1, button2} {
  widget.(Painter).Paint()
  if clicker, ok := widget.(Clicker); ok {
    clicker.Click()
  }
  fmt.Println() // print a empty line 
}

我們可以看到,我們可以使用介面來多型,也可以使用 泛型的 interface{} 來多型,但是需要有一個型別轉換。

反轉控制

我們再來看一個示例,我們有一個存放整數的資料結構,如下所示:

type IntSet struct {
    data map[int]bool
}
func NewIntSet() IntSet {
    return IntSet{make(map[int]bool)}
}
func (set *IntSet) Add(x int) {
    set.data[x] = true
}
func (set *IntSet) Delete(x int) {
    delete(set.data, x)
}
func (set *IntSet) Contains(x int) bool {
    return set.data[x]
}

其中實現了 Add() 、Delete() 和 Contains() 三個操作,前兩個是寫操作,後一個是讀操作。

實現Undo功能

現在我們想實現一個 Undo 的功能。我們可以把把 IntSet 再包裝一下變成 UndoableIntSet 程式碼如下所示:

type UndoableIntSet struct { // Poor style
    IntSet    // Embedding (delegation)
    functions []func()
}

func NewUndoableIntSet() UndoableIntSet {
    return UndoableIntSet{NewIntSet(), nil}
}


func (set *UndoableIntSet) Add(x int) { // Override
    if !set.Contains(x) {
        set.data[x] = true
        set.functions = append(set.functions, func() { set.Delete(x) })
    } else {
        set.functions = append(set.functions, nil)
    }
}


func (set *UndoableIntSet) Delete(x int) { // Override
    if set.Contains(x) {
        delete(set.data, x)
        set.functions = append(set.functions, func() { set.Add(x) })
    } else {
        set.functions = append(set.functions, nil)
    }
}

func (set *UndoableIntSet) Undo() error {
    if len(set.functions) == 0 {
        return errors.New("No functions to undo")
    }
    index := len(set.functions) - 1
    if function := set.functions[index]; function != nil {
        function()
        set.functions[index] = nil // For garbage collection
    }
    set.functions = set.functions[:index]
    return nil
}

在上面的程式碼中,我們可以看到

  • 我們在 UndoableIntSet 中嵌入了IntSet ,然後Override了 它的 Add()和 Delete() 方法。
  • Contains() 方法沒有Override,所以,會被帶到 UndoableInSet 中來了。
  • 在Override的 Add()中,記錄 Delete 操作
  • 在Override的 Delete() 中,記錄 Add 操作
  • 在新加入 Undo() 中進行Undo操作。

通過這樣的方式來為已有的程式碼擴充套件新的功能是一個很好的選擇,這樣,可以在重用原有程式碼功能和重新新的功能中達到一個平衡。但是,這種方式最大的問題是,Undo操作其實是一種控制邏輯,並不是業務邏輯,所以,在複用 Undo這個功能上是有問題。因為其中加入了大量跟 IntSet 相關的業務邏輯。

反轉依賴

現在我們來看另一種方法:

我們先宣告一種函式介面,表現我們的Undo控制可以接受的函式簽名是什麼樣的:

type Undo []func()

有了上面這個協議後,我們的Undo控制邏輯就可以寫成如下:

func (undo *Undo) Add(function func()) {
  *undo = append(*undo, function)
}

func (undo *Undo) Undo() error {
  functions := *undo
  if len(functions) == 0 {
    return errors.New("No functions to undo")
  }
  index := len(functions) - 1
  if function := functions[index]; function != nil {
    function()
    functions[index] = nil // For garbage collection
  }
  *undo = functions[:index]
  return nil
}

這裡你不必覺得奇怪, Undo 本來就是一個型別,不必是一個結構體,是一個函式陣列也沒什麼問題。

然後,我們在我們的IntSet裡嵌入 Undo,然後,再在 Add() 和 Delete() 裡使用上面的方法,就可以完成功能。

type IntSet struct {
    data map[int]bool
    undo Undo
}

func NewIntSet() IntSet {
    return IntSet{data: make(map[int]bool)}
}

func (set *IntSet) Undo() error {
    return set.undo.Undo()
}

func (set *IntSet) Contains(x int) bool {
    return set.data[x]
}

func (set *IntSet) Add(x int) {
    if !set.Contains(x) {
        set.data[x] = true
        set.undo.Add(func() { set.Delete(x) })
    } else {
        set.undo.Add(nil)
    }
}

func (set *IntSet) Delete(x int) {
    if set.Contains(x) {
        delete(set.data, x)
        set.undo.Add(func() { set.Add(x) })
    } else {
        set.undo.Add(nil)
    }
}

這個就是控制反轉,不再由 控制邏輯 Undo 來依賴業務邏輯 IntSet,而是由業務邏輯 IntSet 來依賴 Undo 。其依賴的是其實是一個協議,這個協議是一個沒有引數的函式陣列。我們也可以看到,我們 Undo 的程式碼就可以複用了。

本文非本人所作,轉載左耳朵耗子部落格和出處 酷 殼 – CoolShell

本作品採用《CC 協議》,轉載必須註明作者和本文連結
滴水穿石,石破天驚----馬乂

相關文章