Game Programming Patterns / Decoupling Patterns

Service Locator

×

This book is a work in progress!

If you see a mistake, find something unclear, or have a suggestion, please file a ticket. To know when new chapters are up, join the mailing list:

Thank you!

— Bob (@munificentbob)

Intent

Provide a global point of access to a service without coupling users to the concrete class that implements it.

Motivation

Some objects or systems in a game tend to get around, visiting almost every corner of the codebase. It’s hard to find a part of the game that won’t need a memory allocator, logging, the file system, or random numbers at some point. Systems like those can be thought of as services that need to be available to the entire game.

For our example, we’ll consider audio. It doesn’t have quite the reach of something lower-level like a memory allocator, but it still touches a bunch of game systems: A falling rock hits the ground with a crash (physics). A sniper NPC fires his rifle and a shot rings out (AI). The user selects a menu item with a beep of confirmation (user interface).

Each of these places will need to be able to call into the audio system, with something like one of these:

// Use a static class?
AudioSystem::playSound(VERY_LOUD_BANG);

// Or maybe a singleton?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);

Either gets us where we’re trying to go, but we stumbled into some sticky coupling along the way. Every place in the game calling into our audio system directly references the concrete AudioSystem class and the mechanism for accessing it — either as a static class or a Singleton.

These callsites, of course, have to be coupled to something in order to make a sound play, but letting them poke directly at the concrete audio implementation is like giving a hundred strangers directions to your house just so they can drop a letter on your doorstep. Not only is it a little bit too personal, it’s a real pain when you move and you have to tell each person the new directions.

There’s a better solution: a phone book. People that need to get in touch with us can look us up by name and get our current address. When we move, we tell the phone company. They update the book, and everyone gets the new address. In fact, we don’t even need to give out our real address at all. We can list a P.O. box or some other "representation" of ourselves instead. By having callers go through the book to find us, we have a convenient single place where we control how we’re found.

This is the Service Locator pattern in a nutshell: it decouples code that needs a service from both who it is (the concrete implementation type) and where it is (how we get to the instance of it).

The Pattern

A service class defines an abstract interface to a set of operations. A concrete service provider implements this interface. A separate service locator provides access to the service by finding an appropriate provider while hiding both the provider’s concrete type and the process used to locate it.

When to Use It

Anytime you make something globally accessible to every part of your program, you’re asking for trouble. That’s the main problem with the Singleton pattern, and this pattern is no different. My simplest advice for when to use a service locator is: sparingly.

Instead of using a global mechanism to give some code access to an object it needs, first consider just passing the object to it. That’s dead simple, and it makes the coupling completely obvious. That will cover most of your needs.

But… there are some times when manually passing around an object is gratuitous or actively makes code harder to read. Some systems, like logging or memory management, shouldn’t be part of a module’s public API. The parameters to your rendering code should have to do with rendering, not stuff like logging.

Likewise, other systems represent facilities that are fundamentally singular in nature. Your game probably only has one audio device or display system that it can talk to. It is an ambient property of the environment, so plumbing it through ten layers of methods just so one deeply nested call can get to it is adding needless complexity to your code.

In those kinds of cases, this pattern can help. As we’ll see, it functions as a more flexible, more configurable cousin of the singleton. When used well, it can make your codebase more flexible with little runtime cost.

Keep in Mind

The service doesn’t know who is locating it

Since the locator is globally accessible, any code in the game could be requesting a service and then poking at it. This means that service must be able to work correctly in any circumstance. For example, a class that expects to only be used during the simulation portion of the game loop and not during rendering may not work as a service — it wouldn’t be able to ensure that it’s being used at the right time. So, if a class expects to only be used in a certain context, it’s safest to avoid exposing it to the entire world with this pattern.

The service actually has to be located

With a Singleton or a static class, there’s no chance for the instance we need to not be available. Calling code can take for granted that it’s there. But since this pattern has to locate the service, we may need to handle cases where that fails. Fortunately, we’ll cover a strategy later to address this and guarantee that we’ll always get some service when you need it.

Sample Code

Getting back to our audio system problem, let’s address it by exposing the system to the rest of the codebase through a service locator.

The service

We’ll start off with the audio API. This is the interface that our service will be exposing:

class Audio
{
public:
  virtual ~Audio() {}
  virtual void playSound(int soundID) = 0;
  virtual void stopSound(int soundID) = 0;
  virtual void stopAllSounds() = 0;
};

A real audio engine would be much more complex than this, of course, but this shows the basic idea. What’s important is that it’s an abstract interface class with no implementation bound to it.

The service provider

By itself, our audio interface isn’t very useful. We need a concrete implementation. This book isn’t about how to write audio code for a game console, so you’ll have to imagine there’s some actual code in the bodies of these functions, but you get the idea:

class ConsoleAudio : public Audio
{
public:
  virtual void playSound(int soundID)
  {
    // Play sound using console audio api...
  }

  virtual void stopSound(int soundID)
  {
    // Stop sound using console audio api...
  }

  virtual void stopAllSounds()
  {
    // Stop all sounds using console audio api...
  }
};

Now we have an interface and an implementation. The remaining piece is the service locator — the class that ties the two together.

A simple locator

The implementation here is about the simplest kind of service locator you can define:

class Locator
{
public:
  static Audio* getAudio() { return service_; }

  static void provide(Audio* service)
  {
    service_ = service;
  }

private:
  static Audio* service_;
};

The static getAudio() function does the locating — we can call it from anywhere in the codebase and it will give us back an instance of our Audio service to use:

void someGameCode()
{
  Audio *audio = Locator::getAudio();
  audio->playSound(VERY_LOUD_BANG);
}

The way it "locates" is very simple: it relies on some outside code to register a service provider before any tries to use the service. When the game is starting up, it calls some code like this:

void initGame()
{
  ConsoleAudio *audio = new ConsoleAudio();
  Locator::provide(audio);
}

The key part to notice here is that our someGameCode() function isn’t aware of the concrete ConsoleAudio class, just the abstract Audio interface. Equally important, not even the locator class is coupled to the concrete service provider. The only place in code that knows about the actual concrete class is the initialization function that registers the service.

There’s one more level of decoupling here: the Audio interface isn’t aware of the fact that it’s being accessed in most places through a service locator. As far as it knows, it’s just a regular abstract base class. This is useful because it means we can apply this pattern to existing classes that weren’t necessarily designed around it. This is in contrast with Singleton, which affects the design of the "service" class itself.

A null service

Our implementation so far is certainly simple, and it’s pretty flexible too. But it has one big shortcoming: if we try to use the service before a provider has been registered, it returns NULL. If the calling code doesn’t check that, we’re going to crash the game.

Fortunately, there’s another design pattern called "Null Object" we can use to address this. The basic idea is that in places where we would return NULL when we fail to find or create an object, we instead return a special object that implements the same interface as the desired object. Its implementation basically does nothing, but allows code that receives the object to safely continue on as if it had received a "real" one.

To use this, we’ll define another "null" service provider:

class NullAudio: public Audio
{
public:
  virtual void playSound(int soundID) { /* Do nothing. */ }
  virtual void stopSound(int soundID) { /* Do nothing. */ }
  virtual void stopAllSounds()        { /* Do nothing. */ }
};

As you can see, it implements the service interface, but doesn’t actually do anything. Now, we change our locator to this:

class Locator
{
public:
  static void initialize() { service_ = &nullService_; }

  static Audio& getAudio() { return *service_; }

  static void provide(Audio* service)
  {
    if (service == NULL)
    {
      // Revert to null service.
      service_ = &nullService_;
    }
    else
    {
      service_ = service;
    }
  }

private:
  static Audio* service_;
  static NullAudio nullService_;
};

Calling code will never know that a "real" service wasn’t found, nor does it have to worry about handling NULL. It’s guaranteed to always get back a valid object.

This is also useful for intentionally failing to find services. If we want to disable a system temporarily, we now have an easy way to do so: simply don’t register a provider for the service and the locator will default to a null provider.

Logging decorator

Now that our system is pretty robust, let’s discuss another refinement this pattern lets us do: decorated services. I’ll explain with an example.

During development, a little logging when interesting events occur can help you figure out what’s going on under the hood of your game engine. If you’re working on AI, you’d like to know when an entity changes AI states. If you’re the sound programmer, you may want a record of every sound as it plays so you can check that they trigger in the right order.

The typical solution is to just litter the code with calls to some log() function. Unfortunately, that replaces one problem with another: now we have too much logging. The AI coder really doesn’t care when sounds are playing, and the sound person doesn’t care about AI state transitions, but now they both have to wade through each other’s messages.

Ideally, we would be able to selectively enable logging for just the stuff we care about, and in the final game build, there’d be no logging at all. If the different systems we want to conditionally log are exposed as services, then we can solve this using the Decorator pattern. Let’s define another audio service provider implementation like this:

class LoggedAudio : public Audio
{
public:
  LoggedAudio(Audio &wrapped) : wrapped_(wrapped) {}

  virtual void playSound(int soundID)
  {
    log("play sound");
    wrapped_.playSound(soundID);
  }

  virtual void stopSound(int soundID)
  {
    log("stop sound");
    wrapped_.stopSound(soundID);
  }

  virtual void stopAllSounds()
  {
    log("stop all sounds");
    wrapped_.stopAllSounds();
  }


private:
  void log(const char* message)
  {
    // Code to log message...
  }

  Audio &wrapped_;
};

As you can see, it wraps another audio provider and exposes the same interface. It forwards the actual audio behavior to the inner provider, but also logs each sound call. If a programmer wants to enable audio logging, they call this:

void enableAudioLogging()
{
  // Decorate the existing service.
  Audio *service = new LoggedAudio(Locator::getAudio());

  // Swap it in.
  Locator::provide(service);
}

Now any calls to the audio service will be logged before continuing as before. And, of course, this plays nicely with our null service, so you can both disable audio and yet still log the sounds that it would play if sound were enabled.

Design Decisions

We’ve covered a typical implementation, but there’s a couple of ways that it can vary, based on differing answers to a few core questions:

How is the service located?

What happens if service could not be located?

Among these options, the one I see used most frequently is simply asserting that the service will be found. By the time a game gets out the door, it’s been very heavily tested, and it will likely be run on a reliable piece of hardware. The chances of a service failing to be found by then are pretty slim.

On a larger team, I encourage you to throw a null service in. It doesn’t take much effort to implement, and can spare you from some downtime during development when a service isn’t available. It also gives you an easy way to turn a service off if it’s buggy or is just distracting you from what you’re working on.

What is the scope of the service?

Up to this point, we’ve assumed that the locator will provide access to the service to anyone who wants it. While this is the typical way the pattern is used, another option is to limit access to a single class and its descendants, like so:

class Base
{
  // Code to locate service and set service_...

protected:
  // Derived classes can use service
  static Audio& getAudio() { return *service_; }

private:
  static Audio* service_;
};

With this, access to the service is restricted to classes that inherit Base. There are advantages either way:

My general guideline is that if the service is restricted to a single domain in the game then limit its scope to a class. For example, a service for getting access to the network can probably be limited to online classes. Services that get used more widely like logging should be global.

See Also