Welcome to the first lesson in our Creational Design Patterns course. We are starting with a powerful and widely used pattern: the Singleton Pattern. This pattern helps ensure that a class has only one instance and provides a global point of access to it. Understanding this pattern is a fantastic first step on your journey to mastering creational design patterns.
The Singleton Pattern is one of the simplest and most commonly used design patterns in software development. Its primary purpose is to restrict the instantiation of a class to a single object. This pattern ensures that a class has only one instance and offers a global access point to that instance.
Using the Singleton Pattern simplifies the management of shared resources and is particularly useful for scenarios such as:
For example, if each module within an application creates its own instance of a configuration loader, you could end up with unnecessary duplicates and inconsistencies. By using the Singleton Pattern, you ensure that all parts of the application use the same instance of the configuration loader, maintaining a consistent and efficient approach to configuration management.
To better understand the Singleton Pattern, let's build a Logger
class step by step. This approach will help you grasp each component necessary to implement the Singleton Pattern effectively in C#.
First, let's define a simple Logger
class by making it sealed and including a private constructor to prevent instantiation from outside the class:
C#1// Sealed Logger class, preventing inheritance 2public sealed class Logger 3{ 4 // Private constructor to prevent instantiation from outside 5 private Logger() {} 6}
Here, the Logger
class is sealed to prevent inheritance, and the private constructor ensures that no other instances can be instantiated from outside the class.
Next, we'll add a private static readonly Lazy<Logger>
object for thread-safe, lazy initialization of our singleton instance:
C#1// Sealed Logger class, preventing inheritance 2public sealed class Logger 3{ 4 // Private constructor to prevent instantiation from outside 5 private Logger() {} 6 7 // Lazy initialization to ensure thread-safety 8 private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger()); 9}
Here's an explanation of the key components used in this step:
instance
field belongs to the class itself rather than to any specific object. This ensures that there's only one shared instance of Logger
.instance
field can only be assigned once at runtime and cannot be changed afterward. This guarantees that the Lazy<Logger>
instance is immutable.Lazy<Logger>
type provides a way to delay the creation of the instance until it is actually needed. This means that the Logger
instance won't be created until the first time it is accessed, saving resources and improving performance. Additionally, it ensures thread-safety, meaning multiple threads can safely access the Logger
instance without causing race conditions or creating multiple instances.Now, we'll add a public static property to return the single Logger
instance:
C#1// Sealed Logger class, preventing inheritance 2public sealed class Logger 3{ 4 // Private constructor to prevent instantiation from outside 5 private Logger() {} 6 7 // Lazy initialization to ensure thread-safety 8 private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger()); 9 10 // Public property to access the single instance 11 public static Logger Instance => instance.Value; 12}
This step provides a public property to access the single instance of Logger
, ensuring it is created only when needed.
Finally, we'll add a Log
method to demonstrate the functionality of our Logger
singleton:
C#1// Sealed Logger class, preventing inheritance 2public sealed class Logger 3{ 4 // Private constructor to prevent instantiation from outside 5 private Logger() {} 6 7 // Lazy initialization to ensure thread-safety 8 private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger()); 9 10 // Public property to access the single instance 11 public static Logger Instance => instance.Value; 12 13 // Method to log messages to the console 14 public void Log(string message) 15 { 16 Console.WriteLine(message); 17 } 18}
In this final step, the Log
method is added to allow logging messages to the console, showcasing the singleton's functionality.
Let's see how we can use the Logger
singleton in a simple Program
class. Notice that we don't directly instantiate Logger
; instead, we access the single instance through the Instance
property.
C#1class Program 2{ 3 static void Main() 4 { 5 // Access the Logger instance and log a message 6 Logger.Instance.Log("Singleton pattern example with Logger."); 7 } 8}
Behind the scenes, a few things happen to ensure that only one instance of Logger
is created and reused:
Instance
is accessed, it triggers the creation of the single Logger
instance.Instance
will return the already created instance.This mechanism ensures thread-safety and that the Singleton Pattern only allows one instance of Logger
to exist throughout the application.
The Singleton Pattern is critical for scenarios where exactly one object is needed to manage a specific task, such as logging, configuration settings, or managing database connections.
By guaranteeing that only one instance of a class exists, you can:
Understanding and implementing the Singleton Pattern will provide you with a robust tool for managing resources efficiently. It’s a simple yet powerful way to improve your program’s design and reliability.
Let’s dive in and learn how to apply this pattern effectively. Are you ready? Let’s start coding!