极客时间已完结课程限时免费阅读

110 | Go编程模式:委托和反转控制

110 | Go编程模式:委托和反转控制-极客时间

110 | Go编程模式:委托和反转控制

讲述:杨超

时长05:23大小4.92M

你好,我是陈皓,网名左耳朵耗子。
控制反转(Inversion of Control,loC )是一种软件设计的方法,它的主要思想是把控制逻辑与业务逻辑分开,不要在业务逻辑里写控制逻辑,因为这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。
我之前在《IoC/DIP 其实是一种管理思想》这篇文章中,举过一个开关和电灯的例子。其实,这里的开关就是控制逻辑,电器是业务逻辑。我们不要在电器中实现开关,而是要把开关抽象成一种协议,让电器都依赖它。这样的编程方式可以有效降低程序复杂度,并提升代码重用度。
面向对象的设计模式我就不提了,我们来看看 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 组件一样,在结构的设计上进行层层分解了。比如,我可以新写出两个结构体 ButtonListBox
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;对于 ButtonListBox来说,PainterClicker都有。
我们来看一些实现:
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 的代码就可以复用了。
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。
分享给需要的人,Ta购买本课程,你将得29
生成海报并分享

赞 29

提建议

上一篇
109 | Go 编程模式:Functional Options
下一篇
111 | Go 编程模式:Map-Reduce
unpreview
 写留言

精选留言(8)

  • debugtalk
    2021-06-12
    手动点赞
    4
  • limix
    2022-09-07 来自安徽
    控制逻辑的特征是可复用性比较高,多场景可用,而业务逻辑的特征是专用性,控制逻辑,应该复用控制逻辑,而不是复用也许逻辑
    2
  • cvvz
    2022-08-27 来自日本
    反转控制: 第一种改写方法——继承,好处是原有代码可以复用,控制逻辑和业务逻辑解耦,坏处是通用的控制逻辑无法复用 第二种改写方法——反转控制,好处是复用通用的控制逻辑,坏处是要修改原有代码的逻辑,把控制逻辑嵌入到了业务逻辑中
  • Geek_ce6971
    2022-01-19
    实现undo功能,有个地方是写反了吗 在 Override 的 Add()中,记录 Delete 操作;在 Override 的 Delete() 中,记录 Add 操作;
    共 1 条评论
  • 方勇(gopher)
    2021-12-20
    日常中有些装饰器其实可以用这种方式替换
  • 衡子
    2021-11-30
    厉害了👍
  • 一光年
    2021-08-11
    控制逻辑依赖业务逻辑,不如让业务容器依赖控制逻辑
  • Geek_46da16
    2021-07-26
    会玩,