DEV Community

Udara Dananjaya
Udara Dananjaya

Posted on

Gang of Four (GoF) Design Patterns in C#: A Comprehensive Guide

Design patterns are reusable solutions to common problems in software design. They were popularized by the "Gang of Four" (GoF) in their seminal book Design Patterns: Elements of Reusable Object-Oriented Software. These patterns are divided into three categories: Creational, Structural, and Behavioral.

In this article, we'll explore all 23 GoF design patterns, with brief explanations and practical implementations in C#. Each example is kept simple for clarity, focusing on the core idea. We'll use console applications or basic classes to demonstrate the patterns. Feel free to copy-paste these into your IDE (like Visual Studio) to experiment.

Whether you're a beginner or an experienced developer, understanding these patterns will help you write more flexible, maintainable, and scalable code. Let's dive in!

1. Creational Design Patterns

These patterns focus on object creation mechanisms, providing flexibility in deciding which objects to instantiate.

Singleton Pattern

Ensures that a class has only one instance and provides a global point of access to it.

Use Case: Global instances like logging systems or database connection pools.

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

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

    public static Singleton Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new Singleton();
                }
                return _instance;
            }
        }
    }

    public void ShowMessage()
    {
        Console.WriteLine("Hello from Singleton!");
    }
}

// Usage
Singleton.Instance.ShowMessage();
Enter fullscreen mode Exit fullscreen mode

Factory Method Pattern

Defines an interface for creating an object but allows subclasses to alter the type of objects created.

Use Case: Delegating object creation when type is determined at runtime.

public abstract class Product { }

public class ConcreteProductA : Product { }
public class ConcreteProductB : Product { }

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.GetType().Name);  // Outputs: ConcreteProductA
Enter fullscreen mode Exit fullscreen mode

Abstract Factory Pattern

Provides an interface for creating families of related or dependent objects without specifying concrete classes.

Use Case: Creating sets of related objects, leaving instantiation to subclasses.

public interface IButton { void Render(); }
public interface ICheckbox { void Render(); }

public class WindowsButton : IButton { public void Render() => Console.WriteLine("Windows Button"); }
public class WindowsCheckbox : ICheckbox { public void Render() => Console.WriteLine("Windows Checkbox"); }

public class MacButton : IButton { public void Render() => Console.WriteLine("Mac Button"); }
public class MacCheckbox : ICheckbox { public void Render() => Console.WriteLine("Mac Checkbox"); }

public interface IGUIFactory
{
    IButton CreateButton();
    ICheckbox CreateCheckbox();
}

public class WindowsFactory : IGUIFactory
{
    public IButton CreateButton() => new WindowsButton();
    public ICheckbox CreateCheckbox() => new WindowsCheckbox();
}

public class MacFactory : IGUIFactory
{
    public IButton CreateButton() => new MacButton();
    public ICheckbox CreateCheckbox() => new MacCheckbox();
}

// Usage
IGUIFactory factory = new WindowsFactory();
factory.CreateButton().Render();  // Outputs: Windows Button
Enter fullscreen mode Exit fullscreen mode

Builder Pattern

Allows step-by-step construction of complex objects, isolating construction from the final object.

Use Case: Complex objects with many optional parameters.

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

public interface IBuilder
{
    void BuildPartA();
    void BuildPartB();
    void BuildPartC();
    Product GetProduct();
}

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

    public void BuildPartA() => _product.PartA = "PartA";
    public void BuildPartB() => _product.PartB = "PartB";
    public void BuildPartC() => _product.PartC = "PartC";

    public Product GetProduct()
    {
        Product result = _product;
        _product = new Product();  // Reset
        return result;
    }
}

public class Director
{
    private IBuilder _builder;

    public Director(IBuilder builder) => _builder = builder;

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

// Usage
IBuilder builder = new ConcreteBuilder();
Director director = new Director(builder);
director.Construct();
Product product = builder.GetProduct();
Console.WriteLine($"{product.PartA}, {product.PartB}, {product.PartC}");
Enter fullscreen mode Exit fullscreen mode

Prototype Pattern

Creates new objects by copying an existing prototype.

Use Case: Expensive or complex object creation.

using System;

public class Prototype : ICloneable
{
    public string Property { get; set; }

