Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A Beginner's Tutorial For Understanding and Implementing Service Locator Pattern in C#

4.77/5 (7 votes)
19 Jan 2016CPOL8 min read 15.1K   96  
In this article we will try to understand the service locator pattern.

Introduction

In this article we will try to understand the service locator pattern. We will also implement a contrived implementation to demonstrate the service locator pattern.

Background

Whenever we have a scenario where one class is providing some functionality and another class want to use this functionality, the simplest way to achieve this would be to instantiate the class providing the service in the client class and use it. For instance class A that want to call a method of class B, we can simply have an object of B inside A and call its methods whenever we need to. The code will look something like following.

C#
public class B
{
    public void DoTaskOne()
    {
        Console.WriteLine("B.DoSomething");
    }
}

public class A
{
    private B b;

    public A()
    {
        b = new B();
    }

    public void GetOneDone()
    {
        b.DoTaskOne();           
    }
}

This approach of having the class instances contained inside other classes will work but it has some downsides. The first problem is that each class needs to know about every other class that it wants to use. This will make this application a maintenance nightmare. Also, the above approach will increase the coupling between the classes.

From the best practices' perspective whenever we are designing our classes we should keep the dependency inversion principle in mind. Dependency Inversion Principle says that the higher level modules should always depend on abstractions rather than lower level modules directly. So we should always design our classes in such a way that they always depend on the interfaces or abstract classes rather than other concrete classes.

So the classes we saw in the above example will change. We first need to have an interface that A can use to call DoTaskOne. Class B should implement this interface. The new classes will look like following.

C#
interface IDoable
{
    void DoTaskOne();
}

public class B : IDoable
{
    public void DoTaskOne()
    {
        Console.WriteLine("B.DoSomething");
    }
}

public class A
{
    private IDoable doable;

    public A()
    {
        // How to create the doable object here???
        // doable = new B();
        // This seems wrong
    }

    public void GetOneDone()
    {
        doable.DoTaskOne();
    }
}

The above code shows the classes perfectly designed where the higher lever modules depend on abstractions and the lower level modules implementing these abstractions. But wait... How are we going to create and object of B. Should we still do that as we did in the previous code i.e. doing a new on B in the A's constructor? But would it not defeat the whole purpose of having loose coupling?

The first answer to this question would be implement a factory pattern. Since the Factory pattern totally abstract our the responsibility of creating classes from the client classes, we could have a factory class that could create the concrete instances of type IDoable and the class A can use this factory to get the concrete implementation of IDoable which is class B in this case. So with factory implementation in place our code will look something like following.

C#
public class DoableFactory
{
    public B GetConcreteDoable()
    {
        return new B();
    }
}

// Constructor of A
public A()
{
    DoableFactory factory = new DoableFactory();
    doable = factory.GetConcreteDoable();
}

Now we will not discuss the details for Factory pattern in this article but I strongly recommend that you get yourself acquainted with this pattern before proceeding further. You can find more about factory pattern here: Understanding and Implementing Factory Pattern in C#[^]

Why are we talking about Factory Pattern?

So one might wonder why are we talking about factory pattern when our goal was to talk about service locator. Well the answer to this question lies in the fact that from the calling code's perspective both patterns are same. With the use of factory we have solved the following problems.

  • We have inverted the control between client and the service class.
  • We have client code depending on abstraction rather than actual service implementation i.e. loose coupling.
  • We have a way to hook up the concrete implementations with the interface using the factory class.

But there are few more issues one should look at before deciding to go with the factory pattern. Since the factory class is returning the new instance of the requested object, there are few things we need to ask.

  • What about the cost of construction. If this class that we are creating inside factory is very expensive to create, is it a good idea to let the client use the factory and create as many instances as they need.
  • What should be done when we already have an instance ready for the client to be used i.e. we don't want to instantiate a new class but rather return an existing instance of the object.
  • What about the ownership? Since the factory class return a new instance to the caller client, the calling code/class owns the instance. What if the ownership lies with someone else(this point is somewhat redundant as the previous point is also the same i.e. returning an existing instance owned by someone else).

Now if we look at the above questions, we might find ourselves needing something other than a factory which instead of returning a new instance returns an existing instance of object from the system. And this is where service locator comes in picture.

Using the code

So from the above discussion, it is clear that we need Service locator instead of a factory whenever we want to 'Locate' an existing object/service and return it to the caller. And the client code does NOT own the returned object/service but instead only going to use it.

From this discussion it is clear that we need to create a Service Locator class that is capable of

  1. Letting the application register the concrete implementations(objects/services) for a given contract(interfaces).
  2. Let the calling client code get the concrete implementation(object/service) using a contract(interface).

Let us try to look at this in action using a very contrived example.

The example we will be working on will be a simple audio file manager which let the user manage a given audio file. One part of this application is to be able to play the file being managed. The way we will implement this is that we will have an ApplicationFacade class that will handle the play request from the presentation layer(console in this case). This facade will then use the IPlaybackService interface to request the playback. Lets say that the current playback is being handled by DirectX and we have a concrete class for playing an audio file using DXPlaybackService. But we want the application to be extensible to support other playback engine(lets say LibVLCPlaybackService) later.

