Dependency Inversion in Go

Two years ago, some engineers in my company had a study on the book "Hands-on Dependency Injection in Go." Overall, the book was about the famous SOLID principles:

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

At that time, I couldn't get the last one: the dependency inversion principle. Robert C. Martin said that "High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstraction should not depend upon details. Details should depend on abstractions." Using abstractions (i.e., interfaces in Go) was intuitive to me. Many languages have a similar concept to the Go interface (such as an abstract class in Java). But in the book, the author said that the interface should be defined in a consuming side package, not a consumed package. It was weird to me; I thought that it was natural to specify interfaces in a package implementing features (as in many OOP languages). But recently, I have had many cases the dependency inversion principle was useful. So, I'd like to explain it.

Let's assume we're going to develop a user management system. The system uses a storage to store the user information.

package entity

// User represents a user entity.
type User struct {
	ID string
	Name string
}


The storage technology can be anything; it can be either a SQL database, an in-memory cache, or a file system. So, use an abstraction (interface) instead of a specific implementation sounds valid. Assuming there is a storage implementation and its package is strg. The package provides a Storage interface, and it consists of three methods: Add, Get, and Delete. NewStorage function returns the Storage interface.

package strg

import (
	"fmt"
)

// Storage defines a storage interface.
type Storage interface {
	Add(key string, item interface{}) error
	Get(key string) (interface{}, error)
	Delete(key string) error
}

...

// NewStorage provides a Storage interface.
func NewStorage() Storage {
...
}


Then the user.Manager can use it in its implementation.

package user

import (
	"fmt"

	"github.com/dikoko/blog/depinv/v1/entity"
	"github.com/dikoko/blog/depinv/v1/strg"
)

// Manager defines the user data manager.
type Manager struct {
	storage strg.Storage
}

// NewManager creates a new Manager.
func NewManager(storage strg.Storage) *Manager {
	return &Manager{
		storage: storage,
	}
}

// AddUser adds a user data.
func (m *Manager) AddUser(user *entity.User) error {
	if user == nil || user.ID == "" {
		return fmt.Errorf("invalid user")
	}
	return m.storage.Add(user.ID, user)
}

// GetUser retrieves a user data.
func (m *Manager) GetUser(id string) (*entity.User, error) {
	item, err :=  m.storage.Get(id)
	if err != nil {
		return nil, err
	}
	user, ok := item.(*entity.User)
	if !ok {
		return nil, fmt.Errorf("invalid user data")
	}
	return user, nil
}

// DeleteUser deletes a user data.
func (m *Manager) DeleteUser(id string) error {
	return m.storage.Delete(id)
}

We can describe the relationship as the following figure.

It looks good, but it objects to the dependency inversion principle. Think about we have decided to change the Storage interface signature. Imagine that we add a note to each Add operation (yes, we can do this without breaking the interface, but just assume that we had to change the interface signature). The upstream (to-be used) interface has been changed, and we have to change the downstream (using, i.e., user.Manager) code to conform to it. We have to update all Storage interface's Add, Get, and Delete usages in the user.Manager implementation. There are only three in the sample code, but as the Manager evolves, it should be getting complicated and error-prone.

And here comes the dependency inversion. According to the definition, we have to define the storage interface ( userStorage ) in user package. The userStorage is a private interface as it is only used in the package. So, the implementation will be as follow:

type userStorage interface {
	add(key string, item interface{}) error
	get(key string) (interface{}, error)
	delete(key string) error
}

type userStorageHandler struct {
	storage strg.Storage
}

func (s *userStorageHandler) add(key string, item interface{}) error {
	return s.storage.Add(key, item, "")
}

func (s *userStorageHandler) get(key string) (interface{}, error) {
	item, _, err := s.storage.Get(key)
	return item, err
}

func (s *userStorageHandler) delete(key string) error {
	_, err := s.storage.Delete(key)
	return err
}

We implemented the userStorageHandler as a concrete implementation of the private userStorage interface. The relationship is now like the following figure.

Now imagine that the Storage interface has changed (with the note). We have to adapt to it, but this time, we only have to update the three private methods of the userStorageHandler. We don't need to update every  user.Manager implementation as it was decoupled with the strg.Storage.

I believe the core concept is making the dependency only in its package. We decoupled the dependency by localizing the dependency of the user.Manager on its private userStorage, and only a specific private implementation (userStorageHandler) has a dependency on the external strg.Storage interface. If we need to use another storage technology, we can easily make another userStorage implementation with the technology (and inject to the Manager.)

One more thing to mention is that the NewManager factory function. It takes the strg.Storage interface as an argument, which dependent on the strg.Storage interface. We might use the userStorage interface argument instead, but it exposes a private interface as an argument of a public method. It is legal; in terms that the Go compiler does not complain of it. But I believe that it is not appropriate as it is a kind of specification; a private interface argument in a public method specification looks weird. I prefer using a concrete struct to an interface. So to speak, I'd instead make strg.Storage as a concrete struct (not an interface) in the first place, and strg.NewStorage returns the instance; according to the axiom - "Accept interfaces, return structs."