Dependency Injection: A Gentle Introduction
Introduction: What Is Dependency Injection?
Dependency Injection is a software design principle where an object's dependencies are
provided to it, rather than the object creating its own dependencies.
I noticed that many of my students initially have trouble grasping the concepts and benefits of Dependency Injection, so I decided to write this article as a general introduction.
What is a Dependency?
In a nutshell, a dependency is something a class needs in order to do its work. Imagine an application that needs to log various activities. In that scenario, you would typically have a class whose responsibility is to handle logging. Classes that need to log things therefore have a dependency on the logging component.
What exactly does 'Injection' mean?
'Injection' just refers to the concept of a class getting its dependencies from an external source (often passed as constructor arguments) rather than creating them internally.
This approach is advantageous because it allows us to specify which specific implementation will be used in which cases.
Let's consider the logging scenario from above:
public class SomeClass(){
ConsoleLogger logger;
public SomeClass(){
logger = new ConsoleLogger();
}
public void DoSomeWork(){
logger.Log("About to do some work.");
// ...Do some work...
}
// ...Rest of class...
}
That doesn't look so bad...
True, this isn't the worst possible implementation, but it does have some subtle limitations:
- The fact that
SomeClass
has a dependency onConsoleLogger
is not readily apparent unless you look at the source code.
This is called a Hidden Dependency. - If, in the future, you want to change from using a
ConsoleLogger
to a (hypothetical)AzureQueueLogger
, you have to edit SomeClass and any other classes that depend onConsoleLogger
to use the newAzureQueueLogger
implementation. In a production application, this may mean editing every file in the entire solution. - Because
SomeClass
creates its ownConsoleLogger
, you cannot write tests that verifySomeClass
interacts withConsoleLogger
as expected.
(i.e. verifying the correct message is logged, or even if anything is logged at all) - For the same reason, if you write unit tests for
SomeClass
, you will need a fully-functioningConsoleLogger
implementation, which means that you can't testSomeClass
in isolation from the rest of the system.
Now let's refactor to inject our Logger dependency:
public class SomeClass(){
ILogger logger;
public SomeClass(ILogger logger){
this.logger = logger;
}
public void DoSomeWork(){
logger.Log("About to do some work.");
// ...Do some work...
}
// ...Rest of class...
}
Notice that instead of creating a Logger
directly, we ask for it in the form of a constructor parameter. What that means is that anyone who wants to work with SomeClass must provide an ILogger
for it to work with.
Another subtle difference is that we now use an
ILogger
interface instead of a concreteLogger
class.ILogger
is an abstraction that allows us to substitute any type that implementsILogger
without changing the SomeClass code. Abstractions and Dependency Injection often go hand-in-hand.
So what's the Difference?
Functionally, there isn't one - SomeClass.DoSomeWork()
operates identically in both examples, however the second example removes all of the limitations imposed by the first example:
- It's explicitly clear that
SomeClass
requires anILogger
in order to do its work - in fact, you can't create one without it. - We can use
SomeClass
with any type that implementsILogger
, instead of being limited to one specific implementation. - It is now possible to write tests that verify the correct interaction between
SomeClass
andILogger
. - Because we are using the
ILogger
abstraction, we can provide a mockILogger
when testing, enabling us to testSomeClass
in complete isolation.
In a nutshell, we can see that Dependency Injection moves the responsibility for creating dependencies outside of the class that uses them.
Is There a Downside?
As with all tools, Dependency Injection has benefits and drawbacks.
One notable side effect of the Dependency Injection pattern is that as your application grows, so does your dependency list. This can rapidly lead to unwieldy constructors, for example:
public class HasManyDependencies{
public HasManyDependencies(
ILogger logger,
ILocalRepository localRepository,
IWebRepsitory webRepository,
IMoonPhaseCalculator moonPhaseCalculator){
// ... imagine the class does something
//useful with these dependencies
}
}
So if we want to create an instance of HasManyDependencies, our code will look something like this:
var hasManyDependencies = new HasManyDependencies(
new Logger(),
new LocalRepository(),
new WebRepository(),
new MoonPhaseCalculator());
That's some fairly ugly code just to get an instance of HasManyDependencies, but it can be even worse - Imagine, for example, if LocalRepository, WebRepository, and MoonPhaseCalculator have dependencies of their own, the constructor call rapidly becomes painful to write.
The good news is that there are tools we can use to mitigate this constructor madness:
Inversion of Control (IoC) to the Rescue!
To solve the constructor complexity issue, we can use an Inversion of Control Container (IoC Container). The container takes on the responsibility of creating instances and fulfilling their dependencies in a way that greatly mitigates the impact of complex constructors.
"Inversion of Control" is a broadly-defined software engineering principle that refers to inverting the flow of control compared to traditional procedural programming. In the case of Dependency Injection, the inversion is related to dependency creation and lifecycle management.
Getting Started with Dependency Injection Using Castle Windsor.
Note: This post will only touch on the subject of IoC. For more information, check out
In regular procedural programming, it is common to create instances using the new
operator. When using an IoC container, we instead ask the container for an instance (called 'Resolving') and it gives us what we asked for. For example, instead of the mess of code above, our code would look something like this:
// Assume that 'container' is a configured IoC container...
var hasManyDependencies = container.Resolve<IHasManyDependencies>();
In the code above, when we call container.Resolve<IHasManyDependencies>
, the container creates all of the dependencies required by HasManyDependencies
(and any dependencies they may have themselves) and provides us with a ready-to-use HasManyDependencies
instance.
The benefit to this approach is that we now have a class (the IoC container) that has the sole responsibility for creating and injecting dependencies, freeing us from the burden of complex constructors.
Conclusion:
Although Dependency Injection appears confusing on the surface, in reality it's just a slightly different pattern that provides multiple benefits to your code:
- Loose coupling enables more flexible, composable software implementations and facilitates code reuse.
- Vastly improves testability by enabling test suites to inject specialized test implementations instead of relying on a production stack.
- When combined with Abstractions, enables us to swap implementations at runtime based on runtime conditions.
- Works cleanly with IoC containers to mitigate inherent constructor complexity.
Dependency Injection also carries some indirect benefits, encouraging practices such as SRP (Single Responsibility Principle) and the Open-Closed Principle, leading to cleaner, more easily maintained code.
good
Thanks for this wonderful article, Sir!
I see that you have used interface to achieve dependency injection, if I am not wrong. Having gone through Factory pattern, it made me think that Interface injection and Factory Pattern have similarities. Isn’t that so? Just curious.
An easily digestible article on dependency injection.