跳转至

你好,我是陈皓,网名左耳朵耗子。

我们再来看Go语言这个模式,Go语言的这个模式挺好玩儿的。声明一个struct,跟C很一样,然后直接把这个struct类型放到另一个struct里。

委托的简单示例

我们来看几个示例:

type Widget struct {
    X, Y int
}

type Label struct {
    Widget        // Embedding (delegation)
    Text   string // Aggregation
    X int         // Override 
}

func (label Label) Paint() {
    // [0xc4200141e0] - Label.Paint("State")
    fmt.Printf("[%p] - Label.Paint(%q)\n", 
        &label, label.Text)
}

由上面可知:

  • 我们声明了一个 Widget,其有 XY
  • 然后用它来声明一个 Label,直接把 Widget 委托进去;
  • 然后再给 Label 声明并实现了一个 Paint() 方法。

于是,我们就可以这样编程了:

label := Label{Widget{10, 10}, "State", 100}

// X=100, Y=10, Text=State, Widget.X=10
fmt.Printf("X=%d, Y=%d, Text=%s Widget.X=%d\n", 
    label.X, label.Y, label.Text, 
    label.Widget.X)
fmt.Println()
// {Widget:{X:10 Y:10} Text:State X:100} 
// {{10 10} State 100}
fmt.Printf("%+v\n%v\n", label, label)

label.Paint()

我们可以看到,如果有成员变量重名,则需要手动地解决冲突。

我们继续扩展代码。

先来一个 Button

type Button struct {
    Label // Embedding (delegation)
}

func NewButton(x, y int, text string) Button {
    return Button{Label{Widget{x, y}, text, x}}
}
func (button Button) Paint() { // Override
    fmt.Printf("[%p] - Button.Paint(%q)\n", 
        &button, button.Text)
}
func (button Button) Click() {
    fmt.Printf("[%p] - Button.Click()\n", &button)
}

再来一个 ListBox

type ListBox struct {
    Widget          // Embedding (delegation)
    Texts  []string // Aggregation
    Index  int      // Aggregation
}
func (listBox ListBox) Paint() {
    fmt.Printf("[%p] - ListBox.Paint(%q)\n", 
        &listBox, listBox.Texts)
}
func (listBox ListBox) Click() {
    fmt.Printf("[%p] - ListBox.Click()\n", &listBox)
}

然后,声明两个接口用于多态:

type Painter interface {
    Paint()
}

type Clicker interface {
    Click()
}

于是我们就可以这样泛型地使用(注意其中的两个for循环):

button1 := Button{Label{Widget{10, 70}, "OK", 10}}
button2 := NewButton(50, 70, "Cancel")
listBox := ListBox{Widget{10, 40}, 
    []string{"AL", "AK", "AZ", "AR"}, 0}

fmt.Println()
//[0xc4200142d0] - Label.Paint("State")
//[0xc420014300] - ListBox.Paint(["AL" "AK" "AZ" "AR"])
//[0xc420014330] - Button.Paint("OK")
//[0xc420014360] - Button.Paint("Cancel")
for _, painter := range []Painter{label, listBox, button1, button2} {
    painter.Paint()
}

fmt.Println()
//[0xc420014450] - ListBox.Click()
//[0xc420014480] - Button.Click()
//[0xc4200144b0] - Button.Click()
for _, widget := range []interface{}{label, listBox, button1, button2} {
    if clicker, ok := widget.(Clicker); ok {
        clicker.Click()
    }
}

一个 Undo 的委托重构

上面这个是 Go 语言中的委托和接口多态的编程方式,其实是面向对象和原型编程综合的玩法。这个玩法可不可以玩得更有意思呢?这是可以的。

首先,我们先声明一个数据容器,其中有 Add()Delete()Contains() 方法。还有一个转字符串的方法。

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]
}

func (set *IntSet) String() string { // Satisfies fmt.Stringer interface
    if len(set.data) == 0 {
        return "{}"
    }
    ints := make([]int, 0, len(set.data))
    for i := range set.data {
        ints = append(ints, i)
    }
    sort.Ints(ints)
    parts := make([]string, 0, len(ints))
    for _, i := range ints {
        parts = append(parts, fmt.Sprint(i))
    }
    return "{" + strings.Join(parts, ",") + "}"
}

