This is a beginner’s tutorial on Service Locator Pattern. While 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 it 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 for 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 is:
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, basically, 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 has an influence on 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 four 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 Service
component is providing. Service
: The Service
component/class is providing services according to Service-Interface description. ServiceLocator
: Is a component/class that encapsulates knowledge of how to obtain services that the Client
needs/depend 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
. Client
depends on ServiceInterface
interface but has no dependency on Service
itself. Service
implements ServiceInterface
interface and offers certain services that the Client
needs. The Client
also has a dependency on the ServiceLocator
. The Client
explicitly requests from 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:
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
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
{
if (locator == null)
{
locator = new ServiceLocator();
}
return locator;
}
}
private ServiceLocator()
{
}
private IServiceA serviceA = null;
private IServiceB serviceB = null;
public IServiceA GetIServiceA()
{
if (serviceA == null)
{
serviceA = new ServiceA();
}
return serviceA;
}
public IServiceB GetIServiceB()
{
if (serviceB == null)
{
serviceB = new ServiceB();
}
return serviceB;
}
}
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 string
s 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.
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
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
{
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;
}
}
static void Main(string[] args)
{
ServiceLocator.Instance.Register("ServiceA", new ServiceA());
ServiceLocator.Instance.Register("ServiceB", new ServiceB());
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:
internal interface IServiceA
{
void UsefulMethod();
}
internal interface IServiceB
{
void UsefulMethod();
}
internal class ServiceA : IServiceA
{
public void UsefulMethod()
{
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
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
{
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;
}
}
static void Main(string[] args)
{
ServiceLocator.Instance.Register<IServiceA>(new ServiceA());
ServiceLocator.Instance.Register<IServiceB>(new ServiceB());
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
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 assembler, and in DIC you have auto-wiring of classes that are independent of 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 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 of 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 IServiceLocator Interface (aka CommonServiceLocator)
In order to decouple application dependency on a particular Service Locator implementation, and in that way, make code/components more reusable, IServiceLocator
interface has been invented. 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 has been 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 being no longer maintained. Anyway, a description of the interface can be found at [14] and it looks like this:
namespace CommonServiceLocator
{
public interface IServiceLocator : IServiceProvider
{
object GetInstance(Type serviceType);
object GetInstance(Type serviceType, string key);
IEnumerable<object> GetAllInstances(Type serviceType);
TService GetInstance<TService>();
TService GetInstance<TService>(string key);
IEnumerable<TService> GetAllInstances<TService>();
}
}
8 Dependency Injection Container (DIC) acting as Service Locator (SL)
It is interesting that Dependency Injection Container (DIC) offers a kind of superset of services of 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 in order to be provided with needed dependencies, then 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 DI Container is made to expose itself through IServiceLocator
interface (application of Adapter Design Pattern).
8.1 Autofac DI Container Acting as 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 Service Locator. What is happening is that Autofac DI Container is used as a back end for 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()
{
Console.WriteLine("ServiceA-UsefulMethod");
}
}
internal class ServiceB : IServiceB
{
public void UsefulMethod()
{
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)
{
Autofac.ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<ServiceA>().As<IServiceA>();
builder.RegisterType<ServiceB>().As<IServiceB>();
Autofac.IContainer container = builder.Build();
CommonServiceLocator.IServiceLocator locator = new AutofacServiceLocator(container);
CommonServiceLocator.ServiceLocator.SetLocatorProvider(() => locator);
CommonServiceLocator.IServiceLocator myServiceLocator = ServiceLocator.Current;
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 necessary 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.
Funny thing is that you can probably add to Autofac 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 due to the fact that you are using DI Container as the back end for 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.
9 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 Service Locator Pattern is considered as 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.
10 References
History
- 13th July, 2022: Initial version