What is dependency injection after all?

What is it, and how dependency injection is helping out in software development.
Tools on worktable

When talking with friends and family starting their software career I often hear the words dependency injection accompanied by DaggerGuice or any other tool aiming to help out injecting your application’s dependencies. Unfortunately barely any of them actually understand or grasp why it’s needed and why do we need such tools to accomplish it. Some are even surprised when I tell them you don’t really need these tools to have dependency injection and that they’re just helpers. Really good helpers, but still just helpers.

Personally I believe the tools are great and help you develop faster and deliver better quality code. However, it’s important to understand the concepts behind. This often makes the tools easier to understand.

I’d like to provide at least one explanation with a step by step approach to what I learned and how I see dependency injection helping out in software development.

Starting with functions

Usually one explains dependency injection using object oriented paradigms. I’d like to first try to explain it with functional programming, because I think it’s simpler. Dependencies are everywhere in software. Hard dependencies or tight coupling as we mostly know them make it harder to extend, test and scale the code, while loose dependencies (or loose coupling) tend to be easier to deal with. So to start with we need to understand what really is a dependency.

In functional programming, functions are first class citizens. This means they can be passed as arguments, returned by other functions, assigned to variables, among other things. Yes you’ve guessed it, in functional programming dependencies are functions.

Let’s look at the map example. I’ll be using Elixir because it’s the most purely functional programing language that I’m comfortable with. I’ll do my best to explain the syntax. Elixir already has this function in the standard library, but for the purpose of this post I’ll just implement it myself. We have a list of integers and at some point in our application we need to square these integers:

def map_to_squares([]) do
  []
end

def map_to_squares([head | tail]) do
  [head * head | map_to_squares(tail)]
end

Using Pattern MatchingElixir decides which function to call. When the list is not empty it will call the second function – map_to_squares([head | tail]) – and recursively build another list until eventually it’s empty. At this point it will call the first function – map_to_squares([]).

Calling map_to_squares([1, 2, 3, 4]) yields the list [1, 4, 9, 16].

Can we call this function with another list and obtain its squared elements? Absolutely!

Can we call this function with a list and apply another operation to the list’s elements? Say we want to return a list of 1s and 0s depending if the number is odd or even respectively? No!

Why can’t we do this? Because the function can only square items. It has the squaring function coupled with its implementation. you can see it in head * head. In other words, map_to_squares is tightly coupled with the squaring function.

How can we make this work for any function? We need to invert the control. Simply put – we no longer say what function we’re going to apply to the list, but we let the calling code specify this function:

def map([], _) do
  []
end

def map([head | tail], fun) do
  [fun.(head) | map(tail, fun)]
end

So now we have a function map that receives another function. This function can be anything as long as it receives 1 parameter and this parameter is an element of the list.

Calling map([1, 2, 3, 4], fn x -> x * x end) will still yield [1, 4, 9, 16], but now we can also call it like so:

map([1, 2, 3, 4], fn x ->
    if Integer.is_even(x) do
      0
    else
      1
    end
end)

and get the list [1, 0, 1, 0]. We no longer care what function we need to apply to the list members as long as it respects the rules already defined.

We’ve successfully given control to whom calls this function. The dependency to the mapping function is still there, but now it’s no longer tightly coupled. This makes it much more flexible and reusable. We don’t have to write several functions that apply an operation to each element in the list. We can use just this one and inject the operation as a function.

Notice also that we have decoupled the logic of the mapping function from the logic that iterates through the list. This is really good for testing. We can now make sure that our function iterates all elements in the list and calls the given mapping function for each element.

It’s worth noting that in functional programming there are others ways of addressing these dependencies which might be better than the one explained here – i.e. Partial Application

In the OO world

Things are not that different in the Object Oriented world (OO). If you are no stranger to this paradigm you know that often your objects have a lot of dependencies. These dependencies are other objects. I’ll be using Kotlin for the next examples mainly because I’m an Android developer. I know one can use a lot more functional constructs in Kotlin than in other OO languages, but for the sake of the argument I’ll try to stay away from these.

Let’s imagine the following scenario. We’re building an application for some transportation company which has several transport mediums and drivers. Each driver can drive one or more transports – bus, plane, car, etc. One could start thinking of having the following:

class Bus {
  fun drive() = println("Driving on the road")
}

class Driver {
  lateinit val bus: Bus

  fun drive() = bus.drive()
}

