Dependency injection is all about connecting abstractions with implementations.
Carefully defining dependencies produces a codebase with more abstractions (interfaces/abstract classes) and just more classes in general.
Ideally the abstractions should be independent of the implementations at an assembly level (no reference between the assembly that contains the abstractions and the implementations). This way it is impossible to use a concrete class instead of an abstraction, even by “accident” (be this a co-worker who is not familiar with the code base or your future self, perhaps during a bad day).
Why is this a problem?
Depending on an interface/abstract class is more flexible than depending on concrete classes. Object oriented languages provide ways in which you can replace those abstractions with concrete implementations at runtime. You want to do this as much as possible, since this is the best way to make your codebase flexible and reusable (see this and this).
When a new object is required, it’s dependencies (abstract classes/interfaces it references) need to be assigned concrete classes. This task can be delegated to an IoC container. When an instance of a particular type is requested to an IoC container, it will “inject” (usually through the class’ constructor) the implementations required by that type (it’s dependencies). And those implementations are defined in a set of mappings that can easily be changed.
But how can you then have these mappings without referencing the assemblies with the implementations?
If, for example you create an ASP.NET MVC project and install StructureMap as your IoC container (I usually use the NuGet package StrucutreMap.MVC5)
Install-Package StrucutreMap.MVC5
You get this Registry
class (Registry classes is where you define the mappings in StructureMap).
public DefaultRegistry() {
Scan(
scan => {
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.With(new ControllerConvention());
});
//For<IExample>().Use<Example>();
}
This registry class is in your main assembly (the ASP.NET MVC web project). For you to use it this way means that your main assembly has to reference the abstractions (IExample
) and the implementations (Example
).
But if you reference implementation and abstractions, even if you just use the abstractions it becomes easy to use a concrete class where an abstraction should be used instead. In big enough projects with many people working on them this is almost a guarantee. It might feel like a shortcut or it might just be because the person doing the code is not familiar with it. It’s as easy as just writing new MyConcreteClass(...)
.
If your assembly does not reference the assembly containing the implementation of the abstract classes/interfaces there’s no chance of this happening.
Setting yourself up for success
If your main assembly does not reference the implementations there is no way of getting this wrong, and it’s very easy to do.
When defining the mappings there’s no way to avoid referencing the abstractions and their implementations, but we can push that out of our main assembly. Sticking with the example of using the ASP.NET MVC as our main assembly, we can it like this (the arrows represent references between assemblies):
Notice that the Abstractions does not depend on anything “concrete” (no implementations), ASP.NET MVC does not depend on anything “concrete” and that Implementations only depends on abstractions (and so does the ASP.NET MVC assembly).
An example will help make this clearer. Imagine you want to create a web page where the user can enter some numbers, click a button and have them sorted. Let’s say that you have this functionality right in your homepage, so if we stick with MVC that usually is your HomeController
.
HomeController
has a dependency on something that can sort an array of numbers, let’s call it ISorter
. Because ISorter
is an abstraction we’ll put it in another assembly (Abstractions) and we’ll make the MVC project reference that assembly.
We need to create an implementation for ISorter
, let’s call it BubbleSort
. This is a concrete class so we’ll put it in an another assembly (Implementations). This assembly references Abstractions (it needs to because it needs to get ISorter
from there).
Finally we need to tie all this together and we do it by creating a Mappings assembly. Mappings references both Abstractions and Implementations. Also, the main assembly (MVC project) reference Mappings.
If we use StructureMap the mappings are specified in a registry class, and it will be as simple as this:
using Abstractions;
using Implementations;
using StructureMap.Configuration.DSL;
namespace Mappings
{
public class MappingsRegistry : Registry
{
public MappingsRegistry()
{
For<ISorter>().Use<BubbleSort>();
}
}
}
You’ll have to configure StructureMap in the MVC project to go looking for this registry. This is how you can do that:
public DefaultRegistry() {
Scan(
scan => {
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.With(new ControllerConvention());
//You have to add these two lines, they will add the mappings from the Mappings assembly
scan.Assembly(typeof(Mappings.MappingsRegistry).Assembly);
scan.LookForRegistries();
});
}
And that’s it. If you follow this structure you can not get it wrong. In the MVC project writing new BubbleSort()
won’t event compile, and better yet, it won’t be possible to do something such as:
public class HomeController : Controller
{
private readonly ISorter _sorter;
public HomeController(ISorter sorter)
{
_sorter = sorter;
}
public ActionResult Index()
{
var bubbleSort = (BubbleSort)_sorter; //compilation error
Because let’s face it, if you have to cast, it means your abstraction is not good enough and you should take care of that, and not use a workaround.
Have a look at a working project that has a page where you can enter numbers and click Sort, and that uses this structure.
git clone https://github.com/ruidfigueiredo/DependencyInjectionDoneRight
Conclusion
When you follow the dependency inversion principle you want to make sure that you do not depend on anything concrete.
You can do that on willpower alone, or you can set your projects up so that this is guaranteed.
By making sure your main project only references the assembly that contains high-level classes and by using an assembly that “ties” those high-level classes to their implementations you can make sure that no high-level class depends on anything concrete.