I think that the easiest way to understand the difference between the two and why a DI container is so much better than a service locator is to think about why we do dependency inversion in the first place.
We do dependency inversion so that each class explicitly states exactly what it is dependent on for operation. We do so because this creates the loosest coupling we can achieve. The looser the coupling, the easier something is to test and refactor (and generally requires the least refactoring in the future because the code is cleaner).
Let's look at the following class:
public class MySpecialStringWriter
{
private readonly IOutputProvider outputProvider;
public MySpecialFormatter(IOutputProvider outputProvider)
{
this.outputProvider = outputProvider;
}
public void OutputString(string source)
{
this.outputProvider.Output("This is the string that was passed: " + source);
}
}
In this class, we are explicitly stating that we need an IOutputProvider and nothing else to make this class work. This is fully testable and has a dependency upon a single interface. I can move this class to anywhere in my application, including a different project and all it needs is access to the IOutputProvider interface. If other developers want to add something new to this class, which requires a second dependency, they have to be explicit about what it is they need in the constructor.
Take a look at the same class with a service locator:
public class MySpecialStringWriter
{
private readonly ServiceLocator serviceLocator;
public MySpecialFormatter(ServiceLocator serviceLocator)
{
this.serviceLocator = serviceLocator;
}
public void OutputString(string source)
{
this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
}
}
Now I've added the service locator as the dependency. Here are the problems that are immediately obvious:
- The very first problem with this is that it takes more code to achieve the same outcome. More code is bad. It's not much more code but it is still more.
- The second problem is that my dependency is no longer explicit. I still need to inject something into the class. Except now the thing that I want is not explicit. It's hidden in a property of the thing that I requested. Now I need access to both the ServiceLocator and the IOutputProvider if I want to move the class to a different assembly.
- The third problem is that an additional dependency can be taken by another developer who doesn't even realise they're taking it when they add code to the class.
- Finally, this code is harder to test (even if ServiceLocator is an interface) because we have to mock ServiceLocator and IOutputProvider instead of just IOutputProvider
So why don't we make the service locator a static class? Let's take a look:
public class MySpecialStringWriter
{
public void OutputString(string source)
{
ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
}
}
This is much simpler, right?
Wrong.
Let's say that IOutputProvider is implemented by a very long running web service that writes the string in fifteen different databases around the world and takes a very long time to complete.
Let's try to test this class. We need a different implementation of IOutputProvider for the test. How do we write the test?
Well to do that we need to do some fancy configuration in the static ServiceLocator class to use a different implementation of IOutputProvider when it is being called by the test. Even writing that sentence was painful. Implementing it would be torturous and it would be a maintenance nightmare. We should never need to modify a class specifically for testing, especially if that class is not the class we are actually trying to test.
So now you're left with either a) a test that is causing obtrusive code changes in the unrelated ServiceLocator class; or b) no test at all. And you're left with a less flexible solution as well.
So the service locator class has to be injected into the constructor. Which means that we're left with the specific problems mentioned earlier. The service locator requires more code, tells other developers that it needs things that it doesn't, encourages other developers write worse code and gives us less flexibility moving forwards.
Put simply service locators increase coupling in an application and encourage other developers to write highly coupled code.