Abstract
This is a beginner’s tutorial on the Service Locator Pattern. While the Service Locator Pattern is presently very unpopular and pushed aside by the Dependency Injection Container usage, it is still interesting to see what it was offering and why is now considered inadequate.
1. Short tutorial
The goal of this article is to provide a concise tutorial on the “Service Locator Design Pattern” with examples in C#. While this design pattern has been pushed aside by the “Dependency Injection Pattern” and usage of “Dependency Injection Container”, it can still be of interest to readers for both academic reasons and practical reasons since legacy code might still rely on this pattern. Today, the usage of the Service Locator pattern in the new code is discouraged and is even by some authors considered an Anti-Pattern.
The presented code is tutorial level, “demo-of-concept” and for brevity does not handle exceptions, etc.
2. Dependency inversion principle - DIP
So, the “Dependency inversion principle (DIP)” is a SOFTWARE DESIGN PRINCIPLE. It is called “principle” because it provides high-level advice on how to design software products.
DIP is one of five design principles known under the acronym SOLID [3] promoted by Robert C. Martin [5]. The DIP principle states:
- High-level modules should not depend on low-level modules. Both should depend on the abstraction.
- Abstractions should not depend on details. Details should depend on abstractions.
Interpretation
While high-level principle talks about “abstraction”, we need to translate that into terms in our specific programming environment, in this case, C#/.NET. Abstractions in C# are realized by interfaces and abstract classes. When talking about “details”, the principle means “concrete implementations”.
So, that means that DIP promotes the usage of the interfaces in C#, and concrete implementations (low-level modules) should depend on interfaces.
Traditional module dependencies look like this.
DIP proposes this new design.
As you can see, some dependencies (arrows) have inverted directions, so that is where the name “inversion” comes from.
The goal of DIP is to create “loosely coupled” software modules. Traditionally high-level modules depend on low-level modules. DIP has a goal to make high-level modules independent of low-level modules’ implementation details. It does that by introducing an “abstract layer” (in the form of an interface) between them.
The DIP principle is a broad concept and influences other design patterns. For example, when applied to the Factory design pattern or Singleton design pattern, it suggests that those patterns should return a reference to an interface, not a reference to an object. The “Dependency Injection Pattern” follows this principle. The “Service Locator Pattern” follows this principle also.
3. Service locator pattern-static version
First of all, the “Service Locator Pattern” is a SOFTWARE DESIGN PATTERN. It is called a “pattern’ because it suggests low-level specific implementation of a specific problem.
The main problem this pattern aims to solve is how to create “loosely coupled” components. The aim is to improve the modularity of the application by removing dependency between the client and the service implementation. The pattern uses a central registry known as a “service locator” which on request of the client, provides it with the services it depends upon.
There are 4 main roles (classes) in this pattern
- Client: The client is a component/class that wants to use services provided by another component called Service.
- ServiceInterface: The service interface is an abstraction describing what kind of services the Service component is providing.
- Service: The Service component/class is providing services according to the Service-Interface description.
- ServiceLocator: This is a component/class that encapsulates knowledge of how to obtain services that the Client needs/depends upon. It is a single point of contact for the Client to get services. It is a singleton registry for all services that are used by the client. The ServiceLocator is responsible for returning instances of services when they are requested by the Client.
The way it works is Client is dependent on ServiceInterface. The client depends on the ServiceInterface interface but has no dependency on the Service itself. Service implements the ServiceInterface interface and offers certain services that the Client needs. The Client also has a dependency on the ServiceLocator. The Client explicitly requests from the ServiceLocator instance of Service it is dependent upon. Once the Client gets the instance of Service it needs, it can perform work.
Here is a class diagram of this pattern.
Here is a sample code of this pattern.
using System;
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
//some useful work
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
//some useful work
Console.WriteLine("ServiceB-UsefulMethod");
}
}
internal class Client
{
public IServiceA serviceA = null;
public IServiceB serviceB = null;
public void DoWork()
{
serviceA?.UsefulMethod();
serviceB?.UsefulMethod();
}
}
internal class ServiceLocator
{
private static ServiceLocator locator = null;
public static ServiceLocator Instance
{
get
{
// ServiceLocator itself is a Singleton
if (locator == null)
{
locator = new ServiceLocator();
}
return locator;
}
}
private ServiceLocator()
{
}
private IServiceA serviceA = null;
private IServiceB serviceB = null;
public IServiceA GetIServiceA()
{
//we will make ServiceA a singleton
//for this example, but does not need
//to be in a general case
if (serviceA == null)
{
serviceA = new ServiceA();
}
return serviceA;
}
public IServiceB GetIServiceB()
{
//we will make ServiceB a singleton
//for this example, but does not need
//to be in a general case
if (serviceB == null)
{
serviceB = new ServiceB();
}
return serviceB;
}
}
class Program
{
static void Main(string[] args)
{
Client client = new Client();
client.serviceA = ServiceLocator.Instance.GetIServiceA();
client.serviceB = ServiceLocator.Instance.GetIServiceB();
client.DoWork();
Console.ReadLine();
}
}
This version of the pattern is called the “static version” because it uses a field for each service to store an object reference and has a dedicated “Get” method name for each type of service it provides. It is not possible to dynamically add a different type of service to the ServiceLocator. Everything is statically hardcoded.
4. Service locator pattern-Dynamic version-String service names
Here we will show a dynamic version of this pattern, a version with strings used as Service names. The principles are the same as above, just the implementation of ServiceLocator is different.
Here is a class diagram
Here is a sample code of this pattern.
using System;
using System.Collections.Generic;
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
// some useful work
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
// some useful work
Console.WriteLine("ServiceB-UsefulMethod");
}
}
internal class Client
{
public IServiceA serviceA = null;
public IServiceB serviceB = null;
public void DoWork()
{
serviceA?.UsefulMethod();
serviceB?.UsefulMethod();
}
}
internal class ServiceLocator
{
private static ServiceLocator locator = null;
public static ServiceLocator Instance
{
get
{
// ServiceLocator itself is a Singleton
if (locator == null)
{
locator = new ServiceLocator();
}
return locator;
}
}
private ServiceLocator()
{
}
private Dictionary<string, object> registry =
new Dictionary<string, object>();
public void Register(string serviceName, object serviceInstance)
{
registry[serviceName] = serviceInstance;
}
public object GetService(string serviceName)
{
object serviceInstance = registry[serviceName];
return serviceInstance;
}
}
class Program
{
static void Main(string[] args)
{
// register services with ServiceLocator
ServiceLocator.Instance.Register("ServiceA", new ServiceA());
ServiceLocator.Instance.Register("ServiceB", new ServiceB());
// create client and get services
Client client = new Client();
client.serviceA = (IServiceA)ServiceLocator.Instance.GetService("ServiceA");
client.serviceB = (IServiceB)ServiceLocator.Instance.GetService("ServiceB");
client.DoWork();
Console.ReadLine();
}
}
Please note that in this version we have an “initialization phase” in which we register services with ServiceLocator. That can be done from code dynamically or from a configuration file.
5. Service locator pattern - Dynamic version - Generics
Here we will show another dynamic version of this pattern, a version based on generics methods. The principles are the same as above, just the implementation of ServiceLocator is different. This version is very popular in literature [6].
Here is a class diagram
Here is a sample code of this pattern.
using System;
using System.Collections.Generic;
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
// some useful work
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
// some useful work
Console.WriteLine("ServiceB-UsefulMethod");
}
}
internal class Client
{
public IServiceA serviceA = null;
public IServiceB serviceB = null;
public void DoWork()
{
serviceA?.UsefulMethod();
serviceB?.UsefulMethod();
}
}
internal class ServiceLocator
{
private static ServiceLocator locator = null;
public static ServiceLocator Instance
{
get
{
// ServiceLocator itself is a Singleton
if (locator == null)
{
locator = new ServiceLocator();
}
return locator;
}
}
private ServiceLocator()
{
}
private Dictionary<Type, object> registry =
new Dictionary<Type, object>();
public void Register<T>(T serviceInstance)
{
registry[typeof(T)] = serviceInstance;
}
public T GetService<T>()
{
T serviceInstance = (T)registry[typeof(T)];
return serviceInstance;
}
}
class Program
{
static void Main(string[] args)
{
// register services with ServiceLocator
ServiceLocator.Instance.Register<IServiceA>(new ServiceA());
ServiceLocator.Instance.Register<IServiceB>(new ServiceB());
// create client and get services
Client client = new Client();
client.serviceA = ServiceLocator.Instance.GetService<IServiceA>();
client.serviceB = ServiceLocator.Instance.GetService<IServiceB>();
client.DoWork();
Console.ReadLine();
}
}
Please note that again in this version we have an “initialization phase” in which we register services with ServiceLocator. That can be done from code dynamically or from a configuration file.
6. Pros and Cons of service locator pattern
6.1 Advantages
The advantages of this pattern are
- It supports the run-time binding and adding components at runtime
- At runtime components can be replaced, for example, ServiceA with ServiceA2, without restarting the application
- Enables parallel code development since modules have a clear boundary, that is interfaces.
- It enables replacing modules at will, due to the DIP principle and separation of modules by interfaces
- Testability is good since you can replace Services with MockServices when registering with ServiceLocator
6.2 Disadvantages
Disadvantages of this pattern are
- The Client has an additional dependency on ServiceLocator. It is not possible to reuse Client code without the ServiceLocator
- The ServiceLocator obscures the Client dependencies, causing run-time errors instead of compile-time errors when dependencies are missing
- All components need to have a reference to the service locator, which is a singleton.
- Implementing the service locator as a singleton can also create scalability problems in highly concurrent environments.
- A service locator makes it easier to introduce breaking changes in interface implementations.
- Testability problems might arise since all tests need to use the same global ServiceLocator (singleton)
- During unit testing, you need to mock both the ServiceLocator and the services it locates
6.3 Some consider it to be an Anti-Pattern
First a small digression. Let us look at this definition from [8] “The anti-pattern is a commonly-used structure or pattern that, despite initially appearing to be an appropriate and effective response to a problem, has more bad consequences than good ones”.
So, some [6] consider Service Locator Pattern, although solves some issues, to have so many bad consequences that they refer to it as an “Anti-Pattern”. In other words, they suggest that the introduced disadvantages outweigh the benefits. The main objections are that it obscures dependencies and makes modules harder to test.
6.4 Service locator pattern (SLP) vs dependency injection container (DIC)
Literature is generally favoring the usage of DIC over SLP whenever possible. Here are frequent comparisons of the two approaches.
- Both patterns have a goal to decouple a Client from the Services it is dependent on using abstraction layer – interfaces. The main difference is that in SLP classes are dependent on ServiceLocator, which acts as an assembler, and in DIC you have auto-wiring of classes that are independent of the assembler, that is in this case DI Container. Also, in SLP client class asks explicitly for a service, while in DIC there is no explicit request.
- Both SLP and DIC introduce the problem that they are prone to run-time errors when dependencies are missing. That is simply the result of the fact that they both try to decouple applications into modules dependent on the “abstraction layer”, that is interfaces. So, when dependencies are missing, that is discovered during run-time in the form of run-time errors.
- Using DIC it is easier to see what are component dependencies, just by looking at the injection mechanism. With SLP you need to search the source code for calls to the service locator. ([11])
- Some influential authors ([11]) argue that in some scenarios when hiding dependencies is not such an issue, they don’t see DIC providing anything more than SLP.
- SLP has a big problem in that all components need to reference the ServiceLocator, which is a singleton. The Singleton pattern for ServiceLocator can be a scalability problem in highly concurrent applications. Those problems can be avoided by using DIC instead of SLP. Both patterns have the same goal.
- It is widely believed that the usage of DIC offers more testability than the usage of SLP. Some authors ([11]) do not share that opinion and believe that both approaches make it easy to replace real service implementations with mock implementations.
7. IService locator interface (aka common service locator)
To decouple application dependency on a particular Service Locator implementation, and in that way make code/components more reusable, the IServiceLocator interface has been invented. The IServiceLocator interface is an abstraction of a Service Locator. In that way, pluggable architecture has been created, so the application does not depend anymore on any particular implementation of the Service Locator, and any concrete Service Locator module that implements that interface can be plugged into the application.
IServiceLocator interface was originally located in Microsoft.Practices.ServiceLocation namespace [12], but that module has been deprecated. It seems that the successor is now namespace CommonServiceLocator at [13], but that project is also no longer maintained. Anyway, a description of the interface can be found at [14] and it looks like this:
namespace CommonServiceLocator
{
// The generic Service Locator interface. This interface is used
// to retrieve services (instances identified by type and optional
// name) from a container.
public interface IServiceLocator : IServiceProvider
{
// Get an instance of the given serviceType
object GetInstance(Type serviceType);
// Get an instance of the given named serviceType
object GetInstance(Type serviceType, string key);
// Get all instances of the given serviceType currently
// registered in the container.
IEnumerable<object> GetAllInstances(Type serviceType);
// Get an instance of the given TService
TService GetInstance<TService>();
// Get an instance of the given named TService
TService GetInstance<TService>(string key);
// Get all instances of the given TService currently
// registered in the container.
IEnumerable<TService> GetAllInstances<TService>();
}
}
8. Dependency injection container (DIC) acting as service locator (SL)
It is interesting that a Dependency Injection Container (DIC) offers a kind of superset of services of a Service Locator (SL) and can act like one if needed, intentionally, or if not used correctly.
If you are querying for dependencies, even if it is a DI Container, it becomes a Service Locator Pattern ([6]). When an application (as opposed to a framework [7]) actively queries a DI Container to be provided with needed dependencies, then the DI Container in reality functions as a Service Locator. So, if you do not use DI Container properly, as a framework, but you create explicit dependencies on the DI Container, you finish in practice with Service Locator Pattern. It is not just about the brand of the container you have, it is about how you use it.
One interesting intentional abuse of the above fact is when the DI Container is made to expose itself through the IServiceLocator interface (application of Adapter Design Pattern).
8.1 Autofac DI container acting as a service locator
We will show that in the example of Autofac [15] DI Container. It offers Autofac.Extras.CommonServiceLocator adapter ([16], [17]) that makes it appear as IServiceLocator. In this case Autofac.Extras.CommonServiceLocator acts as an Adapter Design Pattern and exposes Autofac DI Container as a Service Locator. What is happening is that Autofac DI Container is used as a back end for the Service Locator pattern.
Here is the class diagram
And here is the code example.
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
//some usefull work
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
//some usefull work
Console.WriteLine("ServiceB-UsefulMethod");
}
}
internal class Client
{
public IServiceA serviceA = null;
public IServiceB serviceB = null;
public void DoWork()
{
serviceA?.UsefulMethod();
serviceB?.UsefulMethod();
}
}
static void Main(string[] args)
{
// Register services with Autofac
Autofac.ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ServiceA>().As<IServiceA>();
builder.RegisterType<ServiceB>().As<IServiceB>();
Autofac.IContainer container = builder.Build();
// create Service Locator
CommonServiceLocator.IServiceLocator locator = new AutofacServiceLocator(container);
CommonServiceLocator.ServiceLocator.SetLocatorProvider(() => locator);
CommonServiceLocator.IServiceLocator myServiceLocator = ServiceLocator.Current;
//create client and get services
Client client = new Client();
client.serviceA = myServiceLocator.GetInstance<IServiceA>();
client.serviceB = myServiceLocator.GetInstance<IServiceB>();
client.DoWork();
Console.ReadLine();
}
In the above example, it is not necessarily the usage of the interface IServiceLocator that makes this a Service Locator pattern, but the fact that we are explicitly requesting resolution of the dependencies and implicitly creating dependency on IServiceLocator.
The funny thing is that you can probably add to Autofac's hierarchical chain of dependencies and Autofac will resolve them through Dependency Injection. So, you get a kind of hybrid solution, DI resolution into the depth of dependencies and explicit resolution of top-level dependencies. But that is more of an anomaly because you are using DI Container as the back end for the Service Locator, than a valid pattern. Service Locator resolution typically goes one level into depth, while DI Container resolution goes recursively into any level of depth.
Conclusion
While Service Locator Pattern is generally brushed aside by Dependency Injection Pattern/Container [7] and is considered today as an Anti-Pattern, it is still interesting to look at how it works. I think it is educative to study even patterns that didn’t make it and to learn what made them fail.
It is important to know why the Service Locator Pattern is considered a much inferior solution to Dependency Injection Container, not to just rely on the fact that “it is no longer popular on the internet”.
It is interesting to see the design pattern that the Software Engineering Architecture thinking once considered perspective is now being called an Anti-Pattern and is being almost despised in different forums on the Internet.
Some authors [6] openly admit that they went from proponents and implementation library developers for the Service Locator Pattern to opponents and critics of the patterns.
In the fashion world, many fashion pieces and trends end up repeating themselves. We will see if a similar thing will happen to the world of Software Design Patterns.
References
[1] https://en.wikipedia.org/wiki/Service_locator_pattern
[2] https://stackify.com/service-locator-pattern/
[3] https://martinfowler.com/articles/injection.html#UsingAServiceLocator
[4] https://www.tutorialspoint.com/design_pattern/service_locator_pattern.htm
[5] https://www.geeksforgeeks.org/service-locator-pattern/
[6] Mark Seemann, Steven van Deursen - Dependency Injection Principles, Practices, and Patterns, Manning Publications, 2019
[7] https://www.codeproject.com/Articles/5333947/Dependency-Injection-Pattern-in-Csharp-Short-Tutor
[8] https://en.wikipedia.org/wiki/Anti-pattern
[9] https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
[10] https://www.codeproject.com/Articles/597787/A-tutorial-on-Service-locator-pattern-with-impleme
[11] https://martinfowler.com/articles/injection.html#ServiceLocatorVsDependencyInjection
[12] https://docs.microsoft.com/en-us/previous-versions/msp-n-p/hh860410(v=pandp.51)
[13] https://www.nuget.org/packages/CommonServiceLocator/
[14] https://github.com/unitycontainer/commonservicelocator/blob/master/src/IServiceLocator.cs
[15] https://autofac.org/
[16] https://autofac.org/apidoc/html/CE503C57.htm
[17] https://www.nuget.org/packages/Autofac.Extras.CommonServiceLocator/