Now the reason for using a service locator in this case is that creation of playback service is an expensive operation and we don't want application to be able to create the instance whenever the operation is requested since this will create a delay between the play request from user and actual playback. Also, we want the have only one instance of the engine in our application which will be returned to the calling code.

Let us look at how this application is designed. Lets start by looking at the model that encapsulates the audio file.

C#
public class AudioFile
{
    public string Title { get; set; }
    public string FilePath { get; set; }
}

Now that we have the data structure to hold the audio file, lets look at the interface that all the playback services should conform to.

C#
public interface IPlaybackService
{
    void PlayFile(string filePath);
}

Now lets try to create the Concrete service class that will use DirectX to play the audio file(given a file path).

C#
public class DXPlaybackService : IPlaybackService
{
    public void PlayFile(string filePath)
    {
        Console.WriteLine("DirectX is being used to play {0}", filePath);
    }
}

Now that we have all the playback related data structures and services in place, lets try to write our application facade class in such a way that it will use a service locator to find the concrete instance of playback service and use it to perform playback.

C#
public class ApplicationFacade
{
    AudioFile m_Audiofile = null;
    IPlaybackService service = null;

    public ApplicationFacade(AudioFile file)
    {
        m_Audiofile = file;
    }

    public void Play()
    {
        Console.WriteLine("Requesting playback for {0}", m_Audiofile.Title);
        service = ServiceLocator.GetService < IPlaybackService >();
        if(service != null)
        {
            service.PlayFile(m_Audiofile.FilePath);
        }
    }
}

We still have not created the ServiceLocator class. So lets create a simple service locator that will have the possibility of registering and retrieve the concrete service using the interface.

C#
public class ServiceLocator
{
    static Dictionary < string, object > servicesDictionary = new Dictionary < string, object >();

    public static void Register < T >(T service)
    {
        servicesDictionary[typeof(T).Name] = service;
    }

    public static T GetService < T >()
    {
        T instance = default(T);
        if(servicesDictionary.ContainsKey(typeof(T).Name) == true)
        {
            instance = (T) servicesDictionary[typeof(T).Name];
        }
        return instance;
    }
}

With this we have a rudimentary service locator implementation in place. Now lets try to simulate the user interaction with the application to all this in action. Following things should happen when a user runs the application.

  1. We will create the available playback service at the time of application start.
  2. We will register that service with our service locator class.
  3. User will select the audio file for playback.
  4. The ApplicationFacade will use the service locator behind the scenes to get the handle to concrete service and play the audio file.

Following code mimics all the above mentioned steps.

C#
static void Main(string[] args)
{
	// Lets use the DirectX service to play the file
	IPlaybackService playbackService = new DXPlaybackService();

	// First let us register our audio playback service with service locator
	ServiceLocator.Register < IPlaybackService >(playbackService);
	
	// Lets try now to mimic an audio file playback
	// Let the user select a file
	AudioFile file = new AudioFile
	{
		Title = "Dummy File",
		FilePath = "C:\\DummyFile.wav"
	};

	// Lets instantiate our application facade passing the audio file to it
	ApplicationFacade facade = new ApplicationFacade(file);

	// Lets Emulate the user request for playback
	facade.Play();
	Console.ReadLine();
}

When we run the application we can see that the DirectX will be used to play our audio.

Image 1

Now lets say at some later point we decide to use LibVlc for playback and write a service for that.

C#
public class LibVlcPlaybackService : IPlaybackService
{
    public void PlayFile(string filePath)
    {
        Console.WriteLine("LibVlc is being used to play {0}", filePath);
    }
}

Now the only thing that needed to be changed is to create the instance of LibVlcPlaybackService and register it with our service locator. Following code shows that version of our mimicking code.

C#
static void Main(string[] args)
{
	// Lets use the LinVlc service to play the file
    IPlaybackService playbackService = new LibVlcPlaybackService();

	// First let us register our audio playback service with service locator
	ServiceLocator.Register < IPlaybackService >(playbackService);
	
	// Lets try now to mimic an audio file playback
	// Let the user select a file
	AudioFile file = new AudioFile
	{
		Title = "Dummy File",
		FilePath = "C:\\DummyFile.wav"
	};

	// Lets instantiate our application facade passing the audio file to it
	ApplicationFacade facade = new ApplicationFacade(file);

	// Lets Emulate the user request for playback
	facade.Play();
	Console.ReadLine();
}

And now when we run the application we can see that the LibVlc is being used for playing our audio.

Image 2

The important thing to note here is that we didn't had to change the caller code i.e. ApplicationFacade to use the new service.

Points to interest

One of the important points to remember here is that service locator should be used over factory when we want to locate and return an existing instance of object/service to the caller and we dont want the caller to have the ownership of returned objects. In our example we are creating and registering new services in code but that is for the demonstration purpose only. we could create new services in separate DLLs and put the service registration in config file(and service locator takes care of using the config file to determine the service to be used). Or we could let the user explicitly register the services at the application start like a few ORMs and IoC containers do. This article has been written from beginner's perspective. I hope this has been useful.

History

  • 20 January, 2016 - First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)