Design Patterns in a nutshell

rahul sahay
12 min readJun 12, 2023

--

The Gang of Four (GoF) design patterns are a collection of 23 software design patterns that were first introduced in the book “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, commonly known as the Gang of Four. These patterns provide solutions to recurring design problems in object-oriented software development. Let’s discuss some of the key Gang of Four design patterns:

  1. Creational Patterns:
  • Singleton: Ensures a class has only one instance and provides a global point of access to it.
  • Factory Method: Defines an interface for creating objects but lets subclasses decide which class to instantiate.
  • Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
  • Builder: Separates the construction of complex objects from their representation, allowing the same construction process to create various representations.
  • Prototype: Creates new objects by cloning existing ones, allowing the creation of new instances without knowing their specific classes.
  1. Structural Patterns:
  • Adapter: Converts the interface of a class into another interface that clients expect, allowing classes with incompatible interfaces to work together.
  • Composite: Composes objects into tree structures to represent part-whole hierarchies, making individual objects and compositions of objects interchangeable.
  • Proxy: Provides a surrogate or placeholder for another object to control access to it.
  • Decorator: Dynamically adds responsibilities to an object by wrapping it with one or more decorators, providing a flexible alternative to subclassing for extending functionality.
  • Facade: Provides a simplified interface to a subsystem of classes, acting as a higher-level interface that makes the subsystem easier to use.
  1. Behavioral Patterns:
  • Observer: Defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are notified and updated automatically.
  • Strategy: Defines a family of interchangeable algorithms and encapsulates each one, allowing them to be used interchangeably within a context.
  • Template Method: Defines the skeleton of an algorithm in a base class, allowing subclasses to provide specific implementations of certain steps of the algorithm.
  • Command: Encapsulates a request as an object, allowing parameterization of clients with different requests, queueing or logging requests, and supporting undoable operations.
  • Iterator: Provides a way to access elements of an aggregate object sequentially without exposing its underlying representation.

These are just a few examples of the Gang of Four design patterns. Each pattern addresses a specific problem and provides a proven solution that promotes code reusability, flexibility, and maintainability in software development. It’s important to note that design patterns should be used judiciously, as applying them blindly or inappropriately can lead to unnecessary complexity in code.

Now, let’s go ahead and look at all these patterns with examples.

Singleton: The Singleton pattern ensures that only one instance of a class is created and provides a global point of access to it. The Singleton class has a private constructor to prevent external instantiation, and the static Instance property ensures that only one instance of the class is created and returned.

