CategoryResourceRepost/极客时间专栏/左耳听风/Go语言编程模式/110 | Go编程模式:委托和反转控制.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

291 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="110 | Go编程模式委托和反转控制" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c5/d8/c574db2d68fefc0b649be52e5ddf76d8.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
控制反转([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}, &quot;State:&quot;}
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(&quot;%p:Label.Paint(%q)\n&quot;, &amp;label, label.Text)
}
//因为这个接口可以通过 Label 的嵌入带到新的结构体,
//所以,可以在 Button 中重载这个接口方法
func (button Button) Paint() { // Override
fmt.Printf(&quot;Button.Paint(%s)\n&quot;, button.Text)
}
func (button Button) Click() {
fmt.Printf(&quot;Button.Click(%s)\n&quot;, button.Text)
}
func (listBox ListBox) Paint() {
fmt.Printf(&quot;ListBox.Paint(%q)\n&quot;, listBox.Texts)
}
func (listBox ListBox) Click() {
fmt.Printf(&quot;ListBox.Click(%q)\n&quot;, listBox.Texts)
}
```
说到这儿,我要重点提醒你一下,`Button.Paint()` 接口可以通过 Label 的嵌入带到新的结构体,如果 `Button.Paint()` 不实现的话,会调用 `Label.Paint()` ,所以,在 `Button` 中声明 `Paint()` 方法相当于Override。
### 嵌入结构多态
从下面的程序中,我们可以看到整个多态是怎么执行的。
```
button1 := Button{Label{Widget{10, 70}, &quot;OK&quot;}}
button2 := NewButton(50, 70, &quot;Cancel&quot;)
listBox := ListBox{Widget{10, 40},
[]string{&quot;AL&quot;, &quot;AK&quot;, &quot;AZ&quot;, &quot;AR&quot;}, 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(&quot;No functions to undo&quot;)
}
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(&quot;No functions to undo&quot;)
}
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 的代码就可以复用了。
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。