Design Patterns Explained in C#

Design patterns are crucial concepts in software engineering that help in solving common design issues in software development. They offer reusable solutions to recurring problems, ensuring that code is more modular, flexible, and maintainable. In this article, we will delve into design patterns in the context of C#, a language widely used for various applications from web development to enterprise solutions. We will explore the main types of design patterns, provide C# examples for each, and discuss how these patterns can improve code design and architecture.

What Are Design Patterns?

Design patterns are standardized solutions to common problems encountered in software design. They provide templates for how to solve these problems in various situations. By applying design patterns, developers can avoid reinventing the wheel and adhere to best practices that promote code reusability and maintainability.

Categories of Design Patterns

Design patterns can be broadly categorized into three types:

  1. Creational Patterns: Concerned with the process of object creation, these patterns help manage object creation mechanisms, making it more flexible and efficient.
  2. Structural Patterns: Deal with object composition and the way objects are composed to form larger structures. They help in ensuring that if one part of a system changes, the entire system doesn’t need to change.
  3. Behavioral Patterns: Focus on the interaction between objects and the responsibilities they carry out. These patterns help in defining how objects collaborate and distribute responsibilities.

In the following sections, we will explore each of these categories with detailed examples in C#.

Creational Patterns

Creational patterns are designed to handle object creation mechanisms. They abstract the instantiation process, making it easier to create objects while providing flexibility in how they are created. The main creational patterns include Singleton, Factory Method, Abstract Factory, Builder, and Prototype.

Singleton Pattern

Purpose: Ensure that a class has only one instance and provide a global point of access to that instance.

Implementation:

In C#, a Singleton is typically implemented using a static variable to hold the single instance and a private constructor to prevent direct instantiation.

public class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();

    // Private constructor to prevent instantiation
    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            // Double-check locking to ensure thread safety
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

Usage:

Singleton instance1 = Singleton.Instance;
Singleton instance2 = Singleton.Instance;

Console.WriteLine(object.ReferenceEquals(instance1, instance2)); // True

Advantages:

  • Controlled access to the single instance.
  • Reduced memory usage since only one instance is created.

Disadvantages:

  • Global state can lead to issues in multi-threaded environments if not properly synchronized.

Factory Method Pattern

Purpose: Define an interface for creating an object but let subclasses alter the type of objects that will be created.

Implementation:

public 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();
}

Usage:

Creator creator = new ConcreteCreatorA();
Product product = creator.FactoryMethod();
Console.WriteLine(product.GetName()); // Product A

creator = new ConcreteCreatorB();
product = creator.FactoryMethod();
Console.WriteLine(product.GetName()); // Product B

Advantages:

  • Promotes loose coupling by decoupling the code that creates objects from the code that uses them.
  • Allows for easy addition of new product types without modifying existing code.

Disadvantages:

  • Can lead to a proliferation of classes as new types are introduced.

Abstract Factory Pattern

Purpose: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Implementation:

public interface IAbstractFactory
{
    IProductA CreateProductA();
    IProductB CreateProductB();
}

public interface IProductA { }
public interface IProductB { }

public class ProductA1 : IProductA { }
public class ProductB1 : IProductB { }

public class ProductA2 : IProductA { }
public class ProductB2 : IProductB { }

public class ConcreteFactory1 : IAbstractFactory
{
    public IProductA CreateProductA() => new ProductA1();
    public IProductB CreateProductB() => new ProductB1();
}

public class ConcreteFactory2 : IAbstractFactory
{
    public IProductA CreateProductA() => new ProductA2();
    public IProductB CreateProductB() => new ProductB2();
}

Usage:

IAbstractFactory factory = new ConcreteFactory1();
IProductA productA = factory.CreateProductA();
IProductB productB = factory.CreateProductB();

// Use the products

Advantages:

  • Ensures consistency among products created by a factory.
  • Promotes the use of related product families.

Disadvantages:

  • Increases the number of classes in the application.

Builder Pattern

Purpose: Separate the construction of a complex object from its representation so that the same construction process can create different representations.

Implementation:

public class Product
{
    public string PartA { get; set; }
    public string PartB { get; set; }
}

public abstract class Builder
{
    public abstract void BuildPartA();
    public abstract void BuildPartB();
    public abstract Product GetResult();
}