As you can see we have a class Bus that can be driven and a class Driver that for now can only drive a bus. There are quite some issues with this approach, but let’s start with the bus dependency from the driver class.

With this approach the driver can only drive buses. However we were told that drivers could drive more than one transportation medium. So first thing we need to do is generalize the Bus class. Let’s build an interface for a generic transport.

interface Transport {
  fun drive()
}

class Bus : Transport {
  override fun drive() = println("Driving on the road")
}

This step is very important because it will enable us to make sure the Driver class depends on a generic transport and not only on a bus enabling the driver to effectively drive more than one transport. If you recall the example with functional programming this is similar in that the mapping function specifies a signature, but not how it should behave. Likewise we should specify interfaces and not the behavior. As a rule of thumb always depend on abstractions rather than concrete implementations.

We can now write our Driver class as follows:

class Driver {
  lateinit var transport: Transport

  fun drive() = transport.drive()
}

Now the dependency to Bus is no longer present. The Driver class depends on an interface that describes the contract for transports. We can now have a single driver driving multiple transports:

interface Transport {
  fun drive()
}

class Bus : Transport {
  override fun drive() = println("Driving on the road")
}

class Airplane : Transport {
  override fun drive() = println("Flying over the road")
}

class Driver {
  lateinit var transport: Transport

  fun drive() = transport.drive()
}

fun main(args: Array<String>) {
  val driver = Driver()
  val plane = Airplane()
  val bus = Bus()

  driver.apply {
    transport = plane
    drive()
  }
  driver.apply {
    transport = bus
    drive()
  }
}

As you can see the created driver now can drive both a bus and a plane as well as any other class complying to the Transport interface. Notice also how we’ve created another class – Airplane – that can be used with the class Driver and yet we didn’t have to touch the Driver class.

We’ve given control to the code using Driver. This is why we say dependency injection is a form of inversion of control.

What we’ve seen here is a form of dependency injection where the dependency is injected using a setter. This was done to keep the example simple. Other forms of injection include field injection (which in Kotlin looks pretty much the same) and constructor injection.

I personally prefer the approach of using constructor injection simply because it becomes impossible to call the methods without initializing all dependencies. As an example:

class Driver(private val transport: Transport) {
  fun drive() = transport.drive()
}

Here the dependency must be passed when the instances of Driver are created and therefore it’s impossible to forget about it before invoking the method Driver.drive(). Also I take advantage of Kotlin’s type system to avoid nulls here. However, this would not work for our application because it requires one driver to drive multiple transport mediums.

One last thing that is very important to notice. We can now test the Driver class without worrying about which transport medium it’s using under the hood. In fact, because we inverted the control to the calling code, during the tests we can call this class with a mock object and avoid creating a real transport making it way easier to test.

So why are tools like Dagger and Guice needed?

As we’ve seen now dependency injection is a concept that essentially lets the calling code have control of what’s being used. In the given example the dependencies are quite simple and trivial. So much that we actually manually created all the objects and injected every dependency.

However in a real world scenario things are not this simple. Usually your dependencies have dependencies which will have dependencies of themselves and so on. Usually all of these dependencies have a certain life time that needs to be managed. Things get complicated pretty fast and we end up with a graph of dependencies that is too hard to create and maintain.

Here’s where tools like Dagger and Guice help us out. They make it a lot easier to manage these dependencies. They create the dependency graph by themselves and ensure that when you request a given object, all its dependencies are fulfilled. This removes a lot of boilerplate code and boosts productivity.

Wrap up

In this post I’ve tried to give at least one reason why dependency injection is important in software development. We’ve started with a functional approach and moved to the Object Oriented world.

Here we know that there are a lot of tools to help us out with building and maintaining our dependency graph. Dependencies are present everywhere and they should be taken into account. Hard coded dependencies will make it almost impossible to extend the class’ behavior without touching it. Harnessing these tools helps us in the short and long term.

Photo by Sneaky Elbow on Unsplash

Want to join our Engineering team?
Apply today!
Share:
Frederico Gonçalves

We are a team of more than 750 people from over 50 nations with a shared passion for languages. From our offices in Berlin and New York, we help people discover the joys of self-directed language learning. We currently offer 14 different languages — from Spanish to Indonesian — that millions of active subscribers choose to learn.

We are a team of more than 750 people from over 50 nations with a shared passion for languages. From our offices in Berlin and New York, we help people discover the joys of self-directed language learning. We currently offer 14 different languages — from Spanish to Indonesian — that millions of active subscribers choose to learn.