    public object Clone() => MemberwiseClone();  // Shallow clone
}

// Usage
Prototype original = new Prototype { Property = "Original" };
Prototype clone = (Prototype)original.Clone();
clone.Property = "Clone";
Console.WriteLine(original.Property);  // Outputs: Original
Enter fullscreen mode Exit fullscreen mode

2. Structural Design Patterns

These patterns deal with object composition for flexibility and scalability.

Adapter Pattern

Allows incompatible interfaces to work together via an adapter.

Use Case: Integrating legacy systems or third-party libraries.

public interface ITarget { void Request(); }

public class Adaptee { public void SpecificRequest() => Console.WriteLine("Specific Request"); }

public class Adapter : ITarget
{
    private Adaptee _adaptee = new Adaptee();

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

// Usage
ITarget target = new Adapter();
target.Request();  // Outputs: Specific Request
Enter fullscreen mode Exit fullscreen mode

Bridge Pattern

Decouples abstraction from implementation, allowing independent variation.

Use Case: Changing implementation without affecting abstraction.

public interface IImplementor { void OperationImpl(); }

public class ConcreteImplementorA : IImplementor { public void OperationImpl() => Console.WriteLine("Impl A"); }
public class ConcreteImplementorB : IImplementor { public void OperationImpl() => Console.WriteLine("Impl B"); }

public abstract class Abstraction
{
    protected IImplementor _implementor;

    protected Abstraction(IImplementor implementor) => _implementor = implementor;

    public abstract void Operation();
}

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

    public override void Operation() => _implementor.OperationImpl();
}

// Usage
Abstraction abstraction = new RefinedAbstraction(new ConcreteImplementorA());
abstraction.Operation();  // Outputs: Impl A
Enter fullscreen mode Exit fullscreen mode

Composite Pattern

Composes objects into tree structures for part-whole hierarchies.

Use Case: Hierarchical structures like file systems.

using System.Collections.Generic;

public abstract class Component
{
    public abstract void Operation();
}

public class Leaf : Component
{
    public override 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 Remove(Component component) => _children.Remove(component);

    public override void Operation()
    {
        Console.WriteLine("Composite Operation");
        foreach (var child in _children) child.Operation();
    }
}

// Usage
Composite composite = new Composite();
composite.Add(new Leaf());
composite.Operation();  // Outputs: Composite Operation \n Leaf Operation
Enter fullscreen mode Exit fullscreen mode

Decorator Pattern

Dynamically adds responsibilities to objects without modifying structure.

Use Case: Adding features flexibly, like to UI components.

public interface IComponent { void Operation(); }

public class ConcreteComponent : IComponent { public void Operation() => Console.WriteLine("Concrete Operation"); }

public abstract class Decorator : IComponent
{
    protected IComponent _component;

    protected Decorator(IComponent component) => _component = component;

    public abstract void Operation();
}

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

    public override void Operation()
    {
        _component.Operation();
        Console.WriteLine("Decorator Added");
    }
}

// Usage
IComponent decorated = new ConcreteDecorator(new ConcreteComponent());
decorated.Operation();  // Outputs: Concrete Operation \n Decorator Added
Enter fullscreen mode Exit fullscreen mode

Facade Pattern

Provides a simplified interface to a complex subsystem.

Use Case: Simplifying complex libraries.

public class SubsystemA { public void MethodA() => Console.WriteLine("Subsystem A"); }
public class SubsystemB { public void MethodB() => Console.WriteLine("Subsystem B"); }

public class Facade
{
    private SubsystemA _a = new SubsystemA();
    private SubsystemB _b = new SubsystemB();

    public void Operation()
    {
        _a.MethodA();
        _b.MethodB();
    }
}

// Usage
Facade facade = new Facade();
facade.Operation();  // Outputs: Subsystem A \n Subsystem B
Enter fullscreen mode Exit fullscreen mode

Flyweight Pattern

Reduces memory by sharing similar objects.

Use Case: Large numbers of similar objects, like in games.

using System.Collections.Generic;

public class Flyweight
{
    private string _sharedState;  // Intrinsic state

    public Flyweight(string sharedState) => _sharedState = sharedState;

    public void Operation(string uniqueState) => Console.WriteLine($"Shared: {_sharedState}, Unique: {uniqueState}");
}

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

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

// Usage
FlyweightFactory factory = new FlyweightFactory();
factory.GetFlyweight("Shared1").Operation("Unique1");
Enter fullscreen mode Exit fullscreen mode

Proxy Pattern

Provides a surrogate for another object.

Use Case: Lazy loading, access control.

public interface ISubject { void Request(); }

public class RealSubject : ISubject { public void Request() => Console.WriteLine("Real Request"); }

public class Proxy : ISubject
{
    private RealSubject _realSubject;

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

// Usage
ISubject proxy = new Proxy();
proxy.Request();  // Outputs: Real Request (lazy loaded)
Enter fullscreen mode Exit fullscreen mode

3. Behavioral Design Patterns

These patterns handle object interaction and communication.

Chain of Responsibility Pattern

Allows multiple handlers to process a request.

Use Case: Chains for handling requests or events.

public abstract class Handler
{
    protected Handler _next;