public class Singleton
{
private static Singleton instance;

private Singleton() { }

public static Singleton Instance
{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}

Factory Method:

The Factory Method pattern defines an interface for creating objects, but delegates the responsibility of instantiation to subclasses. In this example, the Creator abstract class declares the FactoryMethod() that returns a Product. The ConcreteCreatorA and ConcreteCreatorB subclasses override this method to instantiate and return specific ConcreteProductA and ConcreteProductB objects, respectively.

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

public class ConcreteProductA : Product
{
public override void Operation()
{
Console.WriteLine("ConcreteProductA operation");
}
}

public class ConcreteProductB : Product
{
public override void Operation()
{
Console.WriteLine("ConcreteProductB operation");
}
}

public abstract class Creator
{
public abstract Product FactoryMethod();
}

public class ConcreteCreatorA : Creator
{
public override Product FactoryMethod()
{
return new ConcreteProductA();
}
}

public class ConcreteCreatorB : Creator
{
public override Product FactoryMethod()
{
return new ConcreteProductB();
}
}

Abstract Factory:

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. The IAbstractFactory interface declares factory methods for creating IAbstractProductA and IAbstractProductB objects. The ConcreteFactory1 and ConcreteFactory2 classes implement this interface and provide specific implementations for creating ConcreteProductA and ConcreteProductB objects.

public interface IAbstractProductA
{
void OperationA();
}

public interface IAbstractProductB
{
void OperationB();
}

public class ConcreteProductA1 : IAbstractProductA
{
public void OperationA()
{
Console.WriteLine("ConcreteProductA1 operation");
}
}

public class ConcreteProductA2 : IAbstractProductA
{
public void OperationA()
{
Console.WriteLine("ConcreteProductA2 operation");
}
}

public class ConcreteProductB1 : IAbstractProductB
{
public void OperationB()
{
Console.WriteLine("ConcreteProductB1 operation");
}
}

public class ConcreteProductB2 : IAbstractProductB
{
public void OperationB()
{
Console.WriteLine("ConcreteProductB2 operation");
}
}

public interface IAbstractFactory
{
IAbstractProductA CreateProductA();
IAbstractProductB CreateProductB();
}

public class ConcreteFactory1 : IAbstractFactory
{
public IAbstractProductA CreateProductA()
{
return new ConcreteProductA1();
}

public IAbstractProductB CreateProductB()
{
return new ConcreteProductB1();
}
}

public class ConcreteFactory2 : IAbstractFactory
{
public IAbstractProductA CreateProductA()
{
return new ConcreteProductA2();
}

public IAbstractProductB CreateProductB()
{
return new ConcreteProductB2();
}
}

Builder:

The Builder pattern separates the construction of complex objects from their representation, allowing the same construction process to create different representations. The Builder abstract class declares methods for building different parts of a Product, and the concrete builders (ConcreteBuilder1 and ConcreteBuilder2) implement these methods to construct specific parts of the Product. The Director class orchestrates the building process by calling the builder's methods in a specific order.

public class Product
{
private string partA;
private string partB;
private string partC;

public void SetPartA(string partA)
{
this.partA = partA;
}

public void SetPartB(string partB)
{
this.partB = partB;
}

public void SetPartC(string partC)
{
this.partC = partC;
}

public void Show()
{
Console.WriteLine($"Product Parts: {partA}, {partB}, {partC}");
}
}

public abstract class Builder
{
protected Product product;

public void CreateProduct()
{
product = new Product();
}

public abstract void BuildPartA();
public abstract void BuildPartB();
public abstract void BuildPartC();

public Product GetProduct()
{
return product;
}
}

public class ConcreteBuilder1 : Builder
{
public override void BuildPartA()
{
product.SetPartA("PartA1");
}

public override void BuildPartB()
{
product.SetPartB("PartB1");
}

public override void BuildPartC()
{
product.SetPartC("PartC1");
}
}

public class ConcreteBuilder2 : Builder
{
public override void BuildPartA()
{
product.SetPartA("PartA2");
}

public override void BuildPartB()
{
product.SetPartB("PartB2");
}

public override void BuildPartC()
{
product.SetPartC("PartC2");
}
}

public class Director
{
public void Construct(Builder builder)
{
builder.CreateProduct();
builder.BuildPartA();
builder.BuildPartB();
builder.BuildPartC();
}
}

Prototype:

The Prototype pattern allows objects to be copied or cloned, creating new instances without knowing their specific classes. The Prototype abstract class defines the Clone() method, which subclasses (ConcretePrototypeA and ConcretePrototypeB) override to create and return copies of themselves.

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

public class ConcretePrototypeA : Prototype
{
public string PropertyA { get; set; }

public override Prototype Clone()
{
return (Prototype)MemberwiseClone();
}
}

public class ConcretePrototypeB : Prototype
{
public int PropertyB { get; set; }

public override Prototype Clone()
{
return (Prototype)MemberwiseClone();
}
}

Structural Pattern:

Adapter: The Adapter pattern allows objects with incompatible interfaces to work together by providing a common interface. It acts as a bridge between two incompatible classes. In the following example, the Adaptee class has a different interface from what the Target interface expects. The Adapter class wraps the Adaptee and implements the Target interface, allowing the client to interact with the Adaptee through the Target interface.

public interface Target
{
void Request();
}

public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Adaptee specific request");
}
}

public class Adapter : Target
{
private Adaptee adaptee;

public Adapter(Adaptee adaptee)
{
this.adaptee = adaptee;
}

public void Request()
{
adaptee.SpecificRequest();
}
}

Composite:

The Composite pattern represents a part-whole hierarchy of objects as a tree structure, where both individual objects and groups of objects can be treated uniformly. In the following example, the Component abstract class defines the common interface for leaf objects and composites. The Leaf class represents individual objects, and the Composite class represents a group of objects. The Composite class can contain other Component objects, allowing nesting of the tree structure.

public abstract class Component
{
public string Name { get; set; }

public Component(string name)
{
Name = name;
}

public abstract void Operation();
}

public class Leaf : Component
{
public Leaf(string name) : base(name)
{
}

public override void Operation()
{
Console.WriteLine($"Leaf {Name} operation");
}
}

public class Composite : Component
{
private List<Component> components = new List<Component>();

public Composite(string name) : base(name)
{
}

public void Add(Component component)
{
components.Add(component);
}

public void Remove(Component component)
{
components.Remove(component);
}

public override void Operation()
{
Console.WriteLine($"Composite {Name} operation");
foreach (Component component in components)
{
component.Operation();
}
}
}

