Skip to Content
Course content

103: The Singleton Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You'll hear a lot of debate about the Singleton pattern. Some developers call it an "anti-pattern" because it can lead to hidden dependencies and makes unit testing a headache if you aren't careful. But in the real world, there are just some things that should only exist once. A configuration manager, a cache, or in our case today, a game settings manager.

Imagine we're building a small indie game. We need a way to track things like volume and screen resolution. We don't want to pass a settings object into every single class in our game—that's a nightmare. We want one central source of truth that any class can talk to.

Starting with a global settings hub

My first instinct is to create a class that holds our data and a static property to access it. Let's keep it simple: just a volume level and a resolution string.

public class GameSettings
{
    public float Volume { get; set; } = 0.75f;
    public string Resolution { get; set; } = "1920x1080";

    public static GameSettings Instance { get; private set; } = new GameSettings();
}

At first glance, this looks great. I can just call GameSettings.Instance.Volume from anywhere in my project. It's convenient, and it gets the job done for a quick prototype.

Oops, I left the door open

Here is where I messed up. I was testing my code and realized that while I provided a convenient Instance, I didn't actually stop anyone from creating another instance of the settings. Look at this:

var settings1 = GameSettings.Instance;
var settings2 = new GameSettings(); // Wait, I can still do this?

settings1.Volume = 0.1f;
settings2.Volume = 1.0f;

Console.WriteLine(settings1.Volume); // 0.1
Console.WriteLine(settings2.Volume); // 1.0

This defeats the entire purpose of a Singleton. If one part of my game is updating the volume on the singleton instance and another part is accidentally using a new GameSettings() object, my game state is now fragmented. I've created two "sources of truth," which is exactly what we're trying to avoid.

Locking it down and making it thread-safe

To fix this, I need to make the constructor private. If the constructor is private, the new keyword won't work outside of the class itself. But there's another issue: thread safety. If two different threads try to initialize the instance at the exact same time, we could still end up with two objects.

In modern C#, the cleanest way to handle this is using the Lazy<T> type. It handles the thread safety for us and ensures the object is only created the very first time it's actually accessed (lazy initialization).

public sealed class GameSettings
{
    // Lazy ensures thread-safety and delayed initialization
    private static readonly Lazy<GameSettings> _lazy = 
        new Lazy<GameSettings>(() => new GameSettings());

    public static GameSettings Instance => _lazy.Value;

    // The magic happens here: private constructor
    private GameSettings() 
    {
        Console.WriteLine("Settings initialized!");
    }

    public float Volume { get; set; } = 0.75f;
    public string Resolution { get; set; } = "1920x1080";
}

I also added the sealed keyword to the class. This prevents other classes from inheriting from it, which could potentially create multiple instances of a derived class. Now, the only way into this class is through GameSettings.Instance. It's locked down, efficient, and safe.




📋 Practical Task

Implementing a Thread-Safe DatabaseConnectionPool Singleton

In a real application, opening a new database connection for every single query is incredibly expensive. To solve this, we often use a connection pool that stays open for the duration of the app's life.

Your task is to create a class called DatabaseConnectionPool that implements the Singleton pattern using the Lazy<T> approach we discussed. It should meet the following requirements:

  • The class must be sealed.
  • The constructor must be private to prevent external instantiation.
  • It must provide a public static property called Instance.
  • Add a simple method called GetConnection() that returns a string (e.g., "Connection-12345") to simulate retrieving a connection from the pool.

Test your implementation by trying to call new DatabaseConnectionPool() in your Main method; your code should fail to compile, proving that the Singleton is properly locked down.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.