    public void SetNext(Handler next) => _next = next;

    public abstract void HandleRequest(int request);
}

public class ConcreteHandlerA : Handler
{
    public override void HandleRequest(int request)
    {
        if (request < 10) Console.WriteLine("Handled by A");
        else _next?.HandleRequest(request);
    }
}

public class ConcreteHandlerB : Handler
{
    public override void HandleRequest(int request)
    {
        if (request >= 10) Console.WriteLine("Handled by B");
        else _next?.HandleRequest(request);
    }
}

// Usage
Handler handlerA = new ConcreteHandlerA();
Handler handlerB = new ConcreteHandlerB();
handlerA.SetNext(handlerB);
handlerA.HandleRequest(5);  // Outputs: Handled by A
Enter fullscreen mode Exit fullscreen mode

Command Pattern

Encapsulates requests as objects for parameterization, queuing, etc.

Use Case: Undo/redo, menu actions.

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("Receiver Action"); }

public class Invoker
{
    private ICommand _command;

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

// Usage
Receiver receiver = new Receiver();
ICommand command = new ConcreteCommand(receiver);
Invoker invoker = new Invoker();
invoker.SetCommand(command);
invoker.ExecuteCommand();  // Outputs: Receiver Action
Enter fullscreen mode Exit fullscreen mode

Interpreter Pattern

Evaluates language grammar or expressions.

Use Case: Parsing expressions like math or regex.

using System.Collections.Generic;

public interface IExpression { int Interpret(Dictionary<string, int> context); }

public class Number : IExpression
{
    private int _number;
    public Number(int number) => _number = number;
    public int Interpret(Dictionary<string, int> context) => _number;
}

public class Add : IExpression
{
    private IExpression _left, _right;
    public Add(IExpression left, IExpression right) { _left = left; _right = right; }
    public int Interpret(Dictionary<string, int> context) => _left.Interpret(context) + _right.Interpret(context);
}

// Usage (simple example: 1 + 2)
IExpression expression = new Add(new Number(1), new Number(2));
Console.WriteLine(expression.Interpret(new Dictionary<string, int>()));  // Outputs: 3
Enter fullscreen mode Exit fullscreen mode

Iterator Pattern

Accesses collection elements sequentially without exposing representation.

Use Case: Iterating lists or sets consistently.

using System.Collections;
using System.Collections.Generic;

public class Aggregate : IEnumerable<string>
{
    private List<string> _items = new List<string> { "Item1", "Item2" };

    public IEnumerator<string> GetEnumerator() => _items.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// Usage
Aggregate aggregate = new Aggregate();
foreach (var item in aggregate) Console.WriteLine(item);  // Outputs: Item1 \n Item2
Enter fullscreen mode Exit fullscreen mode

Mediator Pattern

Centralizes communication between objects to reduce dependencies.

Use Case: Centralizing object interactions.

public interface IMediator { void Notify(object sender, string ev); }

public class ConcreteMediator : IMediator
{
    public Colleague1 Colleague1 { get; set; }
    public Colleague2 Colleague2 { get; set; }

    public void Notify(object sender, string ev)
    {
        if (ev == "A") Colleague2.DoSomething();
        else if (ev == "B") Colleague1.DoSomething();
    }
}

public abstract class Colleague
{
    protected IMediator _mediator;
    protected Colleague(IMediator mediator) => _mediator = mediator;
}

public class Colleague1 : Colleague
{
    public Colleague1(IMediator mediator) : base(mediator) { }
    public void DoA() => _mediator.Notify(this, "A");
    public void DoSomething() => Console.WriteLine("Colleague1 reacts");
}

public class Colleague2 : Colleague
{
    public Colleague2(IMediator mediator) : base(mediator) { }
    public void DoB() => _mediator.Notify(this, "B");
    public void DoSomething() => Console.WriteLine("Colleague2 reacts");
}

// Usage
ConcreteMediator mediator = new ConcreteMediator();
Colleague1 c1 = new Colleague1(mediator);
Colleague2 c2 = new Colleague2(mediator);
mediator.Colleague1 = c1;
mediator.Colleague2 = c2;
c1.DoA();  // Outputs: Colleague2 reacts
Enter fullscreen mode Exit fullscreen mode

Memento Pattern

Captures and restores object state without violating encapsulation.

Use Case: Undo functionality.

public class Memento
{
    public string State { get; }

