Singleton, Part I

Act I: The Eager Newbie Game Developer

Singleton? My golden hammer. Everything looked like a nail. Singleton? Eureka! Code’s magic wand. Everything? Singleton-ized. Efficiency, right?

public class GameManager {
    private static GameManager _instance;
    public static GameManager Instance {
        get {
            if (_instance == null) {
                _instance = new GameManager();
            }
            return _instance;
        }
    }
}

Ah, I remember when I first stumbled upon the Singleton pattern. You know that feeling when you find a shiny new toy and you just want to play with it everywhere? That was me with Singletons. It felt like I’d found the ‘One Ring’ of game development.

I’d think to myself: “Why would I want multiple instances of a game manager, or a sound manager, when I can just have one that rules them all?” It was like discovering instant noodles for the first time; quick, easy, and seemingly satisfying.

Everywhere I looked in my codebase, there was an opportunity for a Singleton. High scores manager? Singleton. Player stats? Singleton. UI manager? Yep, you guessed it โ€“ Singleton.


Act II: The Realization and The Mess

Singletons? Everywhere. Code, like spaghetti. Entangled, messy. Debugging? Pulling one string, everything unraveled. Unit tests? More like puzzles with missing pieces. Concurrency? Like juggling knives, blindfolded.

Fast forward a few years, and oh boy, did that shine wear off. It’s like when you overeat those instant noodles and realize maybe they aren’t as great as you thought.

Remember when I said I treated Singleton as the ‘One Ring’? Well, just like in the tales of Middle-Earth, there was a price to pay. Here are some of the messes I landed in:

  1. Hidden Dependencies: Singletons? They’re like ninjas. Silent, everywhere, but when they strike? Chaos. Ever tried finding a ninja in a dense forest? That’s what debugging felt like. My game components became tightly coupled. It’s like when you throw all your toys into a single toy box, and then one day, you pull out a toy and half the box comes with it. Not fun.
  2. Testing Nightmares: Trying to write unit tests was like trying to navigate a maze blindfolded. With Singletons everywhere, isolating components became nearly impossible.
  3. Concurrency Issues: Imagine trying to play a game of Jenga, but with multiple hands pulling out blocks simultaneously. Chaos, right? That was my codebase with Singletons when multiple threads tried accessing them.

Act III: The Redemption

As my knowledge in software architecture matured, I began to see the pitfalls of my ways. I thought back to the days when I was sprinkling Singletons like they were magic fairy dust and realized that I’d been so naรฏve. But with wisdom comes solutions.

  • Hidden Dependencies? They’re shadows. Lurking, always. Solution? Shine a light. Dependency Injection.
  • Testing Nightmares? Singletons, the culprits. Solution? Factories. Control the creation.
  • Concurrency Chaos? Singletons, unpredictable. Like wild animals in a city. Solution? Scoped instances. Keep the wild in the wilderness.

Dependency Injection: Instead of Singletons, I started passing dependencies to objects that needed them. It’s like giving each toy its own shelf, making it easier to find and play with without disturbing the others.

public class GameManager
{
    private ISoundManager _soundManager;
    private IGuiManager _guiManager;
    private IInputManager _inputManager;
    private IDataManager _dataManager;

    public GameManager(ISoundManager soundManager, 
                       IGuiManager guiManager, 
                       IInputManager inputManager, 
                       IDataManager dataManager)
    {
        _soundManager = soundManager;
        _guiManager = guiManager;
        _inputManager = inputManager;
        _dataManager = dataManager;
    }
}

// Here's a basic example using a hypothetical DI container:
// Example of how we can bind interfaces to concrete implementations:

DIContainer container = new DIContainer();

// Bindings (if we want to change concrete implementation this is the place
// where we should do it, and the rest of the code stays the same because
// everything is binded to interfaces

container.Bind<ISoundManager>().To<MySoundManager>().AsSingleton();
container.Bind<IGuiManager>().To<MyGuiManager>().AsSingleton();
container.Bind<IInputManager>().To<MyInputManager>().AsTransient();
container.Bind<IDataManager>().To<MyDataManager>().AsSingleton();

// Retrieving an instance (the container will handle the injection)

GameManager gameManager = container.Resolve<GameManager>();

In this example:

  • AsSingleton() ensures that only one instance of the object is created and the same instance is returned for every request.
  • AsTransient() ensures a new instance is provided every time.

Explanation: Binding helps the DI container know which concrete class to use when an interface is requested. When we resolve the GameManager from the container, it sees that the GameManager needs several interfaces (ISoundManager, IGuiManager, etc.). It then checks its bindings to find out which concrete classes to instantiate and then injects these into the GameManager.

This automates the process of object creation and ensures proper adherence to patterns like the Singleton pattern, without manual management. It also centralizes the configuration, making it easier to change implementations or configurations without modifying the rest of your code.