public class ConcreteBuilder : Builder
{
    private Product _product = new Product();

    public override void BuildPartA() => _product.PartA = "Part A";
    public override void BuildPartB() => _product.PartB = "Part B";
    public override Product GetResult() => _product;
}

public class Director
{
    private Builder _builder;

    public Director(Builder builder)
    {
        _builder = builder;
    }

    public void Construct()
    {
        _builder.BuildPartA();
        _builder.BuildPartB();
    }
}

Usage:

Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.Construct();
Product product = builder.GetResult();

Console.WriteLine($"{product.PartA}, {product.PartB}"); // Part A, Part B

Advantages:

  • Allows for the creation of complex objects step by step.
  • Provides flexibility in the construction process and the final product.

Disadvantages:

  • Can result in a more complex code structure with multiple classes.

Prototype Pattern

Purpose: Specify the kinds of objects to create using a prototypical instance and create new objects by copying this prototype.

Implementation:

public abstract class Prototype
{
    public abstract Prototype Clone();
}

public class ConcretePrototype : Prototype
{
    public string Data { get; set; }

    public override Prototype Clone() => (Prototype)this.MemberwiseClone();
}

Usage:

ConcretePrototype prototype = new ConcretePrototype { Data = "Prototype Data" };
ConcretePrototype clone = (ConcretePrototype)prototype.Clone();

Console.WriteLine(clone.Data); // Prototype Data

Advantages:

  • Allows for the creation of objects without knowing their specific classes.
  • Provides an efficient way to create new objects.

Disadvantages:

  • Can be complex to implement if the prototype objects are complicated.

Structural Patterns

Structural patterns focus on how classes and objects are composed to form larger structures. They help ensure that if one part of a system changes, the entire system doesn’t need to change. The primary structural patterns include Adapter, Decorator, Proxy, Composite, Bridge, and Flyweight.

Adapter Pattern

Purpose: Convert the interface of a class into another interface clients expect. The Adapter pattern allows incompatible interfaces to work together.

Implementation:

public interface ITarget
{
    string Request();
}

public class Adaptee
{
    public string SpecificRequest() => "Specific Request";
}

public class Adapter : ITarget
{
    private Adaptee _adaptee;

    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    public string Request() => _adaptee.SpecificRequest();
}

Usage:

Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);

Console.WriteLine(target.Request()); // Specific Request

Advantages:

  • Allows integration of new functionality into existing code without modifying it.
  • Facilitates the use of existing code in new contexts.

Disadvantages:

  • Can introduce additional layers of complexity.

Decorator Pattern

Purpose: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Implementation:

public interface IComponent
{
    string Operation();
}

public class ConcreteComponent : IComponent
{
    public string Operation() => "Concrete Component";
}

public abstract class Decorator : IComponent
{
    protected IComponent _component;

    protected Decorator(IComponent component)
    {


 _component = component;
    }

    public abstract string Operation();
}

public class ConcreteDecorator : Decorator
{
    public ConcreteDecorator(IComponent component) : base(component) { }

    public override string Operation() => $"{_component.Operation()} + ConcreteDecorator";
}

Usage:

IComponent component = new ConcreteComponent();
IComponent decoratedComponent = new ConcreteDecorator(component);

Console.WriteLine(decoratedComponent.Operation()); // Concrete Component + ConcreteDecorator

Advantages:

  • Provides a flexible way to extend functionality.
  • Avoids the need for extensive subclassing.

Disadvantages:

  • Can result in a large number of small classes.

Proxy Pattern

Purpose: Provide a surrogate or placeholder for another object to control access to it. Proxies can manage access, perform lazy initialization, or add additional functionality.

Implementation:

public interface ISubject
{
    string Request();
}

public class RealSubject : ISubject
{
    public string Request() => "RealSubject Request";
}

public class Proxy : ISubject
{
    private RealSubject _realSubject;

    public string Request()
    {
        if (_realSubject == null)
        {
            _realSubject = new RealSubject();
        }
        return _realSubject.Request();
    }
}

Usage:

ISubject proxy = new Proxy();
Console.WriteLine(proxy.Request()); // RealSubject Request

