If you're a developer that takes his/her profession seriously, you most probably have come across the SOLID principles. As a quick review:
- Single responsibility
- Modules, classes, methods should do one specific thing.
- Open/Close principle
- Classes must be open for extension, but closed for modification.
- Liskov substitution
- Instance of subclasses should be able to correctly function in contexts where the parent class is referred to.
- Interface segregation
- More specific interfaces are preferable to fewer general purpose interfaces.
- Dependency inversion
- Prefer abstractions of classes over concrete implementation of classes.
It is the last one that drives the need for Inversion of Control.
Inversion of Control
The purpose of code is to do things. (Yes, I know, immensely insightful!) To do these things, classes must frequently rely on other classes and coordinate them in some way. The classes that are being relied upon are known as dependencies. I categorise dependencies into two groups: direct dependencies and indirect dependencies.
The image above depicts what is meant with direct dependencies. At design time, the actual objects that will be used during runtime are referred to directly and are well known.
In this image, all that Object A knows about its dependencies is what they can do. Not who they will actually be. At design time only abstractions are referred to, and the actual implementation is unknown.
Inversion of Control is about enabling the use of abstractions, like interfaces, in class implementations over the use of the concrete implementations that will be doing the actual work during runtime. That is, when writing a class, dependencies that are needed to perform work for my class, are only referred to by their interfaces or super classes and never by the concretes that are going to be used at runtime.
The implication of using only abstractions is that when the application is running, you need a way to provide the needed concrete dependencies that must be used. In other words, you need to be able to resolve an abstraction to its runtime object and you need to control the lifetime of the resolved object.
There are two widely known patterns for this: Dependency Injection and Service Locator.
In dependency injection, the goal is to completely remove any and all concerns of how to get a dependency from a consuming object, but making the dependency part of the object's state.
This is done by passing the dependencies to an object when it is being constructed, or by setting properties on the object. Method parameter injection is also used, but less popular.
A container object handles the responsibility for figuring out which instances to construct and provide, based on the required abstraction.
Dependency injection relies on the inherent property of applications to have an object tree that generally starts off with few entry points. At these entry points, almost the entirety of the tree can be walked through by starting off with only a few top level objects. In other words, one can explicitly ask the container to resolve a couple of top level objects, and the container can then walk the tree and figure out which subsequent objects require which dependencies to be resolved.
For dependencies that are determined by some runtime decision, factories are objects that are used to provide the required object. Containers generally also provide for just-in-time instantiation.
With service location a container object is still responsible for resolving abstractions to concretes and for managing their lifetimes. However, objects resolve their dependencies by asking a locator object instance for explicit resolving of a dependency at the time they need it.
In an application that uses the service locator pattern, the objects need to be aware of how they find dependencies, and they have an inherent dependency on the service locator object itself.
Both pattern have strengths and weaknesses, a lot the same, as they share a lot of concepts.
One of the main problems with dependency injection is that it requires careful planning of object lifetimes. Object lifetime policies describe when objects should be created, and how long they should stick around.
Generally there will be at least two types of policies, transient and singleton. There are frequently others, like per thread, or per dependency tree, but the first two are the most often used.
It is very important to understand how the object being resolved is supposed to function. Having a single instance for all resolutions of an object that expects to be a unique instance wherever it's used, can cause some unexpected problems. The same can be said for the other way around.
In service location, this is less of a problem as resolution is done when the dependency is required. It could still occur though if the usage of the service locator is not implemented properly.
Although generally a huge pain in the posterior, this isn't necessarily only a negative.
In terms of negatives, dependency configuration is generally a huge task. It grows over time, as the application is developed and more classes are added.
It can also be quite confusing, as dependencies follow dependencies in a huge chain that need to be followed sometimes to get to the bottom of issues.
Further, you will generally also have a choice between XML configuration and code configuration.
The positive about XML configuration is that you can very easily and quickly modify the behaviour of your application by making a simple configuration change. No build and deployment is required. The bad about it is that it's hard to read, gives no indication of simple mistakes like incorrect spelling of a class name or namespace, and can be bulky, long files.
With code configuration, stupid errors like spelling mistakes are impossible, since the resolution is compiled along with the rest of your application. You can also more cleanly and clearly break up the resolution into methods or classes. However, no quick changes without some kind of deployment is possible, since you need to compile the configuration into a DLL.
Both dependency injection and service location generally work the same way with regards to configuration.
Unit testing is imperative for building quality applications. Possibly one of the most important criteria for effective unit tests is the need to test objects in isolation. Isolation here means as separated from any other code in the codebase as possible, including the object's dependencies.
Dependency injection and service location both allow you to reach this ideal of testing in isolation. You can provide the object under scrutiny with faked or mocked dependencies, allowing you to provide the object with customised, well-known results from its dependencies. So you can verify any output and behavioural expectations.
With dependency injection, this is very easy since you can simply provide the mocked dependencies upon construction of your object, or set dependency properties.
Service location requires you to "hi-jack" the registry in a way so you can provide the required mocked dependencies when they're requested by the object. Any good inversion of control framework will provide a way for you to do this. However, your test setup is a little more involved.
The MVC framework, for example, relies on the abstracted service locator called "DependencyResolver". It has a static method called "SetResolver", which allows you to assign any container or registry that implements the "IDependencyResolver" interface. You can provide a customised implementation for this. It also has an overload for "SetResolver" that takes a delegate, which allows you to more clearly resolve dependencies in the your test classes.
Dependency Injection or Service Location
Service location's biggest drawback is the single huge dependency on the provider. The specialized knowledge of object location that is required in all participating objects pollutes the object model. Having objects that only care about the task they are there to perform, blissfully unaware of where the services they use come from, is a cleaner and ultimately more flexible approach.
The biggest complaint I've heard against dependency injection (other than configuration being a pain) is that constructors become unwieldy, as the required dependencies can be plentiful. I would argue that if you reach a point where you feel your constructor becomes unwieldy because of too many required dependencies, your class design is probably faulty, and you should consider it a code smell. The class is possibly breaking the single responsibility principal, or the responsibility you've defined for it is too broad, and should be refined.
I would definitely recommend dependency injection over service location. However, these two patterns are not mutually exclusive, and can be utilised together. In fact, dependency injection does not work without initial service location for the top level objects in the object tree. I would advise to choose one and use it, simply for preventing any confusion for developers that need to work on the code base at a later stage.