The Service Locator breaks the encapsulation in statically typed languages, because this pattern does not express preconditions.The horse has long been dead, but some still want to ride it, so I will kick this horse again. For years, I tried to explain why the
Service Locator is an antipattern (for example, it
violates SOLID ), but recently it dawned on me that most of my arguments focused on the
symptoms , missing the fundamental problem.
As an example of how to treat symptoms, in my
original article , I described how the use of IntelliSense is deteriorating due to the use of the Service Locator. In 2010, it didn’t even occur to me that the underlying problem was a violation of
encapsulation .
Consider my original 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); } } }
The example is written in C #, but it will be similar to both Java and any other comparable statically typed language.
')
Preconditions and PostconditionsOne of the main advantages of encapsulation is abstraction: relief from the burden of understanding every implementation detail in each piece of code in the source code. Properly designed encapsulation makes it possible to use a class without knowing the implementation details. This is achieved by establishing a contract of engagement.
As explained in the book
Object-Oriented Software Construction , a contract consists of a set of pre and post-conditions for interaction. If the client satisfies the preconditions, then the object promises to satisfy the postconditions.
In statically typed languages, such as C # or Java, many preconditions can be expressed by the type system itself, as I
demonstrated earlier .
When you look at the public API of the OrderProcessor class, what do you think, what are its preconditions?
public class OrderProcessor : IOrderProcessor { public void Process(Order order) }
As you can see, there are not many preconditions. The only precondition that can be seen from the API is that before calling the Process method you must have an object of type Order.
Yes, if you try to use OrderProcessor, considering only this precondition, then your attempt will fail at run-time.
var op = new OrderProcessor(); op.Process(order);
Here are the real preconditions:
- requires an object of type Order
- An instance of the IOrderValidator service is required in a global locator directory.
- An instance of the IOrderShipper service is required in a global locator directory.
Two of the three preconditions are invisible in compile-time.
As you can see, the Service Locator breaks encapsulation, because this pattern hides preconditions for correct use of the object.
Passing arguments
Several people jokingly defined Dependency Injection as an advertised term instead of
“passing arguments” , and perhaps there is a bit of truth in this.
The easiest way to make the preconditions obvious would be to use a type system to express requirements. In the end, we already understood that we need an object of type Order. This was obvious because Order is the argument type of the Process method.
Can we make the need for IOrderValidator and IOrderShipper as obvious as the need for an object of type Order using the same technique? Maybe the following code is a solution?
public void Process( Order order, IOrderValidator validator, IOrderShipper shipper)
In some circumstances, this is all that may need to be done - now all three preconditions are equally obvious.
Unfortunately, often, such a solution is impossible. In this case, OrderProcessor implements the IOrderProcessor interface.
public interface IOrderProcessor { void Process(Order order); }
Since the signature of the Process method is already defined, you cannot add arguments to it. You can still make the preconditions visible through the type system, requiring the client to pass the required objects through arguments, you just need to pass them through some other member of the class.
Constructor is the safest way:
public class OrderProcessor : IOrderProcessor { private readonly IOrderValidator validator; private readonly IOrderShipper shipper; public OrderProcessor(IOrderValidator validator, IOrderShipper shipper) { if (validator == null) throw new ArgumentNullException("validator"); if (shipper == null) throw new ArgumentNullException("shipper"); this.validator = validator; this.shipper = shipper; } public void Process(Order order) { if (this.validator.Validate(order)) this.shipper.Ship(order); } }
With this design, the public API looks like this:
public class OrderProcessor : IOrderProcessor { public OrderProcessor(IOrderValidator validator, IOrderShipper shipper) public void Process(Order order) }
Now it’s obvious that all three objects are needed to call the Process method. The latest version of the OrderProcessor class promotes its preconditions through a type system. You cannot even compile the client code until you pass arguments to the constructor and method (you can pass null here, but this is another story).
Conclusion
Service Locator - antipattern in statically typed, object-oriented languages, because it breaks encapsulation. The reason is that this anti-pattern hides preconditions for the correct use of the object.
If you need an affordable introduction to encapsulation, you can watch my
Encapsulation and SOLID course on Pluralsight.com. If you want to learn more about Dependency Injection, you can read my book (
award-winning )
Dependency Injection in .NET.