前言 控制反转(Inversion of Control,loC)是一种软件设计的方法,它的主要思想是把控制逻辑与业务逻辑分开,不要在业务逻辑里写控制逻辑,因为这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。
举一个开关和电灯的例子。其实,这里的开关就是控制逻辑,电器是业务逻辑。我们不要在电器中实现开关,而是要把开关抽象成一种协议,让电器都依赖它。这样的编程方式可以有效降低程序复杂度,并提升代码重用度。
嵌入和委托 结构体嵌入 在 Go 语言中,我们可以很轻松地把一个结构体嵌到另一个结构体中,如下所示:
1 2 3 4 5 6 7 type Widget struct { X, Y int }type Label struct { Widget Text string }
在这个示例中,我们把 Widget嵌入到了 Label 中,于是,我们可以这样使用:
1 2 3 4 label := Label{Widget{10 , 10 }, "State:" } label.X = 11 label.Y = 12
如果在Label 结构体里出现了重名,就需要解决重名问题,例如,如果成员 X 重名,我们就要用 label.X表明是自己的X ,用 label.Wedget.X 表明是嵌入过来的。
有了这样的嵌入,我们就可以像 UI 组件一样,在结构的设计上进行层层分解了。比如,我可以新写出两个结构体 Button 和 ListBox:
1 2 3 4 5 6 7 8 9 type Button struct { Label }type ListBox struct { Widget Texts []string Index int }
方法重写 然后,我们需要两个接口:用 Painter 把组件画出来;Clicker 用于表明点击事件。
1 2 3 4 5 6 7 type Painter interface { Paint() } type Clicker interface { Click() }
当然,对于 Lable 来说,只有 Painter ,没有Clicker;对于 Button 和 ListBox来说,Painter 和Clicker都有。
我们来看一些实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func (label Label) Paint() { fmt.Printf("%p:Label.Paint(%q)\n" , &label, label.Text) }func (button Button) Paint() { 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) }
嵌入结构多态 从下面的程序中,我们可以看到整个多态是怎么执行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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() }
我们可以使用接口来多态,也可以使用泛型的 interface{} 来多态,但是需要有一个类型转换。
反转控制 我们有一个存放整数的数据结构,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 type UndoableIntSet struct { IntSet functions []func () } func NewUndoableIntSet () UndoableIntSet { return UndoableIntSet{NewIntSet(), nil } } func (set *UndoableIntSet) Add(x int ) { 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 ) { 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 } 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 控制可以接受的函数签名是什么样的:
有了这个协议之后,我们的 Undo 控制逻辑就可以写成下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 } *undo = functions[:index] return nil }
看到这里,你不必觉得奇怪, Undo 本来就是一个类型,不必是一个结构体,是一个函数数组也没有什么问题。
然后,我们在 IntSet 里嵌入 Undo,接着在 Add() 和 Delete() 里使用刚刚的方法,就可以完成功能了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 的代码就可以复用了。