If you have been in this industry for any amount of time, you probably have heard about decoupling.

What is decoupling? Decoupling in programming is making parts of your code independent (or at least less dependent!) on other parts of your code.

Why is decoupling good? Let me show you.

Consider the following Go function where we traverse a tree. Note that the type definitions for Node and Stack are not shown. If you wish to see them, they will be at the end of this post.*

In this example, all we do is traverse every node in the Tree and print its value.

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

We could replace the fmt.Println(node.Value) with fancier business logic like summing the node values. Let’s do that.

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

Unfortunately, we had to initialize the sum variable. Additionally, we had to “do something” with sum at the end so I added fmt.Println(sum) at the end. Alternatively, we could return the sum which would change the function to be as follows:

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
}

Great, so we’ve returned the sum! Actually, not so great. For one, the name depthFirstTreeTraversalCoupled no longer makes sense. It would be better named sumTreeValues. We can rename it to that, but what if our codebase still needs the original function? We would need to have two functions implementing an identical algorithm to accomplish completely different things.

So let’s fix this by decoupling the depth first tree traversal algorithm from the business logic. In this case, the business logic is either printing the values or summing the values.

Instead of baking the business logic directly into the algorithm function, we will pass it in as a function parameter named 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)
    }
  }
}

If we want to simply print everything, we do that as follows:

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

Alternatively, we could create a printAllTreeValues function like this:

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

If we want to do a sum, we do it as follows:

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

  fmt.Println(sum)
}

This also can be put into a function.

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

  fmt.Println(sum)
  return sum
}

So why was all of this helpful? Because it allows us to write the depthFirstTreeTraversal only once. So that removes quite a few lines of code if your doing a bunch of tree traversals! Additionally, modifying the code is much easier because you only have to modify the business logic functions and not depthFirstTreeTraversal.

Can I use this during a coding interview? Absolutely! If you get a coding question that requires the use of a well-defined algorithm like depth first traversal, then decoupling your code like this should increase your score with the interviewer! Just make sure you tell them why you are doing it. Now, you can definitely solve your coding problem without doing this. You might run out of time before you actually are able to refactor your code to be more decoupled. You can still get some points potentially if you quickly explain to your interview how you would decouple the code.

Is this code fully decoupled? No. The algorithm in this case is still heavily constrained by the types Node and Stack. How would you decouple the algorithm from those types? This will be the subject of part 2 of this post. Here are their definitions:

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
}

If you learned anything from this post or enjoyed it, I hope you will subscribe to my Substack newsletter Sheep Code!