Dependency injection (DI) is a key principle of clean code. It is also often misrepresented and misunderstood, and thus often disregarded. So, let us begin with the definition of dependency injection:

Dependency injection is the coding technique where objects (via constructors or setters) and methods/functions receive other objects they depend on as arguments. [1]

This is it. I want to emphasize here that the principle of dependency injection is completely independent of frameworks that might help (or hinder) this. This distinction is important, because in most discussions the principle of dependency injection and the need for/use of frameworks are conflated into the same thing. They are not the same, and in this article I will show that they are by no means necessary to unlock the power of dependency injection.

Let’s begin with an example. The problem to solve is straightforward: read data from a file and render it for output, for example simple ASCII text for console output and testing, or HTML for presentation in a browser. With that specification, you will often write or see code like this:

func Render(fileName, filter string, mode int) ([]byte, error) {
	d, err := Process(fileName, filter)
	if err != nil {
		return nil, err
	}
	switch mode {
	case HTML:
		// render HTML output
		return nil, nil
	case CONSOLE:
		// render console output
		return nil, nil
	default:
		return nil, fmt.Errorf("invalid mode: %d", mode)
	}
}

func Process(fileName, filter string) (data, error) {
	d, err := Read(fileName)
	if err != nil {
		return data{}, err
	}

	for _, r := range d {
		if b, err := regexp.Match(filter, []byte(r)); b && err != nil {
			continue
		}
		// do something with filtered data
	}
	return sth, nil
}

func Read(fileName string) ([]string, error) {
	f, err := os.Open(fileName)
	if err != nil {
		return nil, err
	}

	scn := bufio.NewScanner(f)
	scn.Split(bufio.ScanLines)
	out := []string{}
	for scn.Scan() {
		out = append(out, scn.Text())
	}
	f.Close()

	return out, nil
}

Think about how you would unit test Render.

Probably your thoughts are along the lines of

  • Create a few test files with different inputs and files with matching outputs, and run them through Render. OR
  • Have some constant strings in the test, and on the fly write them into temporary files who’s names are passed to Render. OR
  • Use an in-memory file system implementation where paths are automatically recognized and redirected in the os.Open call or pass an extra argument to Read that tells it whether to read from the real OS file system or a test in-memory file system.

All of this just to get a data object that you can then render. If you also think about how you would add the ability to render data coming from a network stream or database, you will end up with more arguments, more if-else or switch statements, and even more complicated test setups.

This is by no means a contrived example. I have seen plenty of code exactly like this throughout my career.

To rewrite this code to use dependency injection you have to do two things:

  • Create a controller function that knows how to create the data objects.
  • Change the signature of the functions to receive the data objects they need.

func controller(fileName, filter string, mode int) {
	fc, err := Read(fileName)
	if err != nil {
		log.Fatal(err)
	}

	d, err := Process(fc, filter)
	if err != nil {
		log.Fatal(err)
	}

	output, err := Render(d, mode)
	if err != nil {
		log.Fatal(err)
	}
	// continue with rendered output
}

func Render(input data, mode int) ([]byte, error) {
	switch mode {
	case HTML:
		// render HTML output
		return nil, nil
	case CONSOLE:
		// render console output
		return nil, nil
	default:
		return nil, fmt.Errorf("invalid mode: %d", mode)
	}
}

func Process(input []string, filter string) (data, error) {
	for _, r := range input {
		if b, err := regexp.Match(filter, []byte(r)); b && err != nil {
			continue
		}
		// do something with filtered data
	}
	return sth, nil
}

func Read(fileName string) ([]string, error) {
	f, err := os.Open(fileName)
	if err != nil {
		return nil, err
	}

	scn := bufio.NewScanner(f)
	scn.Split(bufio.ScanLines)
	out := []string{}
	for scn.Scan() {
		out = append(out, scn.Text())
	}
	f.Close()

	return out, nil
}

Now think about how you would unit test Render. The test for the rendering logic is now completely independent of how you obtain the data in the first place. It no longer matters if the data is read from a file or a network stream or database, the test will remain the same. All that extra knowledge is now located and contained in the controller function.

Why is this so important?

It makes code easier to understand, change and test. [3]

And where do injection frameworks come in? Usually, they make the controller function obsolete by using reflection or some type system features to identify argument types and constructors or factories for them. The advantage of the framework approach is that there is a little bit less code to write. But that’s also a disadvantage, because the code in the controller function usually tells you exactly when and how an object is constructed, while frameworks hide this. When debugging, that hidden information is often important, yet very difficult to track down when hidden inside the framework.

In my experience and preference, this disadvantage far outweighs the benefits of frameworks, so I always favor creating objects explicitly in code.

Dependency injection is also a key technique to make programs more flexible, by injecting configuration and pieces of code, for example in the Strategy design pattern [2]. We’ll cover those uses in Part 2 of the series.

Required Reading

  1. Dependency Injection on Wikipedia
  2. Gamma, Erich, Richard Helm, Ralph Johnson, John Vlissides, and Grady Booch. 1994. Design Patterns: Elements of Reusable Object-Oriented Software. 1st edition. Reading, Mass: Addison-Wesley Professional.
  3. Thomas, David, and Andrew Hunt. 2019. The Pragmatic Programmer: Your Journey To Mastery, 20th Anniversary Edition. 2nd edition. Boston: Addison-Wesley Professional.

By Admin

Leave a Reply

Your email address will not be published. Required fields are marked *