Design patterns are proven solutions to common software challenges, providing guidance on writing clean, modular, and scalable code. In this article, we'll explore three popular design patterns in C#—Singleton, Factory, and Observer—through practical examples and real-world use cases. Whether you're new to design patterns or looking to refine your skills, these patterns will help you build more robust applications.
Table of Contents
- Introduction
- What Are Design Patterns?
- The Singleton Pattern
- The Factory Pattern
- The Observer Pattern
- Best Practices for Applying Design Patterns in C#
- Conclusion
Introduction
Design patterns are essential for developers aiming to write maintainable and scalable code. They offer time-tested solutions to common design challenges. In this article, we’ll demystify three design patterns in C# by sharing practical examples and explaining when and why to use them.
What Are Design Patterns?
Design patterns are general reusable solutions to common problems encountered in software design. They are not finished designs that can be transformed directly into code but rather templates on how to solve a problem in various contexts. Understanding these patterns helps improve code readability, maintainability, and flexibility.
The Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to that instance.
When to Use the Singleton Pattern
- To manage shared resources such as configurations or connection pools.
- When a single instance is sufficient to coordinate actions across the system.
- When global state needs to be accessed and maintained in a controlled way.
Singleton Pattern Code Example
public sealed class Logger
{
private static readonly Lazy<Logger> _instance = new Lazy<Logger>(() => new Logger());
// Private constructor to prevent instantiation
private Logger() { }
public static Logger Instance => _instance.Value;
public void Log(string message)
{
Console.WriteLine($"{DateTime.Now}: {message}");
}
}
// Usage
public class Program
{
public static void Main(string[] args)
{
Logger.Instance.Log("Singleton Pattern in action!");
}
}
In this example, the Logger
class cannot be instantiated more than once. The Lazy<T>
type ensures that the instance is created only when needed, and thread-safety is handled automatically.
The Factory Pattern
The Factory pattern provides a way to create objects without exposing the instantiation logic to the client. It uses a method to handle the instantiation, making it easy to introduce new types or modify existing ones without changing client code.
When to Use the Factory Pattern
- When the creation process is complex or involves conditional logic.
- To encapsulate object creation and decouple the client code from the concrete classes.
- When a system needs to work with various types of related objects.
Factory Pattern Code Example
// Define an interface for shapes
public interface IShape
{
void Draw();
}
// Concrete implementations for different shapes
public class Circle : IShape
{
public void Draw() => Console.WriteLine("Drawing a Circle");
}
public class Square : IShape
{
public void Draw() => Console.WriteLine("Drawing a Square");
}
// Factory class to create shapes
public static class ShapeFactory
{
public static IShape CreateShape(string shapeType)
{
return shapeType.ToLower() switch
{
"circle" => new Circle(),
"square" => new Square(),
_ => throw new ArgumentException("Invalid shape type")
};
}
}
// Usage
public class Program
{
public static void Main(string[] args)
{
IShape shape = ShapeFactory.CreateShape("circle");
shape.Draw();
shape = ShapeFactory.CreateShape("square");
shape.Draw();
}
}
This factory method pattern cleanly encapsulates the object creation logic, making the codebase easier to extend and maintain.
The Observer Pattern
The Observer pattern allows an object (the subject) to maintain a list of its dependents (observers) and notify them automatically of any state changes.
When to Use the Observer Pattern
- To implement distributed event handling systems.
- When a change in one object should trigger updates in multiple dependent objects.
- To decouple objects without tightly binding the subject and its observers.
Observer Pattern Code Example
using System;
using System.Collections.Generic;
// Subject
public class Stock
{
private List<IStockObserver> _observers = new List<IStockObserver>();
private decimal _price;
public string Symbol { get; }
public Stock(string symbol, decimal price)
{
Symbol = symbol;
_price = price;
}
public void Attach(IStockObserver observer) => _observers.Add(observer);
public void Detach(IStockObserver observer) => _observers.Remove(observer);
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
Notify();
}
}
}
private void Notify()
{
foreach (var observer in _observers)
{
observer.Update(this);
}
}
}
// Observer Interface
public interface IStockObserver
{
void Update(Stock stock);
}
// Concrete Observer
public class StockDisplay : IStockObserver
{
public void Update(Stock stock)
{
Console.WriteLine($"Stock {stock.Symbol}: New Price = {stock.Price}");
}
}
// Usage
public class Program
{
public static void Main(string[] args)
{
var stock = new Stock("AAPL", 150m);
var display = new StockDisplay();
stock.Attach(display);
// Changing the price notifies the observer
stock.Price = 155m;
}
}
This example shows how observers can subscribe to changes in a subject (e.g., a stock price) and get notified whenever the subject's state changes.
Best Practices for Applying Design Patterns in C
- Understand the Problem: Use patterns judiciously when they fit the problem; overusing design patterns might lead to unnecessary complexity.
- Keep It Simple: Avoid over-engineering. Use the simplest solution until requirements force a more complex design.
- Follow SOLID Principles: Ensure your design patterns align with SOLID principles to achieve greater maintainability and scalability.
- Refactor When Needed: Don't be afraid to refactor your code as requirements evolve and better design approaches emerge.
Conclusion
Design patterns can significantly enhance the quality of your C# applications by offering structured, scalable solutions to common programming challenges. The Singleton, Factory, and Observer patterns are valuable tools in any developer’s arsenal, each offering unique benefits in different scenarios. By understanding and applying these patterns with real-world examples, you can write cleaner, more maintainable, and robust code.
Happy coding! Feel free to share your thoughts, questions, or experiences with these design patterns in the comments below.
Top comments (1)
Nice