Better than Singletons: The Service Locator Pattern

One of the biggest problems in object-oriented programming is getting access to the objects you need.

A very common solution to this problem is dependency injection. This means you have to pass every method the objects it needs to do its work (you can also pass them to the constructor of the methods object and store them in member variables). What sounds easy and logical at first also gets messy quite quickly when applied in practice.

With dependency injection, you have to make sure the right objects are available where ever they are needed. That means you often have to pass several objects to the same method and you have to add new parameters to methods and constructors all the time. As your code grows bigger passing those objects through your project starts to feel very repetitive and when you refactor your code you have to touch a lot more methods than you would need to touch without dependency injection.

The Singleton Pattern

Another solution, that was very popular in the early days of object-oriented programming is the singleton pattern. A singleton is a class of which there can exist only one instance at any point in time. The class also provides a static method to get this single object from anywhere in the code.

The singleton pattern is not a general solution because unlike dependency injection it doesn’t allow to make many different instances of a class available to other objects. Instead, it is best suited for cases where you have an object that represents a subsystem of your software and you want this subsystem to be available from everywhere in your code.

A subsystem could be a global settings object, an audio subsystem, a game object containing the global state of a game, or something similar.

In this article, we will use a settings object for our examples. It provides methods to load all settings from disk, save all settings to disk and to get and change the value of a single setting. For the sake of simplicity, all settings can only store string values.

The C++ implementation of such a settings singleton could look like this:

class Settings
{
    public:
        static Settings& getInstance()
        {
            // C++11 guarantees the initialization of
            // static variables to be thread-safe
            static Settings instance;

            return instance;
        }

        // actual methods
        bool loadSettings() {/*...*/};
        bool saveSettings() {/*...*/};
        std::string getSetting(const std::string &name) {/*...*/};
        void setSetting(const std::string &name, const std::string &value) {/*...*/};

    private:
        Settings() {}

    public:
        Settings(Settings const&)    = delete;
        void operator=(Settings const&)  = delete;
};

This class has a static method getInstance() that returns the one and only instance. Like all static methods, it is available from every part of a project as long as the declaration of the Settings class is available. The instance is created by declaring a static instance of the class and returning a reference.

To prevent the creation of other objects of the class the default constructor is declared private and the copy and assignment constructor are deleted.

The Settings singleton can be used like:

Settings.getInstance().loadSettings();
std::string title = Settings.getInstance().getSetting("title");

or

Settings &settings = Settings.getInstance();

settings.loadSettings();
std::string title = settings.getSetting("title");

Singletons have several drawbacks:

  • Tight coupling: The code is tightly coupled to the singleton.
  • Hard to swap out: Since the singleton class controls the creation of the single object you can’t easily swap it out for another object.
  • State is carried in a global object that has the same lifetime as the application
  • Hard to test: All the drawbacks mentioned above make it really hard to unit test an application that uses singletons.

The Service Locator Pattern

A very good alternative to a singleton is the service locator pattern. Like the singleton, it provides the code with a global entry point to request the objects that it needs. But unlike the singleton, it puts an intermediary between the requested object and the global requestor method. This additional level of indirection does away with most of the drawbacks of the singleton pattern.

Let’s look at an example of a service locator class to get an understanding of how this works:

class ServiceLocator
{
    public:
        static ISettings *getSettings()
        {
            return ServiceLocator::settings;
        }

        static void provideSettings(ISettings *settings)
        {
            ServiceLocator::settings = settings;
        }

    private:
        static ISettings *settings;
};

// initialize the static class member
ISettings* ServiceLocator::settings = nullptr;

ServiceLocator is a static class that has a static member variable to store a pointer to a Settings object (our service object). It also has a static method getSettings() that allows the client code to get the pointer to this object. And it also has one more static method named provideSettings() that allows the code that initially creates the Settings object to make the pointer to this object known to the service locator in the first place.

Dataflow between creator, ServiceLocator and consumers
The ServiceLocator is globally known in the code base. Any class or function can be the creator of the service object. Once created, the creator registers the service object with the ServiceLocator by calling its static provideSettings() method. From then on every piece of code can get the service object by calling the getSettings() method of the ServiceLocator.

So with this, we can just create a settings object somewhere in our code (ideally at the start of our program) and make it known to the service locator by calling provideSettings() with a pointer to our new object. From then on every other part of our program can get the pointer to the settings object by calling getSettings(). We just need to make sure that we provide the service locator with a pointer to our object before the first consumer calls getSettings(). If we fail at this the service locator will return a nullptr to the consumer and our program will probably crash (unless the client code checks for a null pointer, of course).

Also, we need to free our object at the end of our program and we need to make sure that we don’t free it before the last call to getSettings() happens.

The only thing that is still missing is our settings class.

First, we declare an interface from which we will derive the settings class. This will allow us to replace the Settings class with other implementations (e.g. for unit testing) with ease:

class ISettings
{
    public:
        virtual bool loadSettings() = 0;
        virtual bool saveSettings() = 0;
        virtual std::string getSetting(const std::string &name) = 0;
        virtual void setSetting(const std::string &name, const std::string &value) = 0;
};

For the concrete implementation, we might decide to use JSON as the storage format. Therefore, our class will be named JsonSettings. The beauty of this design is that if we decide to store our settings in a different format in the future, we could create other implementations like RegistrySettings or SqliteSettings, for example. But for brevity, we don’t do a real implementation here anyway, so let’s discuss the design of the JsonSettings class:

class JsonSettings : public ISettings
{
    public:
        JsonSettings()
        {
            /*...*/
        }

        virtual bool loadSettings() override
        {
            /*...*/
        }

