Software Design Patterns are general, reusable solutions to common problems in software design. They are proven approaches to solving recurring design challenges, aimed at improving code quality, maintainability, and scalability. These patterns abstract the solution from the specific problem context, making them adaptable to a variety of situations.
Design patterns are divided into three main categories:
- Creational Patterns – Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
- Structural Patterns – Deal with the composition of classes and objects to form larger structures.
- Behavioral Patterns – Deal with how objects interact and communicate with each other.
Let’s go through the most commonly used design patterns:
1. Creational Design Patterns
These patterns abstract the instantiation process, allowing a system to be independent of how its objects are created.
a. Singleton Pattern
- Intent: Ensures that a class has only one instance, and provides a global point of access to that instance.
- Use Case: Managing a shared resource such as a connection pool or logging.
- Example:
public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton GetInstance() { if (instance == null) instance = new Singleton(); return instance; } }
b. Factory Method Pattern
- Intent: Defines an interface for creating an object, but allows subclasses to alter the type of objects that will be created.
- Use Case: When you need to create instances of several derived classes from a common superclass.
- Example:
public abstract class Product { public abstract void Use(); } public class ConcreteProduct : Product { public override void Use() { Console.WriteLine("Using the concrete product."); } } public abstract class Creator { public abstract Product FactoryMethod(); } public class ConcreteCreator : Creator { public override Product FactoryMethod() { return new ConcreteProduct(); } }
c. Abstract Factory Pattern
- Intent: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
- Use Case: Creating objects from different families that need to be interchangeable.
- Example:
public abstract class AbstractFactory { public abstract IProductA CreateProductA(); public abstract IProductB CreateProductB(); } public class ConcreteFactory1 : AbstractFactory { public override IProductA CreateProductA() { return new ProductA1(); } public override IProductB CreateProductB() { return new ProductB1(); } }
d. Builder Pattern
- Intent: Separates the construction of a complex object from its representation, so that the same construction process can create different representations.
- Use Case: Building complex objects step by step, like constructing a meal with different ingredients.
- Example:
public class Product { private List<string> parts = new List<string>(); public void Add(string part) { parts.Add(part); } } public abstract class Builder { protected Product product = new Product(); public abstract void BuildPart(); public Product GetResult() { return product; } } public class ConcreteBuilder : Builder { public override void BuildPart() { product.Add("Part 1"); } }
2. Structural Design Patterns
These patterns deal with object and class composition, providing ways to structure systems in a flexible and efficient manner.
a. Adapter Pattern
- Intent: Converts one interface to another expected by the client, enabling incompatible interfaces to work together.
- Use Case: When integrating with legacy code or third-party libraries.
- Example:
public class OldSystem { public void OldMethod() { Console.WriteLine("Old method."); } } public interface ITarget { void Request(); } public class Adapter : ITarget { private OldSystem oldSystem; public Adapter(OldSystem system) { oldSystem = system; } public void Request() { oldSystem.OldMethod(); } }
b. Composite Pattern
- Intent: Allows you to compose objects into tree-like structures to represent part-whole hierarchies. Used to treat individual objects and composites uniformly.
- Use Case: Representing hierarchies such as directories and files.
- Example:
public interface Component { void Operation(); } public class Leaf : Component { public void Operation() { Console.WriteLine("Leaf operation."); } } public class Composite : Component { private List<Component> children = new List<Component>(); public void Add(Component component) { children.Add(component); } public void Operation() { foreach (var child in children) { child.Operation(); } } }
c. Proxy Pattern
- Intent: Provides a surrogate or placeholder for another object. Controls access to it.
- Use Case: Lazy initialization or access control for expensive or resource-intensive objects.
- Example:
public interface ISubject { void Request(); } public class RealSubject : ISubject { public void Request() { Console.WriteLine("Real Subject Request."); } } public class Proxy : ISubject { private RealSubject realSubject; public void Request() { if (realSubject == null) { realSubject = new RealSubject(); } realSubject.Request(); } }
3. Behavioral Design Patterns
These patterns focus on the interaction and responsibility between objects, defining how objects communicate and collaborate.
a. Strategy Pattern
- Intent: Defines a family of algorithms and allows them to be interchangeable. The algorithm can be selected at runtime.
- Use Case: When there are multiple algorithms for a task and you want to choose one dynamically.
- Example:
public interface IStrategy { void Execute(); } public class ConcreteStrategyA : IStrategy { public void Execute() { Console.WriteLine("Strategy A"); } } public class Context { private IStrategy strategy; public Context(IStrategy strategy) { this.strategy = strategy; } public void SetStrategy(IStrategy strategy) { this.strategy = strategy; } public void ExecuteStrategy() { strategy.Execute(); } }
b. Observer Pattern
- Intent: Allows an object (subject) to notify a list of dependents (observers) when its state changes.
- Use Case: Event handling systems or notification services.
- Example:
public interface IObserver { void Update(); } public class ConcreteObserver : IObserver { public void Update() { Console.WriteLine("State changed!"); } } public class Subject { private List<IObserver> observers = new List<IObserver>(); public void Attach(IObserver observer) { observers.Add(observer); } public void Notify() { foreach (var observer in observers) { observer.Update(); } } }
c. Command Pattern
- Intent: Encapsulates a request as an object, allowing parameterization of clients with different requests.
- Use Case: Implementing undo/redo functionality or job queues.
- Example:
public interface ICommand { void Execute(); } public class ConcreteCommand : ICommand { private Receiver receiver; public ConcreteCommand(Receiver receiver) { this.receiver = receiver; } public void Execute() { receiver.Action(); } } public class Receiver { public void Action() { Console.WriteLine("Receiver Action."); } }
d. Template Method Pattern
- Intent: Defines the skeleton of an algorithm, allowing subclasses to provide specific implementations for certain steps.
- Use Case: Creating a general algorithm structure where subclasses provide specific details.
- Example:
public abstract class AbstractClass { public void TemplateMethod() { StepOne(); StepTwo(); } protected abstract void StepOne(); protected abstract void StepTwo(); } public class ConcreteClass : AbstractClass { protected override void StepOne() { Console.WriteLine("Step 1"); } protected override void StepTwo() { Console.WriteLine("Step 2"); } }
Summary:
Design patterns are vital tools in software engineering that help address common design challenges. By understanding and implementing these patterns, developers can write flexible, maintainable, and scalable software. Whether you’re solving issues related to object creation, structure, or communication, design patterns provide reliable, time-tested solutions.