On Dependency Injection, Part 1: Why and How
Motivation
Dependency Injection is one of the things that I wish had come up when I was first learning how to program. And to an extent, it did, I just didn’t realize its full potential until it was shown to me on a much larger scale in my first job as a software engineer. I intend to do this for you with this three-part series, although I have some ideas for some examples that might be based off of this series.
Introduction
Dependency Injection is also known as “Inversion of Control,” which is a little more illustrative of what’s actually going on. Instead of classes resolving what they need on their own, instead they ask for the minimum interface that they need.
The Problem
This is easiest explained with an example, so here goes. I’m using Python because it’s fairly simple to follow regardless of what your primary language is.
Here’s my original implementation, a program even simpler than FizzBuzz:
main.py
|
|
This is neat code, as it has one class handling the logging, and another handling the main functionality which happens to be counting to 100 and logging based on what number we find.
Lets say that we want to change some functionality. Maybe we want to log to a file instead. That’s not too hard:
main.py
|
|
We even can change out the file we’re logging to easily, just by passing that as a parameter. Neat!
Now we need to change our implementation of Counter as well:
main.py
|
|
Python’s duck typing takes care of a lot of this for us, so the only line we need to change is in the constructor. In other languages, that might have needed more changes to get the types right, but still a fairly straightforward change nonetheless. Let’s make another one.
Let’s introduce another type of logger, one that logs to an array so we can verify that the correct things are logged:
main.py
|
|
Now we run into a problem: if we want to use the ArrayLogger to test that things run the right way, we can’t also use a different one when we actually run the program. Additionally, we can’t get to that array logger without exposing it on the class. To solve that, we might add a flag to the constructor, or maybe create a subclass that uses a different logger. Either way we have a new getter, and things aren’t looking as elegant as before.
Flag Approach Yikes, that’s a lot of special casing. Not so much single-responsibility anymore.
main.py
|
|
Subclass Approach Look at all that repetition. We need to keep the methods in sync, too, to keep up with Python’s name mangling.
main.py
|
|
Neither of those seem super elegant. Let’s try something else.
Enter Dependency Injection
To start, let’s pull the functionality of the different loggers into a base class that they all can extend from:
main.py
|
|
Next up, let’s do the actual dependency injection. This line is creating a dependency:
main.py
|
|
Instead, let’s use the interface we just created to invert control over that dependency. Counter does not need to worry about which type of logger it gets – just that it gets one at all. I’m going to take this a step further and also ask for how many numbers we should log in the run function:
main.py
|
|
Now when we want one to test, we can give the class the type of logger we need:
main.py
|
|
If we’re running this as the main file, we can use the console logger:
main.py
|
|
We can use loggers over again if we want to, as well:
main.py
|
|
Simple as That
That conversion, just asking for what we’re looking for instead of going and getting it or constructing it ourselves is Dependency Injection. This can fairly easily solve what’s called Static Abuse in other languages, and as I’ll discuss below, it can also help to make code easier to reuse.
Benefits of DI
Some of the benefits I tried to outline above fall into the below categories:
- Allows code to be more generalized
- Breaks coupling and explicit dependencies
- Makes it easier to test things by intercepting data or mocking it away
Generalized Code
Above, we had a counter that anytime we wanted to switch out its dependency on a Logger, we had to modify the implementation of the class. When we needed multiple at the same time, we had to duplicate the code. By cutting that out, we instead get code that uses what it’s told without needing to worry about what that other code is doing. If we wanted to write a logger that uploaded our logs to an API somewhere, that would be simple in the new code, but we’d still have to deal with making source code modifications in the older code because it’s tightly coupled, which brings me to my next point: coupling.
Breaks Coupling
Code is considered tightly coupled when its behavior or implementation can’t be fully executed or understood without understanding another piece. Code like that generally is better off as a single unit, but sometimes dependencies can be broken out and separated which also generally results in better reusability for both pieces.
Easier Testing
Testing gains two things from injected dependencies. First, parts of the code
that consume data produced by the targeted part of code can be mocked out so
that they record what is produced and allow it to be verified. That’s what
ArrayLogger
is doing up above. On the other hand, it also allows parts of the
code that produce data needed for the targeted part of the code to be mocked
out. In more complex software, this can be things like rendering backends, API
calls, or database functionality, which makes it easier to run tests quickly
and with a minimal set of required outside setup.
In Conclusion
Sometimes Dependency Injection gets conflated with Dependency Containers or DI Frameworks, which I’ll be covering in the next part, but the basic concept is what I’ve outlined here – ask for what you need, rather than trying to get it yourself.