        virtual bool saveSettings() override
        {
            /*...*/
        }

        virtual std::string getSetting(const std::string &name) override
        {
            /*...*/
        }

        virtual void setSetting(const std::string &name, const std::string &value)
        {
             /*...*/
        }

        JsonSettings(JsonSettings const&)        = delete;
        void operator=(JsonSettings const&)  = delete;
};

Note that we delete the copy constructor and the copy assignment constructor to prevent the unintended copying of the object. Unlike with the singleton here the default constructor is not private. The client code could still create its own instance of the object (which in most cases should be prohibited via coding guidelines).

In our program, we can now use a JsonSettings object and the service locator like this:

#include <memory>

...

int main(void)
{
    std::unique_ptr<JsonSettings> settings = std::make_unique<JsonSettings>();
    ServiceLocator::provideSettings(settings.get());

    // create other objects and run our program
    ...

    return 0;
}

First, we create the JsonSettings object via make_unique in the main function. We then pass a raw pointer to the object to the service locator and run our normal program code. All code can now access the raw pointer of our JsonSettings object like this:

ISettings *settings = ServiceLocator::getSettings();

settings->loadSettings();
std::string title = settings->getSettings("title");

Since it is managed by a unique_ptr the Settings object gets destroyed once the main() function (and with it the program) terminates.

At any point, we could exchange the object provided by the service locator for another one just by calling ServiceLocator::provideSettings(). This is very handy for testing because whenever we call a method that needs to access the settings object we just provide the service locator with a mock object before calling the method we want to test and our work is done.

Bridging Platform Differences

Another interesting use case is the bridging of the differences between different platforms. Let’s assume on Windows you want to store your settings in the Registry and on Linux, you want to store them in a JSON file in the home directory of the user. In this case, you could check in your main function what platform you are on and then you could just create the respective settings object and provide it to the service locator:

#include <memory>

...

int main(void)
{
#ifdef __linux__
    std::unique_ptr<JsonSettings> settings = std::make_unique<JsonSettings>();
#elif defined (_WIN32)
    std::unique_ptr<RegistrySettings> settings = std::make_unique<RegistrySettings>();
#endif

    ServiceLocator::provideSettings(settings.get());

    // create other objects and run our program
    ...

    return 0;
}

No more Null Pointers: The Null Object

The only ugly thing about this solution is the nullptr that is returned by the service locator if we forget to provide a settings object.

But this problem can be solved if we implement a so-called Null Object. A Null Object is an object that implements an interface by providing only empty methods. We will use such a Null Object instead of a nullptr as our default value.

First, we need to create an empty implementation of the ISettings interface:

class NullSettings : public ISettings
{
    public:
        NullSettings()
        {
        }

        virtual bool loadSettings() override
        {
        }

        virtual bool saveSettings() override
        {
        }

        virtual std::string getSetting(const std::string &name) override
        {
        }

        virtual void setSetting(const std::string &name, const std::string &value) override
        {
        }

        NullSettings(NullSettings const&)    = delete;
        void operator=(NullSettings const&)  = delete;
};

Now we need to adapt the ServiceLocator class to make use of a Null Object:

class ServiceLocator
{
    public:
        static ISettings *getSettings()
        {
            if (ServiceLocator::settings == nullptr) {
                ServiceLocator::init();
            }

            return ServiceLocator::settings;
        }

        static void provideSettings(ISettings *settings)
        {
            if (ServiceLocator::settings == nullptr) {
                ServiceLocator::init();
            }

            if (settings == nullptr) {
                settings = &ServiceLocator::nullSettings;
                return;
            }

            ServiceLocator::settings = settings;
        }

    private:
        static void init()
        {
            settings = &nullSettings;
        }

        static NullSettings nullSettings;
        static ISettings* settings;
};

// initialize the static class members
NullSettings ServiceLocator::nullSettings;
ISettings* ServiceLocator::settings = nullptr;

We made three changes to the class:

  1. There is a new private static member nullPointer which is our Null Object
  2. There is a private init() method now and it is called by the other methods when settings is a nullptr
  3. When the user passes a nullptr to the provideSettings() method we set the settings member to a pointer to the Null Object and discard the nullptr

References Instead of Pointers

Now that we got rid of the bad habit of returning null pointers there is one final improvement we could make to our service locator. Instead of returning a pointer from getSettings() we could return a reference to our settings object. That way the API would communicate to the user that we don’t return null pointers.

For this change, we only have to touch the getSettings() method. All we have to do is change the method prototype to return a reference instead of a pointer and dereference the settings pointer in the return statement:

        static ISettings &getSettings()
        {
            if (ServiceLocator::settings == nullptr) {
                ServiceLocator::init();
            }

            return *ServiceLocator::settings;
        }

Final Thoughts

The service locator pattern feels like a refined version of the singleton pattern. Like the singleton, it gives us global access to an object but unlike the singleton, it doesn’t tightly couple our client code to one object and one class. Instead, it allows us to exchange the object that is used by our code for another object at any time.

Coupling is not as loose as with Dependency Injection, of course. There is still a global entry point and our code is coupled to the static getSettings() method. But depending on where you are coming from this doesn’t have to be a disadvantage. Coupling is loose enough to allow for easy unit testing and for all practical purposes this should be all we really need. Also, we don’t have to pass our service objects around all the time and there will never be a situation where we need to refactor our code because we need to have access to one of our service objects in a particular place.

Also, a service locator is not limited to providing only one kind of service object. Actually, a service locator can provide as many different service objects as needed. Just add an additional pointer variable and get...() and provide...() methods and you are good to go.

Thanks to its relatively loose coupling the service locator pattern is a great and flexible replacement for singleton classes.

Leave a comment