Proxy:

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It allows the proxy object to perform additional tasks before or after the underlying object is accessed. In the following example, the Subject interface declares the common interface for the real subject and proxy. The RealSubject class represents the actual object, and the Proxy class acts as a proxy for the RealSubject, intercepting and forwarding the requests to it while providing additional functionality if needed.

public interface Subject
{
void Request();
}

public class RealSubject : Subject
{
public void Request()
{
Console.WriteLine("RealSubject request");
}
}

public class Proxy : Subject
{
private RealSubject realSubject;

public Proxy()
{
realSubject = new RealSubject();
}

public void Request()
{
// Perform additional operations before forwarding the request
Console.WriteLine("Proxy pre-processing");

// Forward the request to the RealSubject
realSubject.Request();

// Perform additional operations after forwarding the request
Console.WriteLine("Proxy post-processing");
}
}

Decorator:

The Decorator pattern dynamically adds responsibilities to an object by wrapping it with one or more decorator objects. It provides a flexible alternative to subclassing for extending the functionality of objects at runtime. In the following example, the Component abstract class defines the common interface for both the concrete component (ConcreteComponent) and decorators (Decorator). The Decorator class wraps a component object and adds additional behavior before or after calling the component's methods.

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

public class ConcreteComponent : Component
{
public override void Operation()
{
Console.WriteLine("ConcreteComponent operation");
}
}

public abstract class Decorator : Component
{
protected Component component;

public Decorator(Component component)
{
this.component = component;
}

public override void Operation()
{
component.Operation();
}
}

public class ConcreteDecoratorA : Decorator
{
public ConcreteDecoratorA(Component component) : base(component)
{
}

public override void Operation()
{
// Perform additional operations before or after calling the wrapped component's Operation()
Console.WriteLine("ConcreteDecoratorA pre-processing");
base.Operation();
Console.WriteLine("ConcreteDecoratorA post-processing");
}
}

public class ConcreteDecoratorB : Decorator
{
public ConcreteDecoratorB(Component component) : base(component)
{
}

public override void Operation()
{
// Perform additional operations before or after calling the wrapped component's Operation()
Console.WriteLine("ConcreteDecoratorB pre-processing");
base.Operation();
Console.WriteLine("ConcreteDecoratorB post-processing");
}
}

Facade:

The Facade pattern provides a simplified interface to a subsystem of classes, acting as a higher-level interface that makes the subsystem easier to use. It hides the complexity of the subsystem and provides a unified interface for clients. In the following example, the SubsystemA, SubsystemB, and SubsystemC classes represent a subsystem of related classes. The Facade class provides a simplified interface that delegates the requests to the appropriate classes within the subsystem.

public class SubsystemA
{
public void OperationA()
{
Console.WriteLine("SubsystemA operation");
}
}

public class SubsystemB
{
public void OperationB()
{
Console.WriteLine("SubsystemB operation");
}
}

public class SubsystemC
{
public void OperationC()
{
Console.WriteLine("SubsystemC operation");
}
}

public class Facade
{
private SubsystemA subsystemA;
private SubsystemB subsystemB;
private SubsystemC subsystemC;

public Facade()
{
subsystemA = new SubsystemA();
subsystemB = new SubsystemB();
subsystemC = new SubsystemC();
}

public void Operation()
{
subsystemA.OperationA();
subsystemB.OperationB();
subsystemC.OperationC();
}
}

Behavioral Pattern:

Observer:

The Observer pattern defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are notified and updated automatically. In the following example, the Subject class represents the object being observed, and the Observer class represents the observers. The Subject maintains a list of observers and notifies them when its state changes.

public abstract class Subject
{
private List<Observer> observers = new List<Observer>();

public void Attach(Observer observer)
{
observers.Add(observer);
}

public void Detach(Observer observer)
{
observers.Remove(observer);
}

public void Notify()
{
foreach (Observer observer in observers)
{
observer.Update();
}
}
}

public abstract class Observer
{
public abstract void Update();
}

public class ConcreteSubject : Subject
{
private string state;

public string State
{
get { return state; }
set
{
state = value;
Notify();
}
}
}

public class ConcreteObserver : Observer
{
private string name;
private ConcreteSubject subject;

public ConcreteObserver(string name, ConcreteSubject subject)
{
this.name = name;
this.subject = subject;
}

public override void Update()
{
Console.WriteLine($"Observer {name} received update from Subject. New state: {subject.State}");
}
}