    public Memento(string state) => State = state;
}

public class Originator
{
    public string State { get; set; }

    public Memento Save() => new Memento(State);
    public void Restore(Memento memento) => State = memento.State;
}

public class Caretaker
{
    public Memento Memento { get; set; }
}

// Usage
Originator originator = new Originator { State = "State1" };
Caretaker caretaker = new Caretaker { Memento = originator.Save() };
originator.State = "State2";
originator.Restore(caretaker.Memento);
Console.WriteLine(originator.State);  // Outputs: State1
Enter fullscreen mode Exit fullscreen mode

Observer Pattern

Defines one-to-many dependency for notifying dependents of changes.

Use Case: Event listeners in GUIs.

using System.Collections.Generic;

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

public class Subject
{
    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(string message)
    {
        foreach (var observer in _observers) observer.Update(message);
    }
}

public class ConcreteObserver : IObserver
{
    public void Update(string message) => Console.WriteLine($"Received: {message}");
}

// Usage
Subject subject = new Subject();
ConcreteObserver observer = new ConcreteObserver();
subject.Attach(observer);
subject.Notify("Update!");  // Outputs: Received: Update!
Enter fullscreen mode Exit fullscreen mode

State Pattern

Changes object behavior based on internal state.

Use Case: Finite-state machines.

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

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

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

public class Context
{
    public IState State { get; set; }

    public Context(IState state) => State = state;

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

// Usage
Context context = new Context(new ConcreteStateA());
context.Request();  // Outputs: State A
context.Request();  // Outputs: State B
Enter fullscreen mode Exit fullscreen mode

Strategy Pattern

Defines a family of algorithms, selectable at runtime.

Use Case: Runtime algorithm selection, like sorting.

public interface IStrategy { void Algorithm(); }

public class ConcreteStrategyA : IStrategy { public void Algorithm() => Console.WriteLine("Strategy A"); }
public class ConcreteStrategyB : IStrategy { public void Algorithm() => Console.WriteLine("Strategy B"); }

public class Context
{
    private IStrategy _strategy;

    public Context(IStrategy strategy) => _strategy = strategy;

    public void Execute() => _strategy.Algorithm();
}

// Usage
Context context = new Context(new ConcreteStrategyA());
context.Execute();  // Outputs: Strategy A
Enter fullscreen mode Exit fullscreen mode

Template Method Pattern

Defines algorithm skeleton in superclass, with customizable steps in subclasses.

Use Case: Common processes with customizable steps.

public abstract class AbstractClass
{
    public void TemplateMethod()
    {
        PrimitiveOperation1();
        PrimitiveOperation2();
    }

    protected abstract void PrimitiveOperation1();
    protected abstract void PrimitiveOperation2();
}

public class ConcreteClass : AbstractClass
{
    protected override void PrimitiveOperation1() => Console.WriteLine("Op1");
    protected override void PrimitiveOperation2() => Console.WriteLine("Op2");
}

// Usage
AbstractClass concrete = new ConcreteClass();
concrete.TemplateMethod();  // Outputs: Op1 \n Op2
Enter fullscreen mode Exit fullscreen mode

Visitor Pattern

Adds new operations to object structures without changing classes.

Use Case: Adding operations to composites.

using System.Collections.Generic;

public interface IVisitor { void Visit(ElementA element); void Visit(ElementB element); }

public abstract class Element { public abstract void Accept(IVisitor visitor); }

public class ElementA : Element { public override void Accept(IVisitor visitor) => visitor.Visit(this); }
public class ElementB : Element { public override void Accept(IVisitor visitor) => visitor.Visit(this); }

public class ConcreteVisitor : IVisitor
{
    public void Visit(ElementA element) => Console.WriteLine("Visited A");
    public void Visit(ElementB element) => Console.WriteLine("Visited B");
}

public class ObjectStructure
{
    private List<Element> _elements = new List<Element>();

    public void Add(Element element) => _elements.Add(element);

    public void Accept(IVisitor visitor)
    {
        foreach (var element in _elements) element.Accept(visitor);
    }
}

// Usage
ObjectStructure structure = new ObjectStructure();
structure.Add(new ElementA());
structure.Add(new ElementB());
IVisitor visitor = new ConcreteVisitor();
structure.Accept(visitor);  // Outputs: Visited A \n Visited B
Enter fullscreen mode Exit fullscreen mode

Conclusion

We've covered all 23 GoF design patterns with C# implementations. These patterns aren't silver bullets—use them where they fit to avoid over-engineering. Practice by applying them to real projects, like refactoring legacy code or building new systems.

Top comments (0)