Introduction
Across the posts, there has been the mention about something called Dependency Inversion or Dependency Injection. In this post, we will take a deeper look at what that actually means and how it helps us.
Dependency Inversion
When we talk about dependency inversion, we talk about how we structure our projects so that we can modularize them. This allows us to effectively inverse how we compose our projects and components, let’s look at a few examples and try to see how those help us and what exactly is inverted.
Our first example comes from FileSystem Syncher – Part 3, if you recall, we wrote the following class:
public sealed class FileSystemProcessor
{
private readonly IEnumerable<FileProcessingStrategyBase> _strategies;
private readonly IConfigurationProvider _configurationProvider;
public FileSystemProcessor(IConfigurationProvider configurationProvider,
IEnumerable<FileProcessingStrategyBase> strategies)
{
_configurationProvider = configurationProvider
?? throw new ArgumentNullException(nameof(configurationProvider));
_strategies = strategies ?? Enumerable.Empty<FileProcessingStrategyBase>();
}
public void Run()
{
ConfigurationOptions configurationOptions = _configurationProvider.GetOptions();
FileSystemEnumerator fileSystemEnumerator = FileSystemEnumerator.CreateInstance
(configurationOptions.Whitelist, configurationOptions.BlackList);
foreach (FileProcessingStrategyBase fileProcessingStrategyBase in _strategies)
{
foreach (FileInfo sourceFile in fileSystemEnumerator.EnumerateFilesBreathFirst(
configurationOptions.SourceDirectory))
{
string destinationFilePath = sourceFile.FullName.Replace(
configurationOptions.SourceDirectory.FullName,
configurationOptions.DestinationDirectory.FullName);
FileInfo destinationFile = new FileInfo(destinationFilePath);
if (!destinationFile.Exists || sourceFile.Length != destinationFile.Length
|| sourceFile.LastWriteTime != destinationFile.LastWriteTime)
{
fileProcessingStrategyBase.ProcessFiles(sourceFile, destinationFile);
}
}
}
}
}
In this snippet, we can see that the constructor receives an object of type IConfigurationProvider
, that means we don’t care how the object is structured or where it came from as long as it can fulfill the public
interface that is required for this algorithm to work. So, in this case, the usage of the IConfigurationProvider
is to return an object that fulfills a set of needs like what files to copy or not, and the source and destination paths, they can be hard coded or even coming from the other side of the globe, the FileSystemProcessor
only uses that information so it’s not its responsibility to go and fetch those options. In essence, we inverted the dependency of retrieving the ConfigurationOptions
from the algorithm and made it so that the developer has the responsibility of providing what IConfigurationProvider
implementation seems to fit more into their scenario.
The next example is so common that some might not even think of it as an inversion of dependencies:
Enumerable.Range(0, 100).Where(integer => integer % 2 == 0);
This line is very simple, can you see the inversion part? It’s the lambda expression, any time you provide a delegate or a lambda (kinda the same thing) to a method, you are extracting part of the algorithm and handing the responsibility further up stream so that the developer makes a conscious choice about what that condition should be.
We can debate on this principle from the very detailed to the very abstract and it would still hold true, like for example most (if not all, though I avoid generalizing) of the real world machines we build have this built into them as a form of standardization.
A desktop PC needs a CPU and a motherboard to work and a sum of other components, but the only thing those components need to do to work properly is to interface correctly with the slot they are meant for.
On the same note, a car doesn’t have its wheels welded on, so as long as you can replace just the wheels, that’s also a form of Dependency Inversion, in the same category can be a multi headed screwdriver.
Though like all good things, Dependency Inversion must be used in moderation and with good reason, abstracting too much can lead to those “swiss army knives” scenarios, for which yeah, it’s cool to have super modularity, but some just turn ridiculous, like the USB drive on it, doubt you’d find a lot of use in at the camping site or in the wood for it, but you never know .
Dependency Injection
If Dependency Inversion is the principle behind this design, Dependency Injection is the technique with which we accomplish those abstractions.
There are 3 major forms (I say major since there might be other unthought of ways as well) of Dependency Injection, those are via constructors, via method parameters or via public
fields and property setters.
We already saw with the FileSystemProcessor
a form of constructor injection, this way (if no constructor overloads are provided), we make those dependencies mandatory without which the class cannot work properly.
The method parameter injection (yeah, I know that the constructor is a method as well), is mostly used when we want to inject a dependency only for the workings of that particular method, instead of holding on to it for the duration of the object's lifetime. This form is better for keeping methods simple and specialized on what they need to do.
The last form is using public
fields or property setters, this is kind of like saying “using this dependency is optional or I already have a default implementation”, this is a less popular form and requires proper documentation. Personally, instead of using this approach, I would rather create an overload for the method or constructor using it, the reason is that it causes unpredictability, we’re relying on state to change how a component works which means you have to inject the dependency in one spot and then use it in another place which makes for very unstable ground and high maintenance.
Conclusion
We went through the abstract and detailed approached of Dependency Inversion and discussed the common ways of doing Dependency Injection, I hope this might help you recognize these patterns when you encounter them and maybe how to implement them.
Thank you and see you next time.
CodeProject