反转控制,这篇文章摘录自耗子哥博客-Go编程模式
1. 嵌入和委托
1.1 反转控制
反转控制IoC – Inversion of Control 是一种软件设计的方法,其主要的思想是把控制逻辑与业务逻辑分享,不要在业务逻辑里写控制逻辑,这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。
在《IoC/DIP其实是一种管理思想》中的那个开关和电灯的示例一样,开关是控制逻辑,电器是业务逻辑,不要在电器中实现开关,而是把开关抽象成一种协议,让电器都依赖之。
接下来我们看看 Go 语言里面委托的实现方式。
1.1 嵌入
Go 语言通过结构体嵌入实现了面向对象的一些特性,我们看下面这个例子:
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
type Painter interface {
Paint()
}
type Clicker interface {
Click()
}
type Widget struct {
X, Y int
}
// 结构体嵌入
type Label struct {
Widget // Embedding (delegation)
Text string // Aggregation
X // 1. 重名属性
}
type Button struct {
Label // Embedding (delegation)
}
func (label Label) Paint() {
fmt.Printf("%p:Label.Paint(%q)\n", &label, label.Text)
}
// 2. 方法重载
// Paint 接口可以通过 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)
}
// 3. 多态
// 可以使用接口来多态
// 也可以使用 泛型的 interface{} 来多态,但是需要有一个类型转换。
label := Label{Widget{10, 10}, "State:"}
button1 := Button{Label{Widget{10, 70}, "OK"}}
for _, painter := range []Painter{label, button1} {
painter.Paint()
}
for _, widget := range []interface{}{label, button1} {
widget.(Painter).Paint()
if clicker, ok := widget.(Clicker); ok {
clicker.Click()
}
fmt.Println() // print a empty line
}
|
可以看到:
- 我们把 Widget嵌入到了 Label 中,并且存在重名的成员 X,可以用
label.X
表明 是自己的X ,用 label.Wedget.X
表示嵌入过来的。
Button.Paint()
接口可以通过 Label 的嵌入带到新的结构体,如果 Button.Paint()
不实现的话,会调用 Label.Paint()
,所以,在 Button 中声明 Paint() 方法,相当于Override。
- 我们可以使用接口来多态,也可以使用 泛型的 interface{} 来多态,但是需要有一个类型转换。
结构体嵌入是 Go 语言中实现 23 种设计模式的基础,接下来我们来看看如何在 Go 语言中实现反转控制。
1.3 反转控制
我们来看这样一个示例,我们有一个存放整数的数据结构:
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]
}
|
IntSet 实现了 Add() 、Delete() 和 Contains() 三个操作。现在我们想实现一个 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 { // 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控制可以接受的函数签名是什么样的:
有了上面这个协议后,我们的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 // For garbage collection
}
*undo = functions[:index]
return nil
}
|
然后,我们在我们的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 的代码就可以复用了。
个人理解: 委托和反转控制的核心是将控制逻辑实现为一种协议,让业务逻辑依赖于协议,而不是让控制逻辑依赖于业务逻辑,从而抽象控制逻辑达到复用。