Mark Siman wrote a
wonderful post "Service Locator breaks encapsulation." The name of the post speaks for itself that it is dedicated to the
Service Locator pattern (anti-pattern). When a programmer randomly calls an IoC container in code to resolve a dependency of an object, he uses the
Service Locator anti-pattern. Mark considers the following example:
public class OrderProcessor : IOrderProcessor { public void Process(Order order) { var validator = Locator.Resolve<IOrderValidator>(); if (validator.Validate(order)) { var shipper = Locator.Resolve<IOrderShipper>(); shipper.Ship(order); } } }
As we can see, the encapsulation of the
OrderProcessor type is broken due to two hidden dependencies that are quietly resolved in the
Process method. These dependencies are hidden from the calling code, which can lead to an exception at runtime if the client has not properly configured the IoC container, determining the necessary dependencies in it. As a solution to the problem, Mark suggests transferring dependency resolution to the object's constructor.
public class OrderProcessor : IOrderProcessor { public OrderProcessor(IOrderValidator validator, IOrderShipper shipper) public void Process(Order order) }
Thus, the calling code will be aware of what the
OrderProcessor object actually requires.
However, in my opinion, there are still scenarios where dependency hiding can be applied. Consider a WPF application where virtually any
ViewModel requires the following dependencies:
IEventAggregator, IProgress, IPromptCreator . To reveal the meaning of the last two interfaces, I would add that the implementation of
IProgress should be able to accept a piece of long-running code and show a window with a progress scale,
IPromptCreator allows
you to open windows asking for confirmation, consent or refusal (modal dialogs). Now imagine that there are
ViewModels that require in addition two (and maybe three) dependencies to create a model. Here’s how the
ViewModel can start to look like with so many dependencies:
public class PaymentViewModel: ICanPay { public PaymentViewModel(IPaymentSystem paymentSystem, IRulesValidator rulesValidator, IEventAggregator aggregator, IProgress progress, IPromptCreator promptCreator) public void PayFor(Order order) }
What a mess! Too much noise in the constructor declaration. Only two dependencies really carry useful information in terms of business logic.
If we, say, use MEF for dependency injection, then we can do the following:
[Export] public class PaymentViewModel : ICanPay { [Import] protected IEventAggregator aggregator; [Import] protected IProgress progress; [Import] protected IPromptCreator promptCreator; public PaymentViewModel(IPaymentSystem paymentSystem, IRulesValidator rulesValidator) { } public void PayFor(Order order) {
We moved the dependencies from the constructor to the field declarations and marked them with the
Import attribute. Despite the fact that we do not call the IoC container directly (although, MEF is not a pure IoC container), we hide dependencies in the same way as in Mark's example. Nothing really changed. Why do I think this code is not so bad? For several main reasons:
- ViewModels are not business entities, they are just pieces of code for pasting Models and Views . Nobody really cares about the above dependencies;
- ViewModels are not public APIs and they are not reused (in most cases);
- Given the two previous points, team members can simply agree that all ViewModels have these utilitarian dependencies, and that's it;
- These utilitarian dependencies are defined as protected, which allows us to create a ViewModel class inheriting the PaymentViewModel in the tests and replace the dependencies with mocks, since we will have access to those fields. Thus, we do not lose the opportunity to cover the tests PaymentViewModel . In Mark's example (where Service Locator is used ), it was necessary to set up an IoC container in a unit test project in order to lock or fix those dependencies, and this practice can become painful for the unit testing process.
Conclusion
As I have already said, there are no only correct answers or statements for programming in all cases. Generally speaking, we should avoid using the
Service Locator , because it breaks encapsulation, as Mark says in his article. Before you use the
Service Locator , assess the potential harm you will cause the system. If you are sure that hidden dependency resolution does not affect client code (which your teammates can write) and there is no potential harm to the system, then go ahead with the song.