浅谈用Go构建不可变的数据结构的方法

2020-01-28 14:07:25于丽


func NewPerson() Person {
  return Person{}.
    WithName("No Name")
}

接口 (Interface)

到现在为止,我们使用的还是公有的结构体。任由这些结构体方法摆布之下,加上创建 mock 可能会引发非预期的副作用,测试起来会很痛苦。

我们可以创建一个同名的接口,并把相应的结构体重命名为 person 使之私有化:


type Person interface {
  WithName(name string) Person
  Name() string
  WithFavoriteColors(favoriteColors []string) Person
  NumFavoriteColors() int
  FavoriteColorAt(i int) string
  WithFavoriteColorAt(i int, favoriteColor string) Person
}

type person struct {
  name      string
  favoriteColors []string
}

我们现在就可以只重写想要替换的逻辑来创建测试 mock:


type personMock struct {
  Person
  receivedNewColor string
}

func (m personMock) WithFavoriteColorAt(i int, favoriteColor string) Person {
  m.receivedNewColor = favoriteColor
  return m
}

测试代码样例如下:


mock := personMock{}
result := updateFavoriteColors(mock)

result.(personMock).receivedNewColor // "red"

记录变化

如我早前所言,完整的状态转换非常有益于调试,而且我们可以 wither 来挂入钩子的方式捕捉到所有或部分变换过程:


func (p person) nextState() Person {
  fmt.Printf("nextState: %#+vn", p)
  return p
}

func (p person) WithName(name string) Person {
  p.name = name
  return p.nextState() // <- Use "nextState" whenever you return.
}

对于更加复杂的逻辑或个人偏好,你也可以采用 defer 的方式:


func (p person) WithFavoriteColors(favoriteColors []string) Person {
  defer func() {
    p.nextState()
  }()

  p.favoriteColors = favoriteColors
  return p
}

这样变换就可看到了:


nextState: main.person{name:"No Name", favoriteColors:[]string(nil)}
nextState: main.person{name:"Elliot", favoriteColors:[]string(nil)}
nextState: main.person{name:"Elliot", favoriteColors:[]string{"black", "blue"}}

你可以添加更多诸如此类的信息。例如,时间戳、栈追踪记录和其他自定义的上下文信息来使得调试更加容易。

历史及回滚

除了打印变化之外,我们还可以收集这些状态作为历史:


type Person interface {
  // ...
  AtVersion(version int) Person
}

type person struct {
  // ...
  history    []person
}

func (p *person) nextState() Person {
  p.history = append(p.history, *p)
  return *p
}

func (p person) AtVersion(version int) Person {
  return p.history[version]
}

func main() {
  me := NewPerson().
    WithName("Elliot").
    WithFavoriteColors([]string{"black", "blue"})

  // We discard the result, but it will be put into the history.
  updateFavoriteColors(me)

  fmt.Printf("%sn", me.AtVersion(0).Name())
  fmt.Printf("%sn", me.AtVersion(1).Name())
}

// No Name
// Elliot