Software Design Patterns in C#: Examples and Best Practices
1. Singleton Pattern
Purpose: Ensure a class has only one instance and provide a global point of access to it.
Implementation:
csharppublic class Singleton { private static Singleton _instance; private static readonly object _lock = new object(); private Singleton() { } public static Singleton Instance { get { lock (_lock) { if (_instance == null) { _instance = new Singleton(); } return _instance; } } } }
Explanation: The Singleton
class contains a private static variable to hold the single instance and a private constructor to prevent instantiation from outside the class. The Instance
property uses double-checked locking to ensure that only one instance is created, even in multi-threaded environments.
Use Case: Useful for managing a single point of access to resources such as configuration settings, logging, or database connections.
2. Factory Method Pattern
Purpose: Define an interface for creating an object but let subclasses alter the type of objects that will be created.
Implementation:
csharppublic abstract class Product { public abstract string GetName(); } public class ConcreteProductA : Product { public override string GetName() => "Product A"; } public class ConcreteProductB : Product { public override string GetName() => "Product B"; } public abstract class Creator { public abstract Product FactoryMethod(); } public class ConcreteCreatorA : Creator { public override Product FactoryMethod() => new ConcreteProductA(); } public class ConcreteCreatorB : Creator { public override Product FactoryMethod() => new ConcreteProductB(); }
Explanation: The Creator
class defines the FactoryMethod
which is overridden by concrete creators to produce specific products. This pattern allows the instantiation of objects to be determined at runtime, promoting flexibility and extensibility.
Use Case: Ideal for scenarios where the exact type of object to be created is not known until runtime or when dealing with complex object creation logic.
3. Observer Pattern
Purpose: Define a dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Implementation:
csharpusing System; using System.Collections.Generic; public interface IObserver { void Update(string message); } public class ConcreteObserver : IObserver { public void Update(string message) => Console.WriteLine("Observer received message: " + message); } public class Subject { private List
_observers = new List (); public void Attach(IObserver observer) => _observers.Add(observer); public void Detach(IObserver observer) => _observers.Remove(observer); public void Notify(string message) { foreach (var observer in _observers) { observer.Update(message); } } }
Explanation: The Subject
class maintains a list of observers and provides methods to attach, detach, and notify observers. When the Notify
method is called, all attached observers receive the update.
Use Case: Useful in scenarios where multiple objects need to be updated based on changes in a single subject, such as in event handling systems or user interface frameworks.
4. Decorator Pattern
Purpose: Attach additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality.
Implementation:
csharppublic abstract class Component { public abstract string Operation(); } public class ConcreteComponent : Component { public override string Operation() => "ConcreteComponent"; } public abstract class Decorator : Component { protected Component _component; public Decorator(Component component) => _component = component; public override string Operation() => _component.Operation(); } public class ConcreteDecoratorA : Decorator { public ConcreteDecoratorA(Component component) : base(component) { } public override string Operation() => "ConcreteDecoratorA(" + base.Operation() + ")"; }
Explanation: The Decorator
class extends the functionality of the Component
class without modifying its code. ConcreteDecoratorA
adds additional behavior while delegating to the base component.
Use Case: Ideal for situations where objects need to be extended with new behavior at runtime, such as adding features to user interface elements or adding logging capabilities.
5. Strategy Pattern
Purpose: Define a family of algorithms, encapsulate each one, and make them interchangeable. The strategy pattern allows the algorithm to vary independently from clients that use it.
Implementation:
csharppublic interface IStrategy { int Execute(int a, int b); } public class ConcreteStrategyAdd : IStrategy { public int Execute(int a, int b) => a + b; } public class ConcreteStrategyMultiply : IStrategy { public int Execute(int a, int b) => a * b; } public class Context { private IStrategy _strategy; public Context(IStrategy strategy) => _strategy = strategy; public int ExecuteStrategy(int a, int b) => _strategy.Execute(a, b); }
Explanation: The Context
class uses an IStrategy
to execute a specific algorithm. The ConcreteStrategy
implementations define different algorithms that can be used interchangeably.
Use Case: Useful when an algorithm needs to be selected at runtime or when there are multiple variations of an algorithm that need to be implemented.
Conclusion
Design patterns are essential tools in software engineering that provide proven solutions to common design problems. In C#, these patterns can enhance code flexibility, maintainability, and scalability. By incorporating patterns like Singleton, Factory Method, Observer, Decorator, and Strategy into your C# projects, you can leverage best practices to create robust and adaptable applications.
Popular Comments
No Comments Yet