Why use dependency injection?

Yet another article covering the benefits of this daunting design pattern

Chris Cooper
Trade Me Blog
5 min readOct 4, 2017

--

When I was first introduced to Dagger it solved a lot of issues that I had with my Android development. It gave organization to my dependencies and allowed my code to be more testable. However, it was actually the principle of dependency injection doing all the hard work. Dagger was just assisting in this principle as a framework.

As I began to research more into dependency injection, I found that the explanations and examples of why it was such a good design pattern were quite complicated. I felt that anyone looking to get a grasp on the topic would get intimidated and turn away. So, I wanted to give my take on dependency injection and run through a lightweight example. Hopefully, it will show that it is really just a fancy term for a simple concept.

Fig 1. Here we have a simple Driver class, to create it we do not need anything else.

Fig 1. Simple Driver class

Fig 2. Here we have another class, aCar. The Car needs a Driver.

Fig 2. Simple Car class

The Car is dependent on a Driver and it is also responsible for the creation of the Driver. At the moment, this setup is fine but what happens when we decide to modify the Driver class?

Fig 3. What if we now say that in order for us to create a Driver, it must also have a License.

Fig 3. Driver class now requires a License

How is this going to affect our current code? It breaks.

Fig 4. As before, when we create a Car it then goes on to create its Driver. But, the Driver is now missing the newly required License that we said it should have.

Fig 4. Our Car class is now broken due to the Driver needing a License

So, we have to refactor our code. We have to go through and find all the instances of Driver and give it the now required License. This is fine for the single usage in our example, but not in a real world code base. This Driver class could be created in many, many places which would result in a large refactor. Currently, we have implemented a very poor design pattern.

What can we do about this? We can use dependency injection.

To recap, our Car is currently responsible for the creation of its Driver. But what we actually want is to have the Driver dependency injected into the Car. The Car does not care where the Driver comes from, as long as it gets a working one. We want to use what we are given, rather than what we create.

Fig 5. Take a look at our refactored Car class which is now using dependency injection.

Fig 5. Refactored Car class now has the Driver injected

You may now be asking, well where is our Driver going to be created and who is going to provide it for us? This is where our frameworks come in. It will be entirely up to their implementation of how and where we define the construction of our dependencies. Here we begin to move into the fundamentals of the individual frameworks rather than dependency injection. So to keep on topic, I will not go into further detail. The important part here is that we have shifted the creation of the Driver away from the Car.

Fig 6. Now, with our new way of doing things and having the Driver injected, we can go ahead and modify the Driver as much as we like. We can say that when we create a Driver, it must also have a License, it must also have some RacingGloves, it must also have some RacingGoggles.

Fig 6. Driver now requires several dependencies. But our Car is unaffected

And this is fine! Our Car class remains unaffected by all these changes. As long as it is getting a working Driver it does not care. By shifting the responsibility of creating the needed dependency to something else (for example your framework) we can go ahead and modify it without having to update all those who use it.

How does this tie in with code testability?

Having our dependencies injected also allows for us to have more control over them. This is especially useful when it comes to testing.

Fig 7. First, take a look at an example with our old Car. It now has a drive() method that relies on the interaction with its two dependencies Driver and Engine.

Fig 7. Car class and its drive() method without dependency injection

Fig 8. Let us say we want to test the drive() method. To prevent the actual code behind our two dependencies from running, we need to create mocks of them. We will use these mocks during our tests instead of the real classes. However, we simply cannot do this as currently we are restricted to the Driver and Engine that are being created by the Car. As these two objects are being generated via the new operator inside the Car's constructor, we cannot change their behavior.

Fig 8. A test for the drive() method when our Car class is not using dependency injection

Fig 9 & 10. But, with our refactored Car that uses dependency injection, we are able to create and inject any Driver or Engine that we choose for our tests. In this case our mocked objects.

Fig 9. Car class with dependencies injected
Fig 10. Same test method but with our Car class now allowing our mock dependencies to be injected

Using this dependency injection design pattern gives us the freedom to manipulate how the objects react to different method calls. It allows us to write tests that are flexible and that cover a wide range of code paths. I think it is also worth reinforcing here that we are not using any libraries to assist with injecting our dependencies. We ourselves have handled the dependency injection for the above driveMethodTest(). It is purely a design pattern.

So, why do we even bother with frameworks? These frameworks manage dependency injection for us on a much larger scale and make our lives far easier. They handle common problems like circular dependencies and make sure that we are initializing our classes at the correct times before they get used.

With the use of dependency injection libraries being so popular these days, I think it is great to have an understanding of how they are actually helping us and have an insight to the underlying concepts. If you were struggling to fully grasp the idea of dependency injection, hopefully the example we have just been through has shed some light on the topic. Perhaps you can now look into implementing it in your next project.

--

--