Skip to content

Latest commit

 

History

History
 
 

di-and-interfaces

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Dependency injection and interfaces - WIP

It is assumed that you have read the structs section before reading this.

There is a lot of misunderstandings around dependency injection around the programming community. Hopefully this guide will show you how

  • You dont need a framework
  • It does not overcomplicate your design
  • It facilitates testing
  • It allows you to write great, general-purpose functions.

We want to write a function that greets someone, just like we did in the hello-world chapter but this time we are going to be testing the actual printing.

Just to recap, here is what that function could look like

func Greet(name string) {
	fmt.Printf("Hello, %s", name)
}

But how can we test this? Calling fmt.Printf prints to stdout, which is pretty hard for us to capture using the testing framework.

What we need to do is to be able to inject (which is just a fancy word for pass in) the dependency of printing. If we do that, we can then change the implementation to print to something we control so that we can test it. In "real life" you would inject in something that writes to stdout.

If you look at the source code of fmt.Printf you can see a way for us to hook in

// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

Interesting! Under the hood Printf just calls Fprintf passing in os.Stdout.

What exactly is an os.Stdout ? What does Fprintf expect to get passed to it in 1st argument?

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

An io.Writer

type Writer interface {
	Write(p []byte) (n int, err error)
}

As you write more Go code you will find this interface popping up a lot because it's a great general purpose interface for "put this data somewhere".

Now that we know this, we can write a test!

Write the test first

func TestGreet(t *testing.T) {
	buffer := bytes.Buffer{}
	Greet(&buffer,"Chris")

	got := buffer.String()
	want := "Hello, Chris"

	if got != want {
		t.Errorf("got '%s' want '%s'", got, want)
	}
}

The buffer type from the bytes package implements the Writer interface so it is perfect for us to try and record what is being written.

We call the Greet function and afterwards read the buffer into a string so we can assert on it.

Try and run the test

The test will not compile

./di_test.go:10:7: too many arguments in call to Greet
	have (*bytes.Buffer, string)
	want (string)

Write the minimal amount of code for the test to run and check the failing test output

Listen to the compiler and fix the problem.

func Greet(writer *bytes.Buffer, name string) {
	fmt.Printf("Hello, %s", name)
}

Hello, Chris di_test.go:16: got '' want 'Hello, Chris'

The test fails. Notice that the name is getting printed out, but it's going to stdout.

Write enough code to make it pass

Use the writer to send the greeting to the buffer in our test

func Greet(writer io.Writer, name string) {
	fmt.Fprintf(writer, "Hello, %s", name)
}

The test now pass

Refactor

Earlier the compiler told us to pass in a pointer to a bytes.Buffer. This is technically correct but not very useful.

To demonstrate this, try wiring up the Greet function into a Go application where we want it to print to stdout.

func main() {
	Greet(os.Stdout, "Elodie")
}

./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet

As discussed earlier fmt.Fprintf allows you to pass in an io.Writer which we know both os.Stdout and bytes.Buffer implement.

If we change our code to use the more general purpose interface we can now use it in both tests and in our application.

package main

import (
	"fmt"
	"os"
	"io"
)

func Greet(writer io.Writer, name string) {
	fmt.Fprintf(writer, "Hello, %s", name)
}

func main() {
	Greet(os.Stdout, "Elodie")
}

More on io.Writer

What other places can we write data to using io.Writer ? Just how general purpose is our Greet function?

The internet

Run the following

package main

import (
	"fmt"
	"io"
	"net/http"
)

func Greet(writer io.Writer, name string) {
	fmt.Fprintf(writer, "Hello, %s", name)
}

func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
	Greet(w, "world")
}

func main() {
	http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
}

Go to http://localhost:5000. You'll see your greeting function being used.

HTTP servers will be covered in a later chapter so dont worry too much about the details.

When you write a HTTP handler, you are given a http.ResponseWriter and the http.Request that was used to make the request. When you implement your server you write your response using the writer.

You can probably guess that http.ResponseWriter also implements io.Writer so this is why we could re-use our Greet function inside our handler.

Wrapping up

Our first round of code was not easy to test because it wrote data to somewhere we couldn't control.

Motivated by our tests we refactored the code so we could control where the data was written by injecting a dependency.

By having some familiarity of the io.Writer interface we are able to use bytes.Buffer in our test as our Writer and then we can use other Writers from the standard library to use our function in a command line app or in web server.

The more familiar you are with the standard library the more you'll see these general purpose interfaces which you can then re-use in your own code to make your software reusable in a number of contexts.