GoLang|将深度第一的树形遍历与业务逻辑解耦

128 阅读4分钟

如果你在这个行业呆了很久,你可能听说过脱钩的事。

什么是解耦? 编程中的解耦是使你的部分代码独立于(或至少是较少的依赖!)你的代码的其他部分。

为什么解耦是好的?让我告诉你。

考虑一下下面这个Go函数,我们在其中遍历一棵树。type 注意,NodeStack 的定义没有显示。如果你想看的话,它们会在本篇文章的结尾处。

在这个例子中,我们所做的就是遍历树中的每一个节点并打印其值。

func depthFirstTreeTraversalCoupled(root *Node) {
  stack := &Stack{}
  stack.Push(root)

  for !stack.IsEmpty(){
    node := stack.Pop()

    // do fancy business logic with the node
    // or just print the value
    fmt.Println(node.Value)
    
    for _, child := range node.Children {
      stack.Push(child)
    }
  }
}

我们可以用更复杂的业务逻辑来代替fmt.Println(node.Value) ,比如对节点的值进行求和。让我们这么做吧。

func depthFirstTreeTraversalCoupled(root *Node) {
  stack := &Stack{}
  stack.Push(root)

  sum := 0

  for !stack.IsEmpty(){
    node := stack.Pop()

    // do fancy business logic with the node
    // or just print the value
    sum = sum + node.Value
    
    for _, child := range node.Children {
      stack.Push(child)
    }
  }

  fmt.Println(sum)
}

不幸的是,我们必须初始化sum 这个变量。此外,我们必须在最后对sum "做一些事情",所以我在最后添加了fmt.Println(sum) 。另外,我们也可以返回sum ,这样就可以把函数改为如下。

func depthFirstTreeTraversalCoupled(root *Node) int {
  stack := &Stack{}
  stack.Push(root)

  sum := 0 // have to initialize this before the loop
  // for scoping reasons!

  for !stack.IsEmpty(){
    node := stack.Pop()

    // do fancy business logic with the node
    // or just print the value
    sum = sum + node.Value
    
    for _, child := range node.Children {
      stack.Push(child)
    }
  }

  fmt.Println(sum)
  return sum
}

很好,所以我们已经返回了sum!事实上,并不那么好。首先,depthFirstTreeTraversalCoupled 这个名字已经没有意义了。它最好被命名为sumTreeValues 。我们可以把它重命名为这个名字,但如果我们的代码库仍然需要原来的函数呢?我们将需要有两个实现相同算法的函数来完成完全不同的事情。

所以让我们通过将深度第一的树形遍历算法与业务逻辑解耦来解决这个问题。在这种情况下,业务逻辑要么是打印数值,要么是对数值进行求和

我们不把业务逻辑直接放在算法函数中,而是把它作为一个名为businessLogic 的函数参数传入。

func depthFirstTreeTraversal(root *Node,  businessLogic func(*Node)) {
  stack := &Stack{}
  stack.Push(root)

  for !stack.IsEmpty(){
    node := stack.Pop()
    businessLogic(node)
    for _, child := range node.Children {
      stack.Push(child)
    }
  }
}

如果我们想简单地打印所有的东西,我们的做法如下。

func main() {
  depthFirstTreeTraversal(tree, func(n *Node) {
    fmt.Println(n.Value)
  })
}

另外,我们也可以像这样创建一个printAllTreeValues 函数。

func printAllTreeValues(tree *Node) {
  depthFirstTreeTraversal(tree, func(n *Node) {
    fmt.Println(n.Value)
  })
}

如果我们想做一个总和,我们按如下方式进行。

func main() {
  sum := 0
  depthFirstTreeTraversal(tree, func(n *Node) {
    sum = sum + n.Value
  })

  fmt.Println(sum)
}

这也可以放在一个函数中。

func sumAllTreeValues(tree *Node) int {
  sum := 0
  depthFirstTreeTraversal(tree, func(n *Node) {
    sum = sum + n.Value
  })

  fmt.Println(sum)
  return sum
}

那么,为什么这些都是有帮助的呢?因为它允许我们只写一次depthFirstTreeTraversal 。因此,如果你在做一堆树的遍历时,就可以去掉相当多的几行代码!此外,修改代码要容易得多,因为你只需要修改业务逻辑函数而不是depthFirstTreeTraversal

我可以在编码面试时使用这个吗?当然可以!如果你遇到的编码问题需要使用一个定义明确的算法,比如深度第一遍的遍历,那么像这样解耦你的代码应该会增加你在面试官那里的分数!只要确保你告诉他们你为什么要这样做。现在,你绝对可以不这样做就解决你的编码问题。在你真正能够重构你的代码,使其更加解耦之前,你可能已经没有时间了。如果你迅速向你的面试者解释你将如何解耦代码,你仍然可以得到一些潜在的分数。

这段代码是完全解耦的吗? 不是。在这种情况下,算法仍然严重受制于NodeStack 。你将如何把算法与这些类型解耦?这将是本帖第二部分的主题。下面是它们的定义。

type Node struct {
  Value int
  Children []*Node
}

type Stack struct {
  items []*Node
}

func (stack *Stack) Push(node *Node) {
  if (stack.items == nil) {
    stack.items = []*Node{}
  }

  stack.items = append(stack.items, node)
}

func (stack *Stack) Pop() *Node {
  if len(stack.items) == 0 {
    return nil
  }
  top := stack.items[len(stack.items)-1]
  stack.items = stack.items[0:len(stack.items)-1]
  return top
}

func (stack *Stack) IsEmpty() bool {
  return len(stack.items) == 0
}