Advantages:

  • Provides control over access to the real object.
  • Can add additional functionality such as lazy loading or access control.

Disadvantages:

  • Adds an extra layer of indirection.

Composite Pattern

Purpose: Compose objects into tree structures to represent part-whole hierarchies. The Composite pattern allows clients to treat individual objects and compositions of objects uniformly.

Implementation:

public interface IComponent
{
    string Operation();
}

public class Leaf : IComponent
{
    public string Operation() => "Leaf";
}

public class Composite : IComponent
{
    private List<IComponent> _children = new List<IComponent>();

    public void Add(IComponent component) => _children.Add(component);
    public void Remove(IComponent component) => _children.Remove(component);

    public string Operation()
    {
        return "Composite: " + string.Join(", ", _children.Select(c => c.Operation()));
    }
}

Usage:

IComponent leaf1 = new Leaf();
IComponent leaf2 = new Leaf();
Composite composite = new Composite();
composite.Add(leaf1);
composite.Add(leaf2);

Console.WriteLine(composite.Operation()); // Composite: Leaf, Leaf

Advantages:

  • Simplifies the client code by treating individual objects and compositions uniformly.
  • Facilitates the addition of new types of components.

Disadvantages:

  • Can make the design more complex due to the potential for deeply nested structures.

Bridge Pattern

Purpose: Decouple an abstraction from its implementation so that the two can vary independently. The Bridge pattern is used to separate abstraction from implementation.

Implementation:

public interface IImplementor
{
    string Operation();
}

public class ConcreteImplementorA : IImplementor
{
    public string Operation() => "ConcreteImplementorA";
}

public class ConcreteImplementorB : IImplementor
{
    public string Operation() => "ConcreteImplementorB";
}

public abstract class Abstraction
{
    protected IImplementor _implementor;

    protected Abstraction(IImplementor implementor)
    {
        _implementor = implementor;
    }

    public abstract string Operation();
}

public class RefinedAbstraction : Abstraction
{
    public RefinedAbstraction(IImplementor implementor) : base(implementor) { }

    public override string Operation() => $"RefinedAbstraction with {_implementor.Operation()}";
}

Usage:

IImplementor implementor = new ConcreteImplementorA();
Abstraction abstraction = new RefinedAbstraction(implementor);

Console.WriteLine(abstraction.Operation()); // RefinedAbstraction with ConcreteImplementorA

Advantages:

  • Provides flexibility in changing implementations independently from abstractions.
  • Reduces the number of classes required to implement a particular functionality.

Disadvantages:

  • Adds complexity due to the additional abstraction layers.

Flyweight Pattern

Purpose: Use sharing to support a large number of fine-grained objects efficiently. The Flyweight pattern reduces the cost of creating and managing a large number of objects by sharing common parts.

Implementation:

public interface IFlyweight
{
    void Operation(string extrinsicState);
}

public class ConcreteFlyweight : IFlyweight
{
    public void Operation(string extrinsicState) => Console.WriteLine($"ConcreteFlyweight: {extrinsicState}");
}

public class FlyweightFactory
{
    private Dictionary<string, IFlyweight> _flyweights = new Dictionary<string, IFlyweight>();

    public IFlyweight GetFlyweight(string key)
    {
        if (!_flyweights.ContainsKey(key))
        {
            _flyweights[key] = new ConcreteFlyweight();
        }
        return _flyweights[key];
    }
}

Usage:

FlyweightFactory factory = new FlyweightFactory();
IFlyweight flyweight1 = factory.GetFlyweight("A");
IFlyweight flyweight2 = factory.GetFlyweight("A");

flyweight1.Operation("State 1");
flyweight2.Operation("State 2");

Console.WriteLine(object.ReferenceEquals(flyweight1, flyweight2)); // True

Advantages:

  • Reduces memory usage by sharing objects.
  • Improves performance by reusing existing objects.

Disadvantages:

  • Complexity in managing shared objects.

Behavioral Patterns

Behavioral patterns focus on the interactions and responsibilities of objects. They help define how objects collaborate and how responsibilities are distributed. The main behavioral patterns include Observer, Strategy, Command, State, Iterator, Mediator, and Template Method.

Observer Pattern

Purpose: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Implementation:

public interface IObserver
{
    void Update(string state);
}

