Design Patterns in C#: Complete Guide with Examples
Design patterns are proven solutions to typical problems in software development. In the C# and .NET ecosystem, they help create flexible, scalable, and maintainable architecture. In this article, we will break down four key patterns: Singleton, Factory Method, Observer, and Strategy, with real C# examples.
Why are design patterns needed in C#?
Patterns are not ready-made libraries or frameworks. They are conceptual templates that:
- Simplify communication — developers use a unified terminology
- Improve code quality — reduce coupling and increase flexibility
- Save time — no need to reinvent the wheel
- Adhere to SOLID principles — most patterns implement these principles
1. Singleton
Purpose: Ensures that a class has only one instance and provides a global point of access to it.
When to use:
- Loggers
- Configuration managers
- Database connection pools
- Caches
C# example (thread-safe implementation):
public sealed class ConfigurationManager{ private static readonly Lazy<ConfigurationManager> _instance = new Lazy<ConfigurationManager>(() => new ConfigurationManager());
private readonly Dictionary<string, string> _settings;
private ConfigurationManager() { _settings = new Dictionary<string, string> { { "ConnectionString", "Server=localhost;Database=myDb" }, { "ApiKey", "12345-ABCDE" } }; }
public static ConfigurationManager Instance => _instance.Value;
public string GetSetting(string key) { return _settings.TryGetValue(key, out var value) ? value : null; }}
// Usagevar config = ConfigurationManager.Instance;Console.WriteLine(config.GetSetting("ConnectionString"));Advantages:
- Controlled access to the single instance
- Lazy initialization (Lazy<T>)
- Thread safety without locks
2. Factory Method
Purpose: Defines an interface for creating an object but lets subclasses alter the type of object that will be created.
When to use:
- When the specific type of object is not known in advance
- When object creation requires complex logic
- To separate creation code from usage code
C# example:
// Abstract productpublic interface ILogger{ void Log(string message);}
// Concrete productspublic class FileLogger : ILogger{ public void Log(string message) => File.AppendAllText("log.txt", $"{DateTime.Now}: {message}");}
public class ConsoleLogger : ILogger{ public void Log(string message) => Console.WriteLine($"{DateTime.Now}: {message}");}
// Creator with factory methodpublic abstract class LoggerFactory{ public abstract ILogger CreateLogger(); public void LogMessage(string message) { var logger = CreateLogger(); logger.Log(message); }}
public class FileLoggerFactory : LoggerFactory{ public override ILogger CreateLogger() => new FileLogger();}
public class ConsoleLoggerFactory : LoggerFactory{ public override ILogger CreateLogger() => new ConsoleLogger();}
// UsageLoggerFactory factory = new ConsoleLoggerFactory();factory.LogMessage("Hello, world!");