Service Locator¶
Intent: Provide a global point of access to a service without coupling users to the concrete class that implements it.
Warning
The book Game Programming Patterns advises using this pattern sparingly. Anytime something is accessible to every part of your program, you risk the same coupling problems as global variables. The book’s first recommendation is always: pass the object to the code that needs it (dependency injection). Only reach for a service locator when passing the object everywhere is genuinely impractical — e.g. a system used in dozens of unrelated subsystems.
Acron Mapping¶
Pattern role |
Acron equivalent |
|---|---|
Service interface |
Python abstract base class or duck-typed protocol |
Service provider |
Concrete implementation (e.g. |
Service locator |
Module-level registry dict, or a |
Built-in locators |
|
How to Implement¶
Define an interface (or just document the expected API on a base class).
Create a
Locatormodule with a registry dict andget(name)/register(name, instance)functions.At startup (
ShowBase.__init__or aGameinit method), register the concrete services.Game systems call
Locator.get('audio').play('bgm')— no import of the concrete class.For testing or platform swap, call
Locator.register('audio', MockAudioService())before the test runs.
Code Sketch (Python)¶
# services/locator.py
_registry = {}
def register(name: str, service) -> None:
_registry[name] = service
def get(name: str):
if name not in _registry:
raise KeyError(f"Service '{name}' not registered. Did you call register()?")
return _registry[name]
# services/audio.py
class NullAudio:
def play(self, name): pass
def stop(self, name): pass
class AcronAudio:
def __init__(self):
self._sounds = {}
def play(self, name):
if name not in self._sounds:
self._sounds[name] = loader.loadSfx(f'sfx/{name}.ogg')
self._sounds[name].play()
def stop(self, name):
if name in self._sounds:
self._sounds[name].stop()
# main.py — at startup
from services import locator
from services.audio import AcronAudio, NullAudio
if base.sfxManagerIsValid:
locator.register('audio', AcronAudio())
else:
locator.register('audio', NullAudio()) # headless / test mode
# anywhere in game code
from services import locator
locator.get('audio').play('explosion')
Pitfalls¶
Don’t wrap
base,taskMgr,messenger: these are already service locators. Adding a locator layer on top adds indirection with no benefit.Null service pattern: always register a
Null*fallback (no-op implementation) so unregistered services fail gracefully during testing rather than raisingKeyError.Registration order: services registered in
__init__may not yet havebase.loaderavailable. Register insideShowBase.__init__aftersuper().__init__()completes, or use lazy init in the service itself.