public class ConcreteObserver : IObserver
{
    public void Update(string state) => Console.WriteLine($"Observer notified with state: {state}");
}

public class Subject
{
    private List<IObserver> _observers = new List<IObserver>();
    private string _state;

    public void Attach(IObserver observer) => _observers.Add(observer);
    public void Detach(IObserver observer) => _observers.Remove(observer);

    public void Notify()
    {
        foreach (var observer in _observers)
        {
            observer.Update(_state);
        }
    }

    public string State
    {
        get => _state;
        set
        {
            _state = value;
            Notify();
        }
    }
}

Usage:

Subject subject = new Subject();
IObserver observer = new ConcreteObserver();
subject.Attach(observer);

subject.State = "New State"; // Observer notified with state: New State

Advantages:

  • Promotes loose coupling between the subject and observers.
  • Facilitates communication between objects without them being tightly coupled.

Disadvantages:

  • Can lead to a large number of updates and notifications if not managed carefully.

Strategy Pattern

Purpose: Define a family of algorithms, encapsulate each one, and make them interchangeable. The Strategy pattern allows a client to choose an algorithm without altering the code that uses it.

Implementation:

public interface IStrategy
{
    int Execute(int a, int b);
}

public class ConcreteStrategyAdd : IStrategy
{
    public int Execute(int a, int b) => a + b;
}

public class ConcreteStrategySubtract : 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);
}

Usage:

Context context = new Context(new ConcreteStrategyAdd());
Console.WriteLine(context.ExecuteStrategy(5, 3)); // 8

context = new Context(new ConcreteStrategySubtract());
Console.WriteLine(context.ExecuteStrategy(5, 3)); // 2

Advantages:

  • Provides flexibility in changing algorithms.
  • Avoids large conditional statements and hardcoded algorithms.

Disadvantages:

  • Can introduce multiple classes and interfaces.

Command Pattern

Purpose: Encapsulate a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations.

Implementation:

public interface ICommand
{
    void Execute();
}

public class ConcreteCommand : ICommand
{
    private Receiver _receiver;

    public ConcreteCommand(Receiver receiver)
    {
        _receiver = receiver;
    }

    public void Execute() => _receiver.Action();
}

public class Receiver
{
    public void Action() => Console.WriteLine("Action executed");
}

public class Invoker
{
    private ICommand _command;

    public void SetCommand(ICommand command) => _command = command;
    public void Invoke() => _command.Execute();
}

Usage:

Receiver receiver = new Receiver();
ICommand command = new ConcreteCommand(receiver);
Invoker invoker = new Invoker();
invoker.SetCommand(command);
invoker.Invoke(); // Action executed

Advantages:

  • Decouples the sender and receiver of a request.
  • Allows for the queuing and logging of requests.

**Disadvantages

**:

  • Can result in a large number of command classes.

State Pattern

Purpose: Allow an object to alter its behavior when its internal state changes. The State pattern allows an object to appear as if it has changed its class.

Implementation:

public interface IState
{
    void Handle(Context context);
}

public class ConcreteStateA : IState
{
    public void Handle(Context context)
    {
        Console.WriteLine("Handling state A");
        context.State = new ConcreteStateB();
    }
}

public class ConcreteStateB : IState
{
    public void Handle(Context context)
    {
        Console.WriteLine("Handling state B");
    }
}

public class Context
{
    private IState _state;

    public Context(IState state)
    {
        _state = state;
    }

    public IState State
    {
        get => _state;
        set => _state = value;
    }

    public void Request() => _state.Handle(this);
}

Usage:

Context context = new Context(new ConcreteStateA());
context.Request(); // Handling state A
context.Request(); // Handling state B

Advantages:

  • Provides a clean way to implement state-specific behavior.
  • Simplifies complex state-dependent code.

Disadvantages:

  • Can result in an increase in the number of classes.

Iterator Pattern

Purpose: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Implementation:

public interface IIterator<T>
{
    T Current { get; }
    bool MoveNext();
    void Reset();
}

public class ConcreteIterator<T> : IIterator<T>
{
    private List<T> _collection;
    private int _currentIndex = -1;

    public ConcreteIterator(List<T> collection)
    {
        _collection = collection;
    }

