反轉控制IoC – Inversion of Control 是一種軟件設計的方法,其主要的思想是把控制邏輯與業務邏輯分享,不要在業務邏輯裏寫控制邏輯,這樣會讓控制邏輯依賴於業務邏輯,而是反過來,讓業務邏輯依賴控制邏輯。在《IoC/DIP其實是一種管理思想》中的那個開關和電燈的示例一樣,開關是控制邏輯,電器是業務邏輯,不要在電器中實現開關,而是把開關抽象成一種協議,讓電器都依賴之。這樣的編程方式可以有效的降低程序複雜度,並提升代碼重用。
本文是全系列中第4 / 9篇:Go編程模式
- Go編程模式:切片,接口,時間和性能
- Go 編程模式:錯誤處理
- Go 編程模式:Functional Options
- Go編程模式:委託和反轉控制
- Go編程模式:Map-Reduce
- Go 編程模式:Go Generation
- Go編程模式:修飾器
- Go編程模式:Pipeline
- Go 編程模式:k8s Visitor 模式
面向對象的設計模式這裏不提了,我們來看看Go語言使用Embed結構的一個示例。
嵌入和委託
結構體嵌入
在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微信公衆賬號和微信小程序
(轉載本站文章請註明作者和出處 酷 殼 – CoolShell ,請勿用於任何商業用途)