Introduction
This article provides a tutorial on the Service Locator pattern with a sample implementation of a service locator which uses activators to create local or remote services, and discoverers to discover services at run-time.
Background
Fundamentally the locator pattern provides a way of reducing coupling between classes and their dependencies by encapsulating the process involved in obtaining a dependent service.
Let's start with a simple example of a class which uses a logging service implemented by a Logger
class:
class ClientClass
{
private Logger _logger;
public ClientClass()
{
_logger = new Logger();
_logger.Log("Logger was created");
}
}
In the above snippet:
ClientClass
is tightly coupled with a certain implementation of Logger
.ClientClass
understands how to create an instance of Logger
(has a reference to the assembly where Logger
is located).- To test
ClientClass
, you need this specific implementation of Logger
. Of course, you can Mock Logger.
There is nothing wrong with ClientClass
and its tight coupling with Logger
but if you have a need to:
- Have a loose coupling with Logger so that you can change its implementation with little or no change to
ClientClass
. - Remove repetitive logic to locate and create dependent services.
- Be independent of the location of the logging service and the way to instantiate the Logger service.
Then you might want to consider the Service Locator pattern. A good read on the Service locator pattern is on MSDN here. Instead of directly jumping to the service locator pattern , we will walk through potential solutions and see if we can arrive at that pattern ourselves.
A shortcut for COMmers
If you are familiar with COM then you might want to read this section to get a quick understanding of the service locator pattern. If not, you can skip this section.
Remember the good old days when you could just use CoCreateInstance
/Ex
to get a reference to a contract implemented by a class. Here is a simplistic pseudo-code of what you could do
CoCreateInstance "class id", "in process or out of process", "contract/interface ID"
You did not have to care about where the actual executable is located on the system (or which system). COM somehow returned an instance of the class you requested which implemented your required contract. Now the question is what happened internally. I will take the example of an in-process component but the paradigm remains the same for out of process or remote components. Note that this is a simplified version of what actually happens.
When you call CoCreateInstance
:
- COM would look-up the location (DLL) where the class which implements the interface you need (In the registry under HKEY_CLASSES_ROOT\CLSID).
- COM would load the DLL.
- COM would call
DllGetClassObject
to create the factory for the class. - COM would call
CreateInstance
on the factory to get an actual instance of the class you requested. - COM would return the class you requested.
So client classes never had to worry about the location and the activation of their dependent components. This is exactly the intent of the service locator pattern. You could call ServiceLocator::GetComponent(type, contract)
to get an object and somehow the service locator would return an instance of the class you requested. Similar to the COM implementation, the Service locator should use a factory in the background to create the instance of the class.
Moving towards the Service locator pattern
To satisfy some of the needs specified in Background section above, you may refactor your class to look like this:
public class ClientClass
{
private ILogger _logger;
public ClientClass()
{
_logger = LoggerFactory.GetLogger("context");
_logger.Log("Logger was created");
}
}
You may even extend your design to register various kinds of loggers with LoggerFactory
using configuration files, reflection etc. E.g.,
public class ObjectFactory
{
public readonly static ObjectFactory= new ObjectFactory();
public T GetInstance<T>(string context) where T : class
{
if(typeof(T).Equals(typeof(ILogger)))
{
return new Logger() as T;
}
return null;
}
}
ClientClass
will then use ObjectFactory
to get access to various services:
ILogger logger = GeneralObjectCreator.Instance.GetInstance<ILogger>();
ICalc calculator = GeneralObjectCreator.Instance.GetInstance<ICalc>();
This is now very similar to an implementation of a Locator pattern. One question which may come up at this time is what is the difference between a Locator and a Factory. Quoting from MSDN here:
The Service Locator pattern does not describe how to instantiate the services. It describes a way to register services and locate them. Typically, the Service Locator pattern is combined with the Factory pattern and/or the Dependency Injection pattern. This combination allows a service locator to create instances of services.
On the other hand, the Factory pattern is a creational pattern i.e., it is responsible either directly or indirectly to construct related objects.
But there are as many opinions floating out there. You can find implementations of service locators which also are responsible to create instances of services. Our implementation in this article will use the following paradigm:
- Locator will be primarily responsible to provide a central repository of services.
- Locator will use 'activators' to create instances of the actual types. This will decouple the locator from various ways to create instances e.g., create instance from a type in a local assembly or connect to remote service using remoting or WCF.
- Services can be added programmatically or through configuration files to the locator.
- Discoverers can be used to dynamically add discovered services to the locator.
Note that this is an example implementation and is not intended to be used in production environments as is.
Some people may have questions on service locator differs from DI? DI is another pattern which solves the same problem in a different way. The primary difference being that if you use service locator pattern, your classes are explicitly dependent on service locator while DI performs auto-wiring to keep your individual classes independent of service locator (classes are 'given/injected with' dependencies by an 'assembler'). An DI can also act as a locator in some aspects. The locator can be used as a container by DI to find various services to be used during auto-wiring.
Which is better? Service locator or DI? Depends on the context, need and the person answering the question. Both have their advantages and disadvantages and there is enough material on the web to look at to get this information.
Implementation
The diagram below shows a high level view of my implementation of the service locator pattern for the purpose of this tutorial:
Let us look at each player above in detail:
Client
The client uses the service locator to get access to various services.
Here is an simple example of usage of the locator by the client:
IServiceLocator locator = ServiceLocator.instance();
ILogger logger = locator.GetInstance<ILogger>();
logger.Log("this is a log message");
Here is a more extensive example of usage of the locator by the client:
public void TestLocator()
{
LocatorEntry entry = new LocatorEntry();
entry.ContractTypeName = "LocatorExample.Contracts.ILogger, LocatorExample.Contracts";
entry.IsSingleton = false;
entry.LocationName = "ManuallyEnteredLoger";
entry.ActivatorTypeName = typeof(DefaultAssemblyActivator).AssemblyQualifiedName;
var activatorConfig = new DefaultAssemblyActivatorConfig();
activatorConfig.TypeToCreate = "ExampleComponents.Logger, ExampleComponents";
entry.ActivatorConfig = activatorConfig;
ServiceLocator.Instance.RegisterLocatorEntry(entry);
ILogger logger = ServiceLocator.Instance.GetReference<ILogger>("ManuallyEnteredLoger");
logger.Log("Manually added logger called");
logger = ServiceLocator.Instance.GetReference<ILogger>("Logger");
logger.Log("Logger added from config file called");
ICalc calc = ServiceLocator.Instance.GetReference<ICalc>("Calculator");
int sum = calc.Add(10, 20);
logger.Log(" Result of addition using remoting activator is " + sum);
IAuthenticator authenticator = ServiceLocator.Instance.GetReference<IAuthenticator>();
bool authenticated = authenticator.Authenticate("user", "pass");
ServiceLocator.Instance.GetReference<ILogger>("Logger").Log(
"Dynamically discovered Authenticator returned " + authenticated);
IList<ILogger> loggers = ServiceLocator.Instance.GetAllReferences<ILogger>();
foreach (var item in loggers)
{
item.Log("Calling a service which impements ILogger");
}
ILogger logger1 = ServiceLocator.Instance.GetReference<ILogger>("SingletonLogger");
ILogger logger2 = ServiceLocator.Instance.GetReference<ILogger>("SingletonLogger");
if( object.ReferenceEquals(logger1, logger2))
{
logger.Log("Got the same reference for SingletonLogger");
}
logger1 = ServiceLocator.Instance.GetReference<ILogger>("Logger");
logger2 = ServiceLocator.Instance.GetReference<ILogger>("Logger");
if (! object.ReferenceEquals(logger1, logger2))
{
logger.Log("Got different references for non Singletons");
}
}
The configuration file for above example looks like:
<configuration>
<configSections>
<section name="Locator" type="Locator.ServiceLocatorConfig, Locator" />
</configSections>
<Locator>
<LocatorEntries>
<Add LocationName="Logger"
ContractTypeName="LocatorExample.Contracts.ILogger, LocatorExample.Contracts"
ActivatorTypeName="Locator.DefaultAssemblyActivator, Locator"
ActivatorConfigType="Locator.DefaultAssemblyActivatorConfig, Locator">
<ActivatorConfig TypeToCreate="ExampleComponents.Logger, ExampleComponents" />
</Add>
<Add LocationName="SingletonLogger"
ContractTypeName="LocatorExample.Contracts.ILogger, LocatorExample.Contracts"
ActivatorTypeName="Locator.DefaultAssemblyActivator, Locator"
ActivatorConfigType="Locator.DefaultAssemblyActivatorConfig, Locator"
IsSingleton="true">
<ActivatorConfig TypeToCreate="ExampleComponents.AdvancedLogger, ExampleComponents" />
</Add>
<Add LocationName="Calculator" ContractTypeName="LocatorTest.ICalc, LocatorTest"
ActivatorTypeName="Activators.RemotingServiceActivator, Activators"
ActivatorConfigType="Activators.RemotingActivatorConfig, Activators">
<ActivatorConfig Uri="http://localhost:9871/Calc" />
</Add>
</LocatorEntries>
<Discoverers>
<Add DiscovererTypeName="Locator.DefaultAssemblyDiscoverer, Locator"
DiscovererConfigType="Locator.DefaultAssemblyDiscovererConfig, Locator">
<DiscovererConfig SearchPath=".;c:\temp;plugins;..\plugins"
SearchForContract="LocatorExample.Contracts.IAuthenticator, LocatorExample.Contracts" />
</Add>
</Discoverers>
</Locator>
</configuration>
Locator
The locator is responsible to keep a registry of services which can be requested by clients. These services can be accessed by either name or the contract the service implements, e.g., Locator.GetInstance(ILogger.GetType())
or Locator.GetInstance("name")
. In addition other facilities can be provided by the locator like maintaining shared instances of services (singletons) etc.
The locator does not activate the services itself but uses activators to do so. This decouples the locator from various ways to create instances of services e.g., create an instance of a service from a local assembly or connect to a remote service using remoting or WCF.
The information required by the locator and the activators to activate a type are:
- LocatorName: This allows clients to access components by name. Optional.
- ContractType: This allows the clients to access components by type.
- IsSingleton: This specifies if the locator should return a shared instance vs. a new instance.
- ActivatorTypeName: This is the activator which is used by the locator to activate a type
- ActivatorConfigType: This is the activator specific configuration object used by an activator e.g., this may be the fully qualified name of a C# class e.g.,
Utils.Logger
, logger or a Uri of a remote object. The locator provides this information to the Activator. Note that I have used a dynamic configuration section to keep the configuration of the activator with the locator entry.
Discoverer
As demonstrated in above examples, it is possible to register services with locator programmatically or through configuration file. In order to register types not known at deployment time, one can use the discovery paradigm to register types with the locator. The example I have provided searches assemblies in the provided path list. Additional discoverers can be created to retrieve list of services from central repository.
The following diagram shows a sequence diagram of overall workflow (simplistically).
Contents of attached solution
The attached solution consists of the following projects:
- Locator: This is the main project containing the implementation of the Service locator along with
DefaultAssemblyActivator
and DefaultAssemblyDiscoverer
. - Activators: Contains an example of a remoting activator.
- ExampleComponents: Contains simple implementations of services like logger and authenticator.
- LocatorExample.Contracts: Contains the contracts for logger and authenticator (
ILogger
, IAuthenticator
). - Locator.Utilities: Contains some utility classes used by locator.
- LocatorTest: Contains example of usage of the locator in addition to implementation of a sample calculator service.