我们如下使用这个数据容器:

ints := NewIntSet()
for _, i := range []int{1, 3, 5, 7} {
    ints.Add(i)
    fmt.Println(ints)
}
for _, i := range []int{1, 2, 3, 4, 5, 6, 7} {
    fmt.Print(i, ints.Contains(i), " ")
    ints.Delete(i)
    fmt.Println(ints)
}

这个数据容器平淡无奇,我们想给它加一个Undo的功能。我们可以这样来做:

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 // Free closure for garbage collection
    }
    set.functions = set.functions[:index]
    return nil
}

于是就可以这样使用了:

ints := NewUndoableIntSet()
for _, i := range []int{1, 3, 5, 7} {
    ints.Add(i)
    fmt.Println(ints)
}
for _, i := range []int{1, 2, 3, 4, 5, 6, 7} {
    fmt.Println(i, ints.Contains(i), " ")
    ints.Delete(i)
    fmt.Println(ints)
}
fmt.Println()
for {
    if err := ints.Undo(); err != nil {
        break
    }
    fmt.Println(ints)
}

但是,需要注意的是,我们用了一个新的 UndoableIntSet 几乎重写了所有的 IntSet 和 “写” 相关的方法,这样就可以把操作记录下来,然后 Undo 了。

但是,可能别的类也需要Undo的功能,我是不是要重写所有的需要这个功能的类啊?这样的代码类似,就是因为数据容器不一样,我就要去重写它们,这太二了。

我们能不能利用前面学到的泛型编程、函数式编程、IoC等范式来把这个事干得好一些呢?当然是可以的。

如下所示:

  • 我们先声明一个 Undo[] 的函数数组(其实是一个栈);
  • 并实现一个通用 Add()。其需要一个函数指针,并把这个函数指针存放到 Undo[] 函数数组中。
  • Undo() 的函数中,我们会遍历Undo[]函数数组,并执行之,执行完后就弹栈。
type Undo []func()

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 // Free closure for garbage collection
    }
    *undo = functions[:index]
    return nil
}

那么我们的 IntSet 就可以改写成如下的形式:

type IntSet struct {
    data map[int]bool
    undo Undo
}

func NewIntSet() IntSet {
    return IntSet{data: make(map[int]bool)}
}

然后在其中的 AddDelete中实现 Undo 操作。

  • Add 操作时加入 Delete 操作的 Undo。
  • Delete 操作时加入 Add 操作的 Undo。
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)
    }
}

func (set *IntSet) Undo() error {
    return set.undo.Undo()
}

func (set *IntSet) Contains(x int) bool {
    return set.data[x]
}

我们再次看到,Go语言的Undo接口把Undo的流程给抽象出来,而要怎么Undo的事交给了业务代码来维护(通过注册一个Undo的方法)。这样在Undo的时候,就可以回调这个方法来做与业务相关的Undo操作了。

小结

这是不是和最一开始的C++的泛型编程很像?也和map、reduce、filter这样的只关心控制流程,不关心业务逻辑的做法很像?而且,一开始用一个UndoableIntSet来包装IntSet类,到反过来在IntSet里依赖Undo类,这就是控制反转IoC。

以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。

