Why use dependency injection?
Yet another article covering the benefits of this daunting design pattern
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 2. Here we have another class, aCar
. The Car
needs a Driver
.
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
.
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.
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.
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
.
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 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 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.
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.