For a while now, I’ve been chewing on some thoughts about the service locators versus dependency injection, but struggled to find the right way to say it. I sent an email to a colleague today that tried to describe some of the lessons learned from my last project. It comes close the visceral feelings I have around this subject, but describes it well. Here it is unaltered:
When you pull dependencies in from the Container, you’re using a Service Locator feature of Unity; When you push dependencies in via the constructor, you’re using Dependency Injection. There’s much debate over which pattern to use. When using dependency injection, the biggest advantage is that the relationship between objects is well known. This helps us understand the responsibilities of each object better, may have better performance (if we’re resolving dependencies a lot) and it makes it easier to write tests that isolate the behavior of the subject under test.
In a project where we’re resolving dependencies at runtime, the true nature of the dependencies between objects is obscured -- developers must read through the source to understand responsibilities. Each time a change is made to resolve new dependencies from the container, the tests will fail. Since locating and understanding the responsibility of the dependencies in the source is a complex task, the easy solution is to simply register the missing dependencies in the container and move on. In most cases, the object that is registered is the concrete implementation which is also susceptible to the same dependency problem. Ultimately, this compounds the problem until the tests themselves become obscured, vague, incomplete or overly complex.
In contrast, if the dependencies were registered in the constructor, all dependencies are known at compile time. Any changes made to the subject won’t compile until the tests are updated to reflect the new functionality. Note that because the object doesn’t use the container, the tests become just like any other simple POCO test.
This is not a silver bullet solution however. There are times when resolving from the container makes sense – loaders and savers, for example – but even then, the container can be hidden inside a POCO factory. In contrast to service location, the impact of constructor injection is that it may require more work upfront to realize the dependencies, though this can be mitigated in some cases using a TDD methodology where the tests satisfy the responsibilities of the subject under test as it is written.