Understanding Dependency Inversion has been a long journey for me. I quickly understood Dependency Injection as it gained popularity and, as somebody who likes automated unit tests, could immediately see benefits of using it for testing. What is Dependency Inversion though and how does it differ from Dependency Injection? In this article we will look at a very simple example of Dependency Injection and develop it further to consider Dependency Inversion.
Different Types of Dependency
To understand Dependency Inversion you need to understand two different types of dependency:
- Flow of Control Dependency - when you are debugging code and the line of execution goes from one project into another project or dll.
- Source Code Dependency - where one project references another project or dll.
What do we mean by Dependency Inversion
Dependency Inversion is where the Flow of Control Dependency goes in one direction between two projects or dlls, but the Source Code Dependency goes in the other direction.
Why Invert a Dependency?
We invert a Source Code Dependency when we identify that the rate of change of a project is more than the project that depends on it. In this scenario we have a project that rarely changes so shouldn't need to be recompiled or redeployed, but it does every time the project it depends on changes. Inverting the Source Code Dependency between the two projects means that the project that changes the most now has a Source Code Dependency on the one that changes the least, meaning only the project that changes the most needs to be recompiled and deployed. The Flow of Control Dependency does not change though.
Simple C# .NET Core Example
Lets take a look at a simple C# .NET Core example starting at a point where we are injecting a dependency. A common case for Dependency Injection is logging. Logging is likely to change, often using a different implementation or location between environments such as production and development.
The starting point for our example looks like this. We have a single project called OurApp.exe with 2 classes. One that implements a simple logging function and the program class which runs when the executable runs.
Logger.cs looks like this:
and Program.cs looks like this:
Notice that when Main runs in Program.cs, on line 9 it creates an instance of Logger which it then injects into the DoSomething() method. The DoSomething method has a Flow of Control Dependency on Logger and when it gets to line 15 the flow of control will be passed from Program.cs to Logger.cs for the duration of the Log method. This is a very simple example of Dependency Injection and makes the DoSomething method very testable as we can mock out out a Logger and inject that in to test the DoSomething method does what we expect without relying on the implementation of Logger.cs. I haven't used a Dependency Injection Framework as I wanted to show Dependency Injection in it's simplest form.
Let's look at what this looks like in terms of dependencies and rate of change. Everything is very simple currently with only one project, OurApp.exe:
Consider Rate of Change
Our problem currently is that Logger is likely to change more than the domain functionality of DoSomething(). Once the domain functionality has been developed as per the real world domain it may never change, but Logger will likely change between different environments. Our current codebase means every time we want to change Logger, we need to recompile the entire OurApp.exe and deploy it. In a small example like this that might not be costly, but when you get a real world application with a large dependency tree it can become not only time consuming but risk deploying changes that shouldn't be deployed.
Encapsulate and Inject Things that Change More
We are already injecting Logger into the DoSomething() function, but we can Encapsulate it into a class library that OurApp.exe then depends on. It also makes sense to Encapsulate DoSomething() to a different class library as it will change at a different rate to both Logger.cs and OurApp.exe.
The solution now looks like this, note our new class libraries called Logging.dll and OurDomain.dll, and the Source Code Dependency from OurApp.exe to both of them:
Logger.cs has been moved from OurApp.exe to Logging.dll. Logger.cs itself doesn't change apart from it's namespace now matches the dll it has been abstracted to:
DoSomething() method has been moved into a new class called DomainEntity.cs in OurDomain.dll, which needs a Project Reference to Logging.dll as Logger.cs is still injected as a dependency into DoSomething():
Program.cs still creates an instance of Logger to inject into the DoSomething method, but also needs to create an instance of the EntityDomain class that now encapsulates the DoSomething method:
If we revisit the dependencies now we can start to build up a picture of dependencies and rate of change, and identify any opportunities to invert Source Code Dependencies.
Opportunity to Invert a Dependency
The Flow of Control Dependencies are fine, we do not need to worry about those. If we consider the rate of change though, we expect Logging.dll to change more than OurDomain.dll, so although we are already injecting the Logger.cs from Logging.dll into the DoSomething() method on DomainEntity.cs in OurDomain.dll via OurApp.exe, we really need to invert the Source Code dependency to point the other way, from Logging.dll to OurDomain.exe. The Source Code Dependencies will then match the expected Rate Of Change. Currently if Logging.dll changes we would need to recompile and deploy OurDomain.dll as well, which is less likely to have been changed. OurApp.exe is an executable, or the lowest level module, that will be recompiled and redeployed whenever anything in the dependency tree beneath it changes so we do not need to worry about that.
Dependency Inversion Principle
The Dependency Inversion Principle states:
High level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
We have already seen the opportunity to invert the Source Code Dependency between Logging.dll and OurDomain.dll because the direction of the Source Code Dependency does not match our expected rate of change for those two modules. If we start to consider the Dependency Inversion Principle we can further confirm the opportunity for Dependency Inversion. Look at our modules set out in a Clean Architecture Onion style diagram with high level modules in the middle and low level modules in the outer ring. All Source Code Dependencies should flow in from the low level modules to the high level modules. We can clearly see we have a dependency going in the wrong direction, from the high level OurDomain.dll module in the middle towards the low level Logging.dll module in the outer circle:
Also, when we consider Details and Abstractions, an Abstraction is an interface and a Detail is an implementation. We currently are not working against abstractions in our modules. For instance the DomainEntity.cs class is a Detail, and the Logger.cs that we inject in to satisfy it's Flow of Control Dependency is also a Detail.
Depend on Abstractions
Our implementations are now encapsulated into cohesive modules, and we have identified the expected rate of change of those modules, Flow of Control Dependencies, and Source Code Dependencies. This information has helped us identify an opportunity to invert a Source Code Dependency to avoid the need to recompile and deploy the OurDomain.dll module every time Logging.dll changes, and we expect Logging.dll to change more than OurDomain.dll.
Our first step in Dependency Inversion is to ensure that our Details depend on Abstractions, so let's have a look at the dependency that the DomainEntity.cs Detail in OurDomain.dll has on the Logger.cs Detail in Logging.dll. All we need to do is define an ILogger interface in Logging.dll, update Logger.cs to implement that interface, and finally update DomainEntity.cs so the dependency injected into the DoSomething() method expects the ILogger Abstraction rather the existing Logger Detail.
Our solution now looks like this:
The new ILogger interface, or Abstraction, in Logging.dll looks like this:
DomainEntity.cs in OurDomain.dll now looks like this, with the previous Logger Detail being injected into DoSomething() replaced by the new ILogger Abstraction:
Note that the Main method in our Program.cs of the OurApp.exe module does not need to change at all. It still creates a concrete instance of the Logger detail which it injects into the DoSomething() method of the DomainEntity.cs Detail:
Also note that no Source Code Dependencies have been changed during this step to change Details to depend on Abstractions. This step is more around correcting Flow of Control Dependencies rather than Source Code Dependencies, which we will tackle next.
Low Level Modules Should Depend on Higher Level Modules
As we saw in our Onion Clean Architecture diagram, our higher level OurDomain.dll module currently depends on the lower level Logging.dll module which violates the Dependency Inversion Principle. Now that our DomainEntity.cs Detail is dependent on the ILogger Abstraction rather than the Logger detail, the final step to resolve the Dependency Inversion Principle is to simply move the ILogger definition from the lower level Logging.dll module into the higher level OurDomain.dll module. This also makes sense in that the Flow of Control Dependency that DomainEntity.cs Detail has on an ILogger Abstraction is now owned by the higher level OurDomain.dll module, rather than the lower level Logging.dll module.
It is worth changing the namespace of the ILogger Abstraction as we move it to OurDomain.dll so it now looks like this:
We also need to remove the Source Code Dependency from OurDomain.dll to Logging.dll, and remove the "using Logging" namespace import from the top of DomainEntity.cs.
In the Logging.dll module, we need to add a Source Code Dependency to OurDomain.dll, then import the namespace at the top of the Logger.cs Detail:
Our solution now looks like this:
Our final Source Code Dependencies between projects looks like this:
- The low level OurApp.exe module is dependent on both the low level Logging.dll module and the higher level OurDomain.dll module.
- The low level Logging.dll module is dependent on the higher level OurDomain.dll module.
- The high level OurDomain.dll module is not dependent on any other module, and therefore is not dependent on either of the lower level modules.
To summarise the Source Code Dependency changes we've made in this step, we have inverted the Source Code Dependency that previously pointed from the high level OurDomain.dll module to the low level Logging.dll module. If we revisit our dependencies and rate of change diagram, we can see that this changes the order of the Source Code Dependencies to now match the rate of change.
Our Onion Clean Architecture diagram now clearly satisfies the Dependency Inversion Principle, showing all Source Code Dependencies flowing from the outer low level modules towards the inner high level modules. The direction of the arrow between Logging.dll and OurDomain.dll has been inverted to point in the opposite direction:
It may feel like a long journey to get to this point but inverting a dependency can be summarised in the following two steps:
- For Flow of Control Dependencies, inject dependencies as interfaces, or abstractions.
- Ensure the abstractions, or interfaces, are defined in a higher level module and implemented in a lower level module to ensure the Source Code Dependency is Inverted.
Knowing both when to invert a dependency and how, we can approach designing and writing code with this in mind, so the Dependency Inversion Principle is followed instinctively rather than following steps retrospectively to resolve violations.
- Clean Code by Robert C. Martin
- Clean Architecture by Robert C. Martin
- An Atypical ASP.NET Core 6 Design Patterns Guide: A SOLID adventure into architectural principles and design patterns using .NET 6 and C# 10 by Carl-Hugo Marcotte
- Dependency Injection Principles, practices and patterns by Steven van Deursen