Go program pattern 03：Inversion of Control
Tags: go, pattern
Published: 2024-04-26 01:05:59


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.

> This article is first published in the medium MPP plan. If you are a medium user, please follow me in [medium](https://medium.hxzhouh.com/). Thank you very much.

In this article, let's learn about Inversion of Control (IoC). [Inversion of Control](https://en.wikipedia.org/wiki/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:

```go
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:

```go
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:

```go 
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`:

```go
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:

```go
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:
```shell
/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.