Reasons why Dependency Injection is done this way:

  1. Separation of Concerns: Each manager (ISoundManager, IGuiManager, etc.) presumably handles a separate concern, and GameManager simply orchestrates them. DI ensures that GameManager is not responsible for creating or managing the lifecycle of these dependencies.
  2. Flexibility: By depending on abstractions (interfaces) and not concrete implementations, you can easily swap out, for example, one sound manager implementation for another without changing the GameManager class.
  3. Testability: For unit testing, you can inject mock or stub versions of the dependencies, allowing you to test the GameManager in isolation without real implementations of its dependencies.
  4. Reusability: Dependencies can be shared and reused across multiple classes, ensuring that there’s a single source of truth and potentially reducing redundancy and memory usage.
  5. Maintainability: If a dependency needs to change or be refactored, you can do so without affecting classes that depend on it, as long as the interface contract remains consistent.
  6. Explicit Dependencies: By injecting dependencies, it’s clear what a class requires to function properly. This can make it easier to understand and debug the system.

By embracing Dependency Injection, you’re adhering to the SOLID principles of object-oriented programming, particularly the Dependency Inversion Principle, which states that high-level modules should not depend on low-level modules but both should depend on abstractions. Dependency Injection deserves separate post and will be covered in detail in future.


Factories & Service Locators: Rather than force a single instance, I created factories to produce objects when necessary and service locators to manage instances. It’s like having a toy store where you can either buy a new toy or find the one you want from the display.

The Service Locator pattern provides another way to access services (like dependencies) in a system. Unlike Dependency Injection, where dependencies are pushed into a class through constructors, properties, or methods, the Service Locator pattern lets a class pull its dependencies from a central registry.

Let’s adapt the GameManager example to illustrate the Service Locator pattern:

First, we need to create our service locator.

public class ServiceLocator
{
    private IDictionary<Type, object> services;

    private static ServiceLocator _instance;
    public static ServiceLocator Instance => _instance ??= new ServiceLocator();

    private ServiceLocator()
    {
        services = new Dictionary<Type, object>();
    }

    public T GetService<T>()
    {
        try
        {
            return (T)services[typeof(T)];
        }
        catch (KeyNotFoundException)
        {
            throw new ApplicationException($"The requested service of type {typeof(T)} is not registered.");
        }
    }

    public void RegisterService<T>(T service)
    {
        services[typeof(T)] = service;
    }
}

Before using any services, we need to register them with the locator:

ServiceLocator.Instance.RegisterService(new MySoundManager());
ServiceLocator.Instance.RegisterService(new MyGuiManager());
ServiceLocator.Instance.RegisterService(new MyInputManager());
ServiceLocator.Instance.RegisterService(new MyDataManager());

Rather than injecting dependencies through the constructor, GameManager will now request its dependencies from the ServiceLocator:

public class GameManager
{
    private ISoundManager _soundManager;
    private IGuiManager _guiManager;
    private IInputManager _inputManager;
    private IDataManager _dataManager;

    public GameManager()
    {
        _soundManager = ServiceLocator.Instance.GetService<ISoundManager>();
        _guiManager = ServiceLocator.Instance.GetService<IGuiManager>();
        _inputManager = ServiceLocator.Instance.GetService<IInputManager>();
        _dataManager = ServiceLocator.Instance.GetService<IDataManager>();
    }

    // ... Rest of the class remains unchanged
}

When you want to create a new instance of GameManager, you just do:

GameManager gameManager = new GameManager();

Advantages of Service Locator:

  1. Simplicity: It’s straightforward to set up, especially in small projects or prototyping.
  2. Flexibility: You can change out service implementations without modifying the classes that use them.

Criticism of Service Locator:

  1. Hidden Dependencies: Unlike Dependency Injection, which makes dependencies explicit through constructors or properties, the Service Locator hides dependencies, making classes harder to understand or test in isolation.
  2. Global State: The Service Locator often acts as a global point of access, which can make systems harder to understand and debug.
  3. Tight Coupling: Classes are directly coupled to the service locator’s specific implementation, which can make it harder to refactor or replace components in the future.

In practice, Dependency Injection (especially when using DI containers) is often favored over the Service Locator pattern in many scenarios due to its advantages in testability, maintainability, and adhering to SOLID principles. However, understanding the Service Locator pattern can still be valuable, and it may be suitable for specific situations or simpler projects.


Scoped Singletons: For cases where Singletons made sense, I made sure they were scoped, like having a Singleton for each level or scene. This way, it’s like having a toy only for the playground and not the entire neighborhood.


Epilogue: Lessons Learned

Remember the bug? Changed a setting, three features broke. Like pouring water, not knowing where it’d flow. Or the save system? Wanted one save file. Got fifty. Singletons, acting like unsupervised kids with paint.

It’s funny when I look back. Remember that time I spent an entire day debugging because two seemingly unrelated components were affecting each other? Or when I almost threw my computer out of the window because of untraceable bugs caused by Singleton misuse? Ah, good times.

But you know what? Every misstep was a lesson. Every frustration was a push towards becoming a better developer. So, to all the budding developers out there: It’s okay to make mistakes, to fall in love with a pattern, and to later realize its flaws. It’s all part of the journey.

In the world of game development, patterns are tools, not doctrines. Use them wisely, adapt, and never stop learning. And always remember, no matter how alluring a shiny new toy might seem, moderation is key!Lesson?

Tools are tools. Not toys. Use wisely. Avoid the trap. Keep it clean, keep it lean. And always, always learn.