Design Patterns with C# Examples

Introduction

Design patterns are tried and tested solutions to common problems in software design. They provide standard terminology and are specific to particular scenarios, making them highly useful for developers. In this article, we'll explore several design patterns and see how they can be implemented in C#.

1. Creational Patterns

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

1.1 Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
    public static Singleton Instance { get { return lazy.Value; } }
    private Singleton()
    {
    }
}
// Usage
var singletonInstance = Singleton.Instance;

1.2 Factory Method Pattern

The Factory Method pattern defines an interface for creating an object but allows subclasses to alter the type of objects that will be created.

public abstract class Product
{
    public abstract string Operation();
}
public class ConcreteProductA : Product
{
    public override string Operation()
    {
        return "ConcreteProductA";
    }
}
public class ConcreteProductB : Product
{
    public override string Operation()
    {
        return "ConcreteProductB";
    }
}
public abstract class Creator
{
    public abstract Product FactoryMethod();
    public string SomeOperation()
    {
        var product = FactoryMethod();
        return "Creator: The same creator's code has just worked with " + product.Operation();
    }
}
public class ConcreteCreatorA : Creator
{
    public override Product FactoryMethod()
    {
        return new ConcreteProductA();
    }
}
public class ConcreteCreatorB : Creator
{
    public override Product FactoryMethod()
    {
        return new ConcreteProductB();
    }
}
// Usage
var creatorA = new ConcreteCreatorA();
Console.WriteLine(creatorA.SomeOperation());
var creatorB = new ConcreteCreatorB();
Console.WriteLine(creatorB.SomeOperation());

2. Structural Patterns

Structural patterns concern class and object composition. They use inheritance to compose interfaces and define ways to compose objects to obtain new functionalities.

2.1 Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

public interface ITarget
{
    string GetRequest();
}
public class Adaptee
{
    public string GetSpecificRequest()
    {
        return "Specific request";
    }
}
public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;
    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }
    public string GetRequest()
    {
        return $"This is '{_adaptee.GetSpecificRequest()}'";
    }
}
// Usage
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);
Console.WriteLine(target.GetRequest());

2.2 Composite Pattern

The Composite pattern lets clients treat individual objects and compositions of objects uniformly.

public abstract class Component
{
    public abstract string Operation();
}
public class Leaf : Component
{
    public override string Operation()
    {
        return "Leaf";
    }
}
public class Composite : Component
{
    protected List<Component> _children = new List<Component>();
    public void Add(Component component)
    {
        _children.Add(component);
    }
    public void Remove(Component component)
    {
        _children.Remove(component);
    }
    public override string Operation()
    {
        int i = 0;
        string result = "Branch(";
        foreach (Component component in _children)
        {
            result += component.Operation();
            if (i != _children.Count - 1)
            {
                result += "+";
            }
            i++;
        }

        return result + ")";
    }
}
// Usage
var leaf = new Leaf();
var composite = new Composite();
composite.Add(leaf);
var composite1 = new Composite();
composite1.Add(composite);
composite1.Add(new Leaf());
Console.WriteLine(composite1.Operation());

3. Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.

3.1 Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that all its dependents are notified and updated automatically when one object changes state.

public interface IObserver
{
    void Update(ISubject subject);
}
public interface ISubject
{
    void Attach(IObserver observer);
    void Detach(IObserver observer);
    void Notify();
}
public class Subject : ISubject
{
    public int State { get; set; } = -0;
    private List<IObserver> _observers = new List<IObserver>();
    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(this);
        }
    }
    public void SomeBusinessLogic()
    {
        Console.WriteLine("Subject: I'm doing something important.");
        this.State = new Random().Next(0, 10);
        Console.WriteLine($"Subject: My state has just changed to: {this.State}");
        this.Notify();
    }
}
public class ConcreteObserverA : IObserver
{
    public void Update(ISubject subject)
    {
        if ((subject as Subject).State < 3)
        {
            Console.WriteLine("ConcreteObserverA: Reacted to the event.");
        }
    }
}
public class ConcreteObserverB : IObserver
{
    public void Update(ISubject subject)
    {
        if ((subject as Subject).State == 0 || (subject as Subject).State >= 2)
        {
            Console.WriteLine("ConcreteObserverB: Reacted to the event.");
        }
    }
}
// Usage
var subject = new Subject();
var observerA = new ConcreteObserverA();
subject.Attach(observerA);
var observerB = new ConcreteObserverB();
subject.Attach(observerB);
subject.SomeBusinessLogic();
subject.SomeBusinessLogic();

3.2 Strategy Pattern

The Strategy pattern enables selecting an algorithm's behavior at runtime.

public interface IStrategy
{
    object DoAlgorithm(object data);
}
public class Context
{
    private IStrategy _strategy;
    public Context()
    { }
    public Context(IStrategy strategy)
    {
        this._strategy = strategy;
    }
    public void SetStrategy(IStrategy strategy)
    {
        this._strategy = strategy;
    }
    public void DoSomeBusinessLogic()
    {
        Console.WriteLine("Context: Sorting data using the strategy (not sure how it'll do it)");
        var result = this._strategy.DoAlgorithm(new List<string> { "a", "b", "c", "d", "e" });
        string resultStr = string.Empty;
        foreach (var element in result as List<string>)
        {
            resultStr += element + ",";
        }
        Console.WriteLine(resultStr);
    }
}
public class ConcreteStrategyA : IStrategy
{
    public object DoAlgorithm(object data)
    {
        var list = data as List<string>;
        list.Sort();

        return list;
    }
}
public class ConcreteStrategyB : IStrategy
{
    public object DoAlgorithm(object data)
    {
        var list = data as List<string>;
        list.Sort();
        list.Reverse();

        return list;
    }
}
// Usage
var context = new Context();
Console.WriteLine("Client: Strategy is set to normal sorting.");
context.SetStrategy(new ConcreteStrategyA());
context.DoSomeBusinessLogic();
Console.WriteLine();
Console.WriteLine("Client: Strategy is set to reverse sorting.");
context.SetStrategy(new ConcreteStrategyB());
context.DoSomeBusinessLogic();

Conclusion

Design patterns are essential tools for developers. They help in creating more flexible, reusable, and maintainable code. This article covered some of the most common design patterns and provided C# examples. Understanding and applying these patterns can greatly enhance your software design skills and contribute to more efficient development processes.


Similar Articles