你好,我是陈皓,网名左耳朵耗子。 控制反转([Inversion of Control](https://en.wikipedia.org/wiki/Inversion_of_control)[,loC](https://en.wikipedia.org/wiki/Inversion_of_control) )是一种软件设计的方法,它的主要思想是把控制逻辑与业务逻辑分开,不要在业务逻辑里写控制逻辑,因为这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。 我之前在《[IoC/DIP其实是一种管理思想](https://coolshell.cn/articles/9949.html)》这篇文章中,举过一个开关和电灯的例子。其实,这里的开关就是控制逻辑,电器是业务逻辑。我们不要在电器中实现开关,而是要把开关抽象成一种协议,让电器都依赖它。这样的编程方式可以有效降低程序复杂度,并提升代码重用度。 面向对象的设计模式我就不提了,我们来看看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 的代码就可以复用了。 好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。