精选留言(15)
  • milley 👍(39) 💬(0)

    这样的代码和思维只能说赏心悦目!

    2018-02-06

  • 小文同学 👍(11) 💬(0)

    1、文章说了什么? 文章分了两部分,一部分先简单说了 Golang 的委托用法。简单来说,就是讲一个 structA 嵌套到另外一个 structB 中,structB 会自动继承 structA 的字段。其后,通过一个更加复杂的例子说明委托的用法。(作为一个 Java 程序员,Golang 为 struct 增加方法,和定义接口的方法让人印象深刻) 另一部分,作者举了一个更加复杂的例子说明 Go 中委托和接口多态是如何实现一个数据容器的 Undo 实现的。为了说明这部分,作者通过以下步骤一说说进阶说明: 1、最简单的一个 IntSet,并定义了 Add ,Delete 方法; 2、通过一个委托的方法,将 IntSet 委托给一个新的 struct,新 struct 再重写一次 Add,Delete 方法以记录步骤(保存Undo函数对象),完成 Undo 功能; 3、最后作者希望可以进一步改写,编写一个 Undo 栈,委托给 IntSet ,并在 IntSet 编写 Add,Delete 的方法中就完成 Undo 函数对象的保存。这也是一个实现方法。

    2020-11-04

  • Jie 👍(4) 💬(1)

    求教,最后那段代码执行undo的时候会继续添加undo函数,那样不就回不到最初的状态了?后续一直在撤销undo—撤销撤销undo……

    2020-11-11

  • 亢(知行合一的路上) 👍(4) 💬(0)

    依赖的东西要可靠、稳定,也就是接口。 业务与控制分离,控制就可以复用。 把变化频率不同的事物分开。

    2019-02-26

  • 拉欧 👍(3) 💬(0)

    go里面这个undo功能的实现类似scala里面的trait,也是把一些功能模块(以及实现)单独封装起来,然后以委托或者继承的形式组装到类里面,这种灵活组装的方式确实比java的interface要更方便使用,不同语言之间是有共同点的

    2019-05-29

  • 寻找的人cs 👍(3) 💬(0)

    web端功能多一点就好了,比如显示文章列表的时候感觉不如app端那么清爽

    2019-02-06

  • Jacob.C 👍(2) 💬(0)

    再在undo里加个反撤销的功能,就更秀了

    2021-03-05

  • 你为啥那么牛 👍(2) 💬(0)

    写了半年的go语言了,终于体会到go语言的美感了。那种只要会嘎嘎叫的,我就认为是一只🦆的境界。😃

    2020-09-15

  • Z3 👍(2) 💬(0)

    sort.Ints(ints) parts := make([]string, 0, len(ints)) for _, i := range ints { 这块要sort吗? 能否直接for (i=0;i<len)print ints[i]

    2018-02-06

  • 小虾米 👍(1) 💬(0)

    这样写的undo在第一次插入过后,可以无限撤销了吧

    2018-02-06

  • 小破 👍(1) 💬(0)

    几个月前听到代码时间做节目,陈老师讲的内容让我感觉很实在,今天终于跟过来了😃

    2018-02-06

  • qiushye 👍(0) 💬(0)

    没有理解的可以直接拷贝代码去执行,不懂的地方打日志输出指针之类的来帮助理解,可以思考下after undo的相同输出如何来的。 package main import ( "errors" "fmt" "sort" "strings" ) func main() { ints := NewIntSet() for _, i := range []int{1, 3, 5} { ints.Add(i) fmt.Println("after add:", ints.String()) } for _, i := range []int{1, 2, 3, 4, 5} { fmt.Println("want delete:", i, ints.Contains(i), " ") ints.Delete(i) fmt.Println("after delete:", ints.String()) } fmt.Println("------- undo result ---------") for { if err := ints.Undo(); err != nil { fmt.Println(err) break } fmt.Println("after undo:", ints.String()) } } type IntSet struct { data map[int]bool undo Undo } func NewIntSet() IntSet { return IntSet{data: make(map[int]bool)} } 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) } } func (set *IntSet) Undo() error { return set.undo.Undo() } func (set *IntSet) Contains(x int) bool { return set.data[x] } func (set *IntSet) String() string { if len(set.data) == 0 { return "{}" } ints := make([]int, 0, len(set.data)) for i := range set.data { ints = append(ints, i) } sort.Ints(ints) parts := make([]string, 0, len(ints)) for _, i := range ints { parts = append(parts, fmt.Sprint(i)) } return "{" + strings.Join(parts, ",") + "}" } type Undo []func() 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 // Free closure for garbage collection } *undo = functions[:index] return nil }

    2023-09-15

  • seedjyh 👍(0) 💬(0)

    委托模式其实就是利用了go的组合功能实现了类似C++的继承功能。就undo数组而言,继承了基类的栈、注册undo的方法和执行undo的方法。

    2021-10-20

  • seedjyh 👍(0) 💬(0)

    最后的undo数组很有意思。 在C++里,一般是基类Undoable有一个public的实体函数Undo和一个private的纯虚函数undo,前者调用后最后;各个需要undo的子类实现这个纯虚函数。但这样就引入了强耦合(继承)。 在go里是注册一个闭包,让Undo数组回调。

    2021-10-20

  • Geek_bc461b 👍(0) 💬(0)

    单从undo功能来说用装饰器模式是不是更好

    2020-12-16