Strategy:

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently from clients that use it. In the following example, the Strategy interface declares the common operations for the algorithms. The ConcreteStrategyA and ConcreteStrategyB classes implement specific versions of the algorithm. The Context class maintains a reference to a strategy object and delegates the execution of the algorithm to it.

public interface Strategy
{
void Execute();
}

public class ConcreteStrategyA : Strategy
{
public void Execute()
{
Console.WriteLine("Executing Strategy A");
}
}

public class ConcreteStrategyB : Strategy
{
public void Execute()
{
Console.WriteLine("Executing Strategy B");
}
}

public class Context
{
private Strategy strategy;

public Context(Strategy strategy)
{
this.strategy = strategy;
}

public void SetStrategy(Strategy strategy)
{
this.strategy = strategy;
}

public void ExecuteStrategy()
{
strategy.Execute();
}
}

Command:

The Command pattern encapsulates a request as an object, thereby allowing clients to parameterize clients with different requests, queue or log requests, and support undoable operations. In the following example, the Command interface declares the common operations for executing a command. The ConcreteCommand class encapsulates a specific command and binds it to a receiver object. The Invoker class invokes the command, which delegates the execution to the appropriate receiver.

public interface Command
{
void Execute();
}

public class ConcreteCommand : Command
{
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");
}
}

public class Invoker
{
private Command command;

public void SetCommand(Command command)
{
this.command = command;
}

public void ExecuteCommand()
{
command.Execute();
}
}

Template Method:

The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It allows subclasses to redefine certain steps of an algorithm without changing the algorithm’s structure. In the following example, the AbstractClass defines the template method that outlines the algorithm, including some concrete steps and abstract methods. The ConcreteClassA and ConcreteClassB subclasses provide their own implementations of the abstract methods, customizing the algorithm's behavior.

public abstract class AbstractClass
{
public void TemplateMethod()
{
Console.WriteLine("Executing step 1");
PrimitiveOperation1();
Console.WriteLine("Executing step 3");
PrimitiveOperation2();
}

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

public class ConcreteClassA : AbstractClass
{
protected override void PrimitiveOperation1()
{
Console.WriteLine("ConcreteClassA: PrimitiveOperation1");
}

protected override void PrimitiveOperation2()
{
Console.WriteLine("ConcreteClassA: PrimitiveOperation2");
}
}

public class ConcreteClassB : AbstractClass
{
protected override void PrimitiveOperation1()
{
Console.WriteLine("ConcreteClassB: PrimitiveOperation1");
}

protected override void PrimitiveOperation2()
{
Console.WriteLine("ConcreteClassB: PrimitiveOperation2");
}
}

Iterator:

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It decouples the client from the implementation of the collection. In the following example, the Iterator interface defines the operations for traversing the elements. The ConcreteIterator class implements the iterator and maintains the current position in the collection. The Aggregate interface declares the creation of an iterator, and the ConcreteAggregate class implements it, returning a specific iterator for the collection.

public interface Iterator
{
bool HasNext();
object Next();
}

public class ConcreteIterator : Iterator
{
private ConcreteAggregate aggregate;
private int index;

public ConcreteIterator(ConcreteAggregate aggregate)
{
this.aggregate = aggregate;
index = 0;
}

public bool HasNext()
{
return index < aggregate.Count();
}

public object Next()
{
if (HasNext())
{
return aggregate.GetItem(index++);
}

throw new InvalidOperationException("No more elements");
}
}

public interface Aggregate
{
Iterator CreateIterator();
int Count();
object GetItem(int index);
}

public class ConcreteAggregate : Aggregate
{
private List<object> items = new List<object>();

public void AddItem(object item)
{
items.Add(item);
}

public Iterator CreateIterator()
{
return new ConcreteIterator(this);
}

public int Count()
{
return items.Count;
}

public object GetItem(int index)
{
return items[index];
}
}

Alright, folks! We’ve done a comprehensive coverage of all the 23 patterns, providing extensive examples. 📚💡 Our sincere hope is that this article will assist you all in grasping the intricacies of design patterns effortlessly. 🤝🔍 Happy learning and understanding!

🔍📚 In case you folks are interested in learning microservices from scratch using clean architecture, you can begin your journey here: 🏗️💡

Feel free to explore and dive into the exciting world of microservices! 🚀🌟

--

--

rahul sahay
rahul sahay

Written by rahul sahay

🌟 Unleashing the Power of Languages! 🚀 Expert polyglot developer, author, and Course creator on a mission to transform coding into an art. 🎨 Join me in

No responses yet