Following on from my previous post about dependency injection, the topic of IoC containers feels like a nice continuation. I wanted to keep the two posts separate, because it's important to understand that you do not need an IoC Container just because you want to inject your dependencies. Dependency injection itself is a very simple concept - all you're doing is passing an instance of the dependency into your code as an abstraction (ie. via an interface or abstract class the dependency implements). You do not need an IoC container to do this. An IoC Container is just a library to help you manage doing so.
Whilst you may not need one, they are however extremely useful for managing your dependencies - especially when you have a lot of them - which most SOLID codebases tend to have.
First of all, let's just get past the name, which I for one think it makes it sound more complicated than it actually is. I do wonder how many people have been put off due to this poor (IMO) naming.
First of all, forget the term inversion
. It's confusing, and slightly misleading. All inversion
means is that you're changing what has control over which concrete implementation of a dependency is used. From being internal to the code using the dependency, to being external.
Below is an example of a class that is directly coupled to a dependency...
public class MyClass
{
public void MyMethod()
{
var logger = new FileSystemLogger("LogFile.txt");
logger.WriteLog("Hello!");
}
}
In this example, the dependency is FileSystemLogger
. Notice that it's impossible to call MyMethod()
without using the FileSystemLogger
implementation. This means you can never run this method without it hitting the file system!
Now if this was rewritten so that rather than depending on the FileSystemLogger
, it instead depends on an abstraction...
public class MyClass
{
public void MyMethod(ILogger logger)
{
logger.WriteLog("Hello!");
}
}
Now, any calling code can pass in any concrete implementation that implements ILogger. Whether this be the file system logger implementation, a SQL Server implementation, MongoDB, remote REST API logger, whatever. Even a mocked object from your unit or integration tests.
Note that the example above uses Dependency Injection. Whilst, Dependency Injection is a type of Dependency Inversion, and perhaps the most common - it's certainly not the only way of doing Dependency Inversion. Dependency Inversion does not directly mean Dependency Injection. Another way of inverting your dependencies might be to use the Service Locator Pattern.
This is all the inversion of control
(or dependency inversion
means). You're just inverting
who has control over the dependencies. The commonly used dependency inversion principal 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 on abstractions.
For further reading - a very good explanation of dependency inversion can be found in the following blog post ...
So now we understand Dependency Inversion, let's look at what IoC Containers can do for us!
The basic idea is that on startup of your application, you can define mappings between your abstractions and your implementations - eg. between your interfaces and concrete types. Then the IoC Container library will handle creating your objects for you and will automatically inject any dependencies.
For example, following on from the example above, you might bind ILogger to the concrete implementation FileSystemLogger
. Or perhaps a completely different implementation - eg. AzureBlobStorageLogger
. Then all the rest of your code only needs to deal with the ILogger type, and doesn't care about which actual implementation is used.
Because the bindings are configured at the application startup, it's all done in one place and you can easily see what implementations are bound, and you can also easily change the implementations in just one place.
Different referencing projects can specify different implementations by just changing the bindings. Eg. your automated tests can use different implementations than the main production code would.
IoC containers actually do much more than this, and really become useful when you allow the container to manage your entire dependency chain. By this, I mean that for every instance of a class that the IoC container creates for you, it'll also try and resolve any dependencies used by that class, and so on. If all your instances are resolved by the IoC container, then if you need access to a dependency, then you can just add it as a constructor parameter, and you'll be given it automagically.
At this stage, I think we could do with some examples to add a bit of context. My "go-to" IoC Container is Autofac, so my examples will use their syntax. However, the concepts are very similar for most IoC containers.
Let's start with an example of binding interfaces. This would normally go in your application startup / bootstrap code ...
var builder = new ContainerBuilder();
builder.RegisterType<FileSystemLogger>().As<ILogger>();
var container = builder.Build();
This example is just registering a single type - however, you'd normally have a long list of bindings here.
That's it, now you can use them. Obviously the container needs to know which of those bindings to instantiate, and when. How this works depends on the type of application you're creating.
If you're dealing with an ASP.NET MVC application, then you just need to register your IoC Container with ASP.NET, and it'll do the rest for you (different IoC Containers do this in different ways, so consult their documentation for details). You can then just request the interfaces in your Controllers by adding constructor parameters ...
public class MyController : Controller
{
private readonly ILogger _logger;
public MyController(ILogger logger)
{
_logger = logger;
}
}
And you can literally, "ask" for any dependencies you need just by adding new constructor parameters.
If you're not working with MVC, but say just a normal console application, then you'll have to explicitly resolve at least one instance manually...
var logger = container.Resolve<ILogger>();
However, any dependencies that ILogger then has, will automatically then be resolved by the IoC container, and so-on with their dependencies, right down the chain. So ideally, you'll only be manually resolving the very top level dependencies, and the container library would do the rest.
This concept of just adding a constructor parameter to your code when you need an existing dependency becomes more and more useful the larger your application gets. Especially when the dependencies are quite small. It almost feels very "plug and play".
For developers who aren't used to this way of doing this, this is quite a change of mindset. Once it clicks though, it is actually very simple, and makes a codebase much nicer to work with and maintain, and much easier to write unit tests against.
Only last week, I had a situtaion where I had to change the way a system was working so that rather than grabbing data from a Dropbox account (via their API), it instead got that data from an Azure SQL Database. Because I had used an IoC Container library (Autofac) and the Respository Pattern (a blog post on that coming shortly!) - all I did was create a new implementation of my repository interface, then switch it over in my IoC bindings.