    public T Current => _collection[_currentIndex];

    public bool MoveNext()
    {
        if (_currentIndex < _collection.Count - 1)
        {
            _currentIndex++;
            return true;
        }
        return false;
    }

    public void Reset() => _currentIndex = -1;
}

public class Aggregate<T>
{
    private List<T> _items = new List<T>();

    public void Add(T item) => _items.Add(item);
    public IIterator<T> CreateIterator() => new ConcreteIterator<T>(_items);
}

Usage:

Aggregate<int> aggregate = new Aggregate<int>();
aggregate.Add(1);
aggregate.Add(2);
aggregate.Add(3);

IIterator<int> iterator = aggregate.CreateIterator();
while (iterator.MoveNext())
{
    Console.WriteLine(iterator.Current);
}

Advantages:

  • Provides a way to access elements without exposing their underlying structure.
  • Supports multiple traversals of the aggregate object.

Disadvantages:

  • Can introduce additional complexity and classes.

Mediator Pattern

Purpose: Define an object that encapsulates how a set of objects interact. The Mediator pattern promotes loose coupling by keeping objects from referring to each other explicitly.

Implementation:

public abstract class Mediator
{
    public abstract void Send(string message, Colleague colleague);
}

public class ConcreteMediator : Mediator
{
    private Colleague1 _colleague1;
    private Colleague2 _colleague2;

    public ConcreteMediator(Colleague1 colleague1, Colleague2 colleague2)
    {
        _colleague1 = colleague1;
        _colleague2 = colleague2;
        _colleague1.Mediator = this;
        _colleague2.Mediator = this;
    }

    public override void Send(string message, Colleague colleague)
    {
        if (colleague == _colleague1)
        {
            _colleague2.Notify(message);
        }
        else
        {
            _colleague1.Notify(message);
        }
    }
}

public abstract class Colleague
{
    public Mediator Mediator { get; set; }
}

public class Colleague1 : Colleague
{
    public void Send(string message) => Mediator.Send(message, this);
    public void Notify(string message) => Console.WriteLine($"Colleague1 received: {message}");
}

public class Colleague2 : Colleague
{
    public void Send(string message) => Mediator.Send(message, this);
    public void Notify(string message) => Console.WriteLine($"Colleague2 received: {message}");
}

Usage:

Colleague1 colleague1 = new Colleague1();
Colleague2 colleague2 = new Colleague2();
Mediator mediator = new ConcreteMediator(colleague1, colleague2);

colleague1.Send("Hello from Colleague1");
colleague2.Send("Hello from Colleague2");

Advantages:

  • Promotes loose coupling between components.
  • Centralizes communication logic.

Disadvantages:

  • Can become a bottleneck if not managed carefully.

Template Method Pattern

Purpose: Define the skeleton of an algorithm in a base class, allowing subclasses to override specific steps of the algorithm without changing its structure.

Implementation:

public abstract class AbstractClass
{
    public void TemplateMethod()
    {
        Step1();
        Step2();
        Step3();
    }

    protected abstract void Step1();
    protected abstract void Step2();

    private void Step3() => Console.WriteLine("Step3");
}

public class ConcreteClass : AbstractClass
{
    protected override void Step1() => Console.WriteLine("ConcreteClass Step1");
    protected override void Step2() => Console.WriteLine("ConcreteClass Step2");
}

Usage:

AbstractClass obj = new ConcreteClass();
obj.TemplateMethod();

// ConcreteClass Step1
// ConcreteClass Step2
// Step3

Advantages:

  • Defines a common algorithm while allowing customization of specific steps.
  • Promotes code reuse and reduces duplication.

Disadvantages:

  • Subclasses may need to follow a specific structure that can be rigid.

Conclusion

Design patterns are a fundamental aspect of software design, providing reusable solutions to common problems. In C#, these patterns enhance the flexibility, maintainability, and scalability of code. By understanding and applying design patterns such as Singleton, Factory Method, Adapter, Observer, and many others, developers can create more robust and well-architected software.

Each pattern has its own advantages and disadvantages, and the choice of pattern depends on the specific problem and context. By leveraging these patterns, developers can ensure that their code adheres to best practices and remains adaptable to future changes.

Leave a Reply