avatarHxzhouh

Summary

The web content discusses the Inversion of Control (IoC) design pattern in Go, focusing on how to implement an undo feature in a number recording functionality by inverting the dependency between control logic and business logic.

Abstract

The article titled "Go program pattern 03: Inversion of Control" by hxzhouh delves into the concept of Inversion of Control (IoC) within the context of Go programming. Initially, a simple implementation of a number set using a map is presented, followed by an enhancement to include an undo feature. The first approach involves wrapping the IntSet with an UndoableIntSet to track changes, which is noted to be a poor style due to the tight coupling of business logic with control logic. The author then introduces a more sophisticated implementation that adheres to the IoC principle by extracting the undo feature into a separate Undo type, which IntSet depends on. This inversion allows for a cleaner separation of concerns, making the undo functionality reusable across different business logics. The article concludes with a demonstration of how to use the Undo feature with IntSet and emphasizes the benefits of IoC in software design.

Opinions

  • The initial approach of extending IntSet with an undo feature is criticized for poor style due to tight coupling and lack of reusability.
  • The author advocates for the Inversion of Control principle as a means to achieve better separation of concerns and increased modularity.
  • By making the business logic (IntSet) depend on the control logic (Undo), the undo feature becomes more versatile and can be applied to other areas of the application without duplication of effort.
  • The article suggests that the IoC pattern, despite being more complex initially, leads to more maintainable and flexible code in the long term.

Go program pattern 03: Inversion of Control

Hello everyone, I’m hxzhouh

In the previous article, I briefly introduced the composite pattern in Go, which was explained in a simple manner. We understood that Go can achieve polymorphism in object-oriented programming through composition.

In this article, let’s learn about Inversion of Control (IoC). Inversion of Control is a software design method that involves separating control logic from business logic. Instead of writing control logic within the business logic, which creates a dependency of control logic on business logic, IoC reverses this relationship and makes the business logic dependent on the control logic.

Inversion of Control

Let’s consider an example where we want to implement a functionality to record the existence of numbers. We can easily implement the following code:

type IntSet struct {  
     data map[int]struct{}  
 }  
   
 func NewIntSet() IntSet {  
     return IntSet{make(map[int]struct{})}  
 }  
 func (set *IntSet) Add(x int) {  
     set.data[x] = struct{}{}  
 }  
 func (set *IntSet) Delete(x int) {  
     delete(set.data, x)  
 }  
 func (set *IntSet) Contains(x int) bool {  
     _, ok := set.data[x]  
     return ok  
 }

The above code uses a map to store numbers and provides functionalities for adding, deleting, and checking the existence of numbers. Everything seems perfect.

Now, suppose we want to add an undo feature to this functionality. How can we do that? With a little thought, we can write clear code by wrapping IntSet into UndoableIntSet. Here's the code:

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")
     }
     // invert the order of calls
     index := len(set.functions) - 1
     if function := set.functions[index]; function != nil {
         function()
     }
     set.functions = set.functions[:index]
     return nil
 }

This approach is a good choice for extending existing code with new functionalities. It allows for a balance between reusing the existing code and adding new features. However, the main issue with this approach is that the Undo operation is actually a form of control logic, not business logic. The Undo feature cannot be reused because it contains a lot of business logic related to IntSet.

Dependency Inversion

Let’s explore another implementation approach where we extract the undo feature and make IntSet depend on it:

type Undo []func()
 func (undo *Undo) Add(u func()) {  
     *undo = append(*undo, u)  
 }  
 func (undo *Undo) Undo() {  
     if len(*undo) == 0 {  
        return  
     }  
     index := len(*undo) - 1  
     (*undo)[index]()  
     *undo = (*undo)[:index]  
 }

Next, we embed Undo in IntSet:

type IntSet struct {  
     data map[int]struct{}  
     undo Undo  
 }  
   
 func NewIntSet() IntSet {  
     return IntSet{make(map[int]struct{}), make(Undo, 0)}  
 }  
 func (set *IntSet) Undo() {  
     set.undo.Undo()  
 }  
 func (set *IntSet) Add(x int) {  
     if set.Contains(x) {  
        return  
     } else {  
        set.undo.Add(func() {  
           set.Delete(x)  
        })  
        set.data[x] = struct{}{}  
     }  
 }  
 func (set *IntSet) Delete(x int) {  
     if !set.Contains(x) {  
        return  
     } else {  
        set.undo.Add(func() {  
           set.Add(x)  
        })  
        delete(set.data, x)  
     }  
 }  
 func (set *IntSet) Contains(x int) bool {  
     _, ok := set.data[x]  
     return ok  
 }

In our application, we can use it as follows:

func main() {  
     set := NewIntSet()  
     set.Add(1)  
     set.Add(2)  
     fmt.Println(set.Contains(2))  
     set.Undo()  
     fmt.Println(set.Contains(2))  
     set.Delete(1)  
     fmt.Println(set.Contains(1))  
     set.Undo()  
     fmt.Println(set.Contains(1))  
 }

Output:

/Users/hxzhouh/Library/Caches/JetBrains/GoLand2023.3/tmp/GoLand/___go_build_github_com_hxzhouh_go_example_pattern_ioc
 true
 false
 false
 true

This is Inversion of Control, where the control logic Undo no longer depends on the business logic IntSet, but rather the business logic IntSet depends on Undo. Now, the Undo feature can be easily used by other business logics.

Go
Patterns
Recommended from ReadMedium