Locator
Provide a global point of access to a service without coupling users to the concrete class that implements it.
There are many classes that are used frequently throughout the codebase, like allocators, logging, or random number generation.
Let’s talk about playing sounds:
Should we use a static class?
::playSound(VERY_LOUD_BANG); AudioSystem
Or a singleton?
::instance()->playSound(VERY_LOUD_BANG); AudioSystem
Either way does the job, but we introducing coupling. And if we decide to refactor it, we have to change every caller.
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. We don’t need our own address at all.
This is the Service locator pattern: it decouples code that needs a service from who it is (concrete implementation) and where it is (how we get to the instance of it).
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.
This is still global state like the singleton. Use it sparingly.
If you can, consider passing the object to it instead.
But, if you can’t, this pattern works.
The service locator defers coupling between two pieces of code until runtime. This gives you flexibility, but the price you pay is it’s harder to understand what your dependencies are by reading the code.
Since this pattern has to locate the service, we have to handle the case where that fails. We’ll need a null service for that.
Since the locator is globally accessible, any code in the game could be requesting a service and then poking at it. If a service can only be used in certain contexts, it’s best not to expose it to the world with this pattern.
Here’s the interface for the service:
class Audio {
public:
virtual ~Audio() {}
virtual void playSound(int soundID) = 0;
virtual void stopSound(int soundID) = 0;
virtual void stopAllSounds() = 0;
};
Here’s an implementation:
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...
}
};
And the locator:
class Locator {
public:
static Audio* getAudio() { return service_; }
static void provide(Audio* service) {
service_ = service;
}
private:
static Audio* service_;
};
This is dependency injection: if you have one class that depends on another, outside code is responsible for injecting that dependency into the object that needs it.
the static function getAudio()
does the locating. Call
it to get an instance back of the service.
*audio = Locator::getAudio();
Audio ->playSound(VERY_LOUD_BANG); audio
The way it is located is simple. It relies on outside code to register a service provider: When the game is starting up, it calls some code like this:
*audio = new ConsoleAudio();
ConsoleAudio ::provide(audio); Locator
This class takes the interface, not a concrete implementation, so it doesn’t need to know about it’s implementation, which lets it be applied retroactively to existing classes.
Our implementation is simple and flexible so far. 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.
Let’s provide a Null Object to address this. The null object is an implementation that does nothing.
class NullAudio: public Audio {
public:
virtual void playSound(int soundID) { /* Do nothing. */ }
virtual void stopSound(int soundID) { /* Do nothing. */ }
virtual void stopAllSounds() { /* Do nothing. */ }
};
Now we need our locator to handle 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_;
};
We’re defaulting our service to an instance of nullService, and returning a reference, which is an indication this can’t be null.
Let’s discuss another refinement to this pattern: Decorated services.
Let’s try to decorate our audio class by wrapping it with a logging function.
class LoggedAudio : public Audio {
public:
(Audio &wrapped)
LoggedAudio: wrapped_(wrapped)
{}
virtual void playSound(int soundID) {
(;
logwrapped_.playSound(soundID);
}
virtual void stopSound(int soundID) {
(;
logwrapped_.stopSound(soundID);
}
virtual void stopAllSounds() {
(;
logwrapped_.stopAllSounds();
}
private:
void log(const char* message) {
// Code to log message...
}
&wrapped_;
Audio };
Now to enable audiologging, here’s what we do:
// Decorate the existing service.
*service = new LoggedAudio(Locator::getAudio());
Audio
// Swap it in.
::provide(service); Locator
This is what we did, and it’s the most common:
Pros:
Cons:
Using preprocessor macros:
class Locator {
public:
static Audio& getAudio() { return service_; }
private:
#if DEBUG
static DebugAudio service_;
#else
static ReleaseAudio service_;
#endif
};
Pros:
Cons:
This is what most people do in enterprise business software:
We can put everything in a configuration file that’s loaded at runtime.
Pros:
Cons:
If the locator can’t find the service, return NULL
:
Assert the service exists. If not, crash the game.
class Locator {
public:
static Audio& getAudio() {
* service = NULL;
Audio
// Code here to locate service...
assert(service != NULL);
return *service;
}
};
Up till now, anyone can get the service through the locator. We can limit access to a single class and its descendants:
class Base {
// Code to locate service and set service_...
protected:
// Derived classes can use service
static Audio& getAudio() { return *service_; }
private:
static Audio* service_;
};
Access to the service is restricted to classes that inherit Base: that way, we control coupling. This can be useful for services that need to be scoped.
Prev: event-queue Next: data-locality