drio

Interfaces and Dependency Injection with Golang

Chapter 7 on Jon's golang book is about types, methods, and interfaces. At the end of the chapter, Jon talks about how go's interfaces make it easy to apply dependency injection in your code to help you cope with change.

Anyone who has been programming for any length of time quickly learns that applications need to change over time. One of the techniques that has been developed to ease decoupling is called dependency injection. The truth is that it is easy to implement dependency injection in Go without any additional libraries.

At the end of the chapter Jon has a very useful example to help you understand this concept practically. We build a very simple web application in go. The app has a single endpoint that receives the userid and returns a message. Let's go over the code to make sure we understand it.

Our application is going to have to log things. To that purpose, we write a log utility function:

func LogOutput(message string) {
	fmt.Println(message)
}

We also need some way to store our data. A map between ids and names. Let's also add a factory function that returns a instance of our SimpleStore.

// Let's define our data store
type SimpleDataStore struct {
	userData map[string]string
}

func (sds SimpleDataStore) UserNameForID(userID string) (string, bool) {
	name, ok := sds.userData[userID]
	return name, ok
}

// Store factory function
func NewSimpleDataStore() SimpleDataStore {
	return SimpleDataStore{
		userData: map[string]string{
			"1": "Fred",
			"2": "Mary",
			"3": "Pat",
		}}
}

Next we want to write our business logic. That logic will print a message given a user id. The message will be either a hello or a goodbye. When writing that logic we realize that we need access to the DataStore and the logging utility function. We could pass that to our business logic but the problem is we would be coupling our business logic to the implementation of the DataStore and the logging function. What happens if we want to use other type of Datastore or other type of log? To solve that what we can do is to create interfaces that define the functionality that our business logic needs. Then we make sure that our types satisfy those interfaces.

// Define what our business logic is going to need
// what dependencies needs
// something that has a method that let's me get a userID given an ID
type DataStore interface {
	UserNameForID(userID string) (string, bool)
}

// Also, we need something that we can use to log
type Logger interface {
	Log(message string)
}

// This adapts our logOutput function to a LoggerAdapter
// Similar to what http.HandlerFunc does
// Our logOutput function matches this type
type LoggerAdapter func(message string)

func (lg LoggerAdapter) Log(message string) {
	lg(message)
}

Notice the adapter function LoggerAdapter(). This is a very common pattern in go. You have probably seen it in the http package. The utility log function we wrote does not satisfy the Logger interface but we can use the LoggerAdapter type to solve that. The LoggerAdapter is a user type that has a definition that matches the signature of our logOutput function. Then, we define a method on that type that type to satisfy the Logger interface. That method simple calls our utility function. Now we can use that in our business logic:

// Now we define a new type that uses the previous interfaces
// Basically we are keeping all we need for our logic to work in a single type
type SimpleLogic struct {
	l  Logger
	ds DataStore
}

func (sl SimpleLogic) SayHello(userID string) (string, error) {
	sl.l.Log("in SayHello for " + userID)
	name, ok := sl.ds.UserNameForID(userID)
	if !ok {
		return "", errors.New("unknown user")
	}
	return "Hello, " + name, nil
}
func (sl SimpleLogic) SayGoodbye(userID string) (string, error) {
	sl.l.Log("in SayGoodbye for " + userID)
	name, ok := sl.ds.UserNameForID(userID)
	if !ok {
		return "", errors.New("unknown user")
	}
	return "Goodbye, " + name, nil
}

Let's also create a factory function for our SimpleLogic:

func NewSimpleLogic(l Logger, ds DataStore) SimpleLogic {
	return SimpleLogic{
		l:  l,
		ds: ds}
}

Now we are moving to our API. We only have an endpoint /hello that says hello to the person based on the user id we pass. We will use a "controller" that has the logic to read requests and pass them to our Business Logic. Again, we use interfaces to specify what our controller does:

type Logic interface {
	SayHello(userID string) (string, error)
}

Now we use the same pattern to define our Controller: first, we define the concrete type with our dependencies, in this case, the logger and the logic. Those are interfaces not concrete types. Then we define the method that satisfies the Logic interface. We also create a factory function to instantiate a concrete controller.

type Logic interface {
	SayHello(userID string) (string, error)
}

type Controller struct {
	l     Logger
	logic Logic
}

func (c Controller) SayHello(w http.ResponseWriter, r *http.Request) {
	c.l.Log("In SayHello")
	userID := r.URL.Query().Get("user_id")
	message, err := c.logic.SayHello(userID)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(err.Error()))
		return
	}
	w.Write([]byte(message))
}

func NewController(l Logger, logic Logic) Controller {
	return Controller{
		l:     l,
		logic: logic,
	}
}

And we made it, now we can put everything together in our main function. Notice that this is the only point in the code that knows about concrete types.

func main() {
	l := LoggerAdapter(LogOutput)
	ds := NewSimpleDataStore()
	logic := NewSimpleLogic(l, ds)
	c := NewController(l, logic)
	http.HandleFunc("/hello", c.SayHello)
	http.ListenAndServe(":8080", nil)
}

I'd like to end this post with another quote from Jon's book:

If you had to label Go’s style, the best word to use is practical. It borrows concepts from many places with the overriding goal of creating a language that is simple, readable, and maintainable by large teams for many years.