The Service Locator pattern (or anti-pattern) is considered to be one way to implement the Inversion of Control (IoC) principle. When this pattern is used, a class can find or locate some dependency by asking some entity (called the Service Locator) to provide such dependency. Here is an example:
....
Please note also that using names to locate different implementations of the dependency based on names (as a differentiating parameter) does not solve the issue. How can two instances of OrderProcessor (for example) use two different names to locate different implementations of the IEmailSender service? Should we use Dependency Injection to inject the service name? How can a name be enough to present the many different ways a service can be composed? Wouldn't this violate the IoC principle by having the client know much about its dependency?
public interface IEmailSender
{
void SendEmail(string to, string subject, string body);
}
public class OrderProcessor : IOrderProcessor
{
public OrderProcessor()
{
}
private void InformCustomer()
{
IEmailSender email_sender =
ServiceLocator.LocateService<IEmailSender>();
email_sender.SendEmail(
GetCustomerEmail(),
"Your order status",
"Your order has been
submitted");
}
}
Here we have a service contract called IEmailSender that allows for sending email messages. The OrderProcessor class depends on this service. To obtain such service, it invokes a static class, i.e., the ServiceLocator class, to locate the IEmailSender Service. It uses such service to send some email to the customer.
For this to work, the developer would have registered some implementation of IEmailSender with the Service Locator, probably at application startup.
In his blog, Mark Seemann has published two articles to provide reasons why the Service Locator pattern is actually an anti-pattern.
The first article speaks about the fact that this pattern creates hidden dependencies. In our example, it is not very clear from the constructor of OrderProcessor that it has a dependency on IEmailSender.
The second article speaks about the fact that the Service Locator entity provides its consumers access to more services than they need and thus it violates the Interface Segregation Principle. In our example, the OrderProcessor class can use the ServiceLocator to locate some IOtherService service that it does not need.
In this article, I provide another important reason why the Service Locator is an anti-pattern.
The Service Locator is an anti-pattern because it limits Object Composability.
Object Composability is a design principle that drives us to design classes in a such a way that they can be composed in different ways to satisfy current and new requirements.
Consider the previous example of OrderProcessor, but this time I will be using the Dependency Injection pattern instead of the Service Locator pattern. Here is how it would look like:
public class OrderProcessor : IOrderProcessor
{
private readonly IEmailSender m_EmailSender;
public OrderProcessor(IEmailSender email_sender)
{
m_EmailSender = email_sender;
}
private void InformCustomer()
{
m_EmailSender.SendEmail(
GetCustomerEmail(),
"Your order status",
"Your order has been
submitted");
}
...
}
The OrderProcessor class does not know which implementation of IEmailSender it will be using. However, this can also be achieved using the Service Locator.
What is different in this case, is that each OrderProcessor instance can be depending on a different implementation of IEmailSender or even an object graph that is composed in a totally different way.
Consider the case where we want to encrypt email messages. We can use the Decorator Pattern to create a decorator that encrypts the body of the message before sending it. Here is an example:
public class EmailSenderEncryptionDecorator : IEmailSender
{
private readonly ITextEncryptor m_TextEncryptor;
private readonly IEmailSender m_EmailSender;
public EmailSenderEncryptionDecorator(
IEmailSender email_sender,
ITextEncryptor text_encryptor)
{
m_EmailSender = email_sender;
m_TextEncryptor = text_encryptor;
}
public void SendEmail(string to, string subject, string body)
{
string encrypted_body = m_TextEncryptor.EncryptText(body);
m_EmailSender.SendEmail(to ,
subject, encrypted_body);
}
}
Please ignore the fact the email encryption does not work this way. I am just using this as an example.
This class implements the IEmailSender interface and also has a dependency on the IEmailSender interface. When it is invoked to send a message, it first encrypts the body and then invokes its IEmailSender dependency to actually send the message.
Using the Service Locator, we can register this EmailSenderEncryptionDecorator service with the service locator and the job is done. Right? What's the problem then?
The problem starts to appear when we want two instances of OrderProcessor to depend on two different implementations of IEmailSender. For example, we might want one instance of OrderProcessor to send encrypted messages, and another instance of OrderProcessor to send plain text messages.
Using dependency injection, we can easily do this. Actually, we can compose objects in any way that we want.
Consider the following sample Composition Root that uses Pure DI to construct the object graph:
var email_sender = new SmtpEmailSender("server.test.lab", 25);
var text_encryptor = new TextEncryptor();
var email_sender_encryption_decorator =
new EmailSenderEncryptionDecorator(email_sender, text_encryptor);
var order_processor1 =
new OrderProcessor(email_sender);
new OrderProcessor(email_sender);
var order_processor2 =
new OrderProcessor(email_sender_encryption_decorator);
new OrderProcessor(email_sender_encryption_decorator);
//Continue composing the object graph
Here we have very flexible Object Composability, i.e., we can compose the objects in any way that we like.
What happens when we use the service locator? The OrderProcessor will always locate and use the same service, or a different instance of the same service implementation type (or graph).
How can we use the Service Locator pattern to make one instance of OrderProcessor use EmailSenderEncryptionDecorator as its dependency, and another one to use SmtpEmailSender? I cannot think of any clean solution for this.
Please note that I gave a simple example. In real applications, we have more complex examples of object composition. We might have multiple decorators for a single interface, and for different consumers (who might be of the same type) we would need to inject a dependency that is composed in a totally different manner.
Please note also that using names to locate different implementations of the dependency based on names (as a differentiating parameter) does not solve the issue. How can two instances of OrderProcessor (for example) use two different names to locate different implementations of the IEmailSender service? Should we use Dependency Injection to inject the service name? How can a name be enough to present the many different ways a service can be composed? Wouldn't this violate the IoC principle by having the client know much about its dependency?
Summary:
Object Oriented design principles drive us in the direction of creating classes that have high composability. This gives us the power to reuse classes by composing them differently.
Using the Service Locator to achieve IoC will limit composability.
With the Service Locator, our options of specifying the dependency to use are much more limited than in the case of Dependency Injection.
Completely agree. Service locator on a long run brings much more problems than small initial benefit.
ReplyDelete