20 Design Patterns Every Software Developer Needs to Know

Thomas CherickalThomas Cherickal
28 min read

Design patterns in Software, photo realistic, stunning image, a man and a woman, beautiful, in a company with computers ...

In the world of software engineering, some texts are foundational.

"Design Patterns: Elements of Reusable Object-Oriented Software," penned by the illustrious "Gang of Four" (GoF), is one such text.

It presented a catalogue of 23 design patterns that have since become a cornerstone of object-oriented design, promoting reusable, flexible, and maintainable code.

This article revisits 20 of these timeless patterns, presenting them in the classic GoF style.

Let’s get to it!

Creational Design Patterns

Creational patterns abstract the object-creation process, providing flexibility in how objects are instantiated. They help manage complexity by controlling how, when, and what objects are created.


1. Singleton

  • Intent:

    • Ensure a class only has one instance and provide a global point of access to it.
  • Motivation:

    • For certain classes, having exactly one instance is crucial.

    • Think of a system's configuration manager, a logger, or a hardware interface.

    • Singleton prevents the instantiation of multiple objects, offering a single, well-known access point.

  • Applicability:

    • Use the Singleton pattern when there must be precisely one instance of a class, accessible from anywhere in the client code.
  • Explanation:

    • When:

      • Use when you must have one, and only one, object of a specific class for the entire system.

      • For example, a single database connection pool or a service that manages application-wide settings.

    • Why:

      • To control access to a shared resource, avoid conflicting requests, and prevent inconsistent state that could arise from multiple instances manipulating the same resource.

      • It also avoids polluting the global namespace.

    • How:

      • Hide the class constructor and create a static creation method (getInstance()).

      • The first time this method is called, it creates a new instance and stores it in a private static field.

      • All subsequent calls return this cached instance.

  • Structure:

  • Consequences:

    • It provides controlled access to the sole instance.

    • However, it can introduce global state, making unit testing difficult as components become tightly coupled to the Singleton.

  • Python Snippet:

class Singleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

s1 = Singleton()
s2 = Singleton()
print(f"Are s1 and s2 the same instance? {s1 is s2}")

2. Factory Method

  • Intent:

    • Define an interface for creating an object, but let subclasses decide which class to instantiate.
  • Motivation:

    • Frameworks often need to create objects, but the exact type of object varies depending on the application.

    • The Factory Method allows a framework to defer the instantiation to its subclasses.

  • Applicability:

    • Use this pattern when a class cannot anticipate the class of objects it needs to create or when a class wants its subclasses to specify the objects it creates.
  • Explanation:

    • When:

      • Use when you don't know the exact types of objects your code will need to work with beforehand.

      • Also use when you want to give users of your framework or library a way to extend its internal components.

    • Why:

      • To decouple your client code from the concrete classes of the objects you need to create.

      • This promotes loose coupling and adheres to the "Open/Closed Principle," as you can introduce new product types without changing the creator's code.

    • How:

      • Create a "creator" class with an abstract factory_method().

      • Concrete creator subclasses then implement this method to produce specific "product" objects.

      • The client code works with the creator's interface and is thus isolated from the specific product classes.

  • Structure:

  • Consequences:

    • It allows subclasses to provide an extended version of an object and connects parallel class hierarchies.
  • Python Snippet:

from abc import ABC, abstractmethod

class Button(ABC): 
    @abstractmethod
    def click(self): pass

class WindowsButton(Button):
    def click(self): return "Windows button click."

class Dialog(ABC):
    @abstractmethod
    def create_button(self) -> Button: pass
    def render(self): return self.create_button().click()

class WindowsDialog(Dialog):
    def create_button(self): return WindowsButton()

dialog = WindowsDialog()
print(dialog.render())

3. Abstract Factory

  • Intent:

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

    • Consider a UI toolkit that needs to support multiple look-and-feel standards (e.g., Windows, macOS).

    • An Abstract Factory can create a set of related widgets (like buttons and checkboxes) for a specific standard.

  • Applicability:

    • Use this pattern when a system should be independent of how its products are created, composed, and represented.
  • Explanation:

    • When:

      • Use when your system needs to create multiple families of related products, and you want to ensure that the products from one family are always used together.
    • Why:

      • To ensure consistency among products of the same family.

      • It isolates the client code from the concrete implementation of the products, making it easy to switch between product families.

    • How:

      • Define an AbstractFactory interface with a set of factory methods for creating each product in a family (e.g., create_button(), create_checkbox()).

      • Then, create concrete factory classes for each family (e.g., WinFactory, MacFactory) that implement these methods to produce a specific variant of each product.

  • Structure:

  • Consequences:

    • It isolates concrete classes and makes exchanging product families easy.

    • However, adding new kinds of products is difficult because it requires changing the factory interface and all of its subclasses.

  • Python Snippet:

class Button(ABC): pass

class Checkbox(ABC): pass

class WinButton(Button): pass

class WinCheckbox(Checkbox): pass

class GUIFactory(ABC):

    @abstractmethod
    def create_button(self) -> Button: pass

    @abstractmethod
    def create_checkbox(self) -> Checkbox: pass

class WinFactory(GUIFactory):
    def create_button(self): return WinButton()
    def create_checkbox(self): return WinCheckbox()

4. Builder

  • Intent:

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

    • Useful when an object needs to be created with a complex, multi-step process.

    • The Builder pattern allows you to create different variations of an object using the same construction code, avoiding a "telescoping constructor" with many parameters.

  • Applicability:

    • Use this pattern when the algorithm for creating a complex object should be independent of its parts and when the construction process must allow for different representations.
  • Explanation:

    • When:

      • Use when constructing an object is a complex process with many steps or optional configurations.

      • For example, building a House object with optional walls, doors, windows, and a roof.

    • Why:

      • To simplify client code by hiding the complex construction logic.

      • It allows for a step-by-step construction process and enables the same construction logic to produce different types and representations of the object.

    • How:

      • Create a Builder interface with methods for each construction step (e.g., build_walls()).

      • Create ConcreteBuilder classes to implement these steps.

      • The builder also has a method to return the final product. Optionally, a Director class can encapsulate common construction sequences.

  • Structure:

  • Consequences:

    • It lets you vary a product's internal representation, isolates construction and representation code, and gives you finer control over the construction process.
  • Python Snippet:

class Pizza:
    def __init__(self): self.parts = []
    def add(self, part): self.parts.append(part)
    def __str__(self): return f"Pizza with: {', '.join(self.parts)}"

class PizzaBuilder:
    def __init__(self): self.pizza = Pizza()
    def add_sauce(self): self.pizza.add("sauce"); return self
    def add_topping(self, topping): self.pizza.add(topping); return self
    def build(self): return self.pizza

hawaiian = PizzaBuilder().add_sauce().add_topping("ham").add_topping("pineapple").build()
print(hawaiian)

5. Prototype

  • Intent:

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

    • When creating an object is expensive (e.g., requires a database query or a complex computation), it's often more efficient to clone an existing object and modify it as needed.
  • Applicability:

    • Use this pattern when a system should be independent of how its products are created, and when the classes to instantiate are specified at runtime.
  • Explanation:

    • When:

      • Use when the cost of creating a new object is higher than the cost of cloning an existing one.

      • Also use when your code doesn't need to depend on the concrete classes of the objects it needs to copy.

    • Why:

      • To reduce the overhead of object creation.

      • It also allows you to add and remove products at runtime by simply registering new prototypes.

    • How:

      • Create a Prototype interface with a clone() method.

      • All concrete product classes implement this interface.

      • A client can then get a new object by calling the clone() method on a prototype instance, without knowing its concrete class.

  • Structure:

  • Consequences:

    • It allows you to create new objects without being tied to their concrete classes and can be more efficient than standard creation.

    • However, implementing clone can be complex, especially with circular references or complex object graphs.

  • Python Snippet:

import copy

class Shape(ABC):
    @abstractmethod
    def clone(self): pass

class Rectangle(Shape):
    def __init__(self, width, height): self.width, self.height = width, height
    def clone(self): return copy.deepcopy(self) # deepcopy for a full clone
    def __str__(self): return f"Rectangle: {self.width}x{self.height}"

rect1 = Rectangle(10, 20)
rect2 = rect1.clone()
rect2.width = 30
print(rect1)
print(rect2)

Structural Design Patterns

Structural patterns focus on how classes and objects can be composed to form larger, more flexible structures.

Design patterns in Software, photo realistic, stunning image, a man and a woman, beautiful, in a company with computers,...


6. Adapter

  • Intent:

    • Convert the interface of a class into another interface clients expect.

    • An adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

  • Motivation:

    • You have a third-party library or a legacy component that provides the functionality you need, but its interface is not compatible with the rest of your code.

    • The Adapter pattern allows you to use this class by creating a "wrapper" with the expected interface.

  • Applicability:

    • Use it when you want to use an existing class with an incompatible interface or when you want to create a reusable class that cooperates with unrelated classes.
  • Explanation:

    • When:

      • Use when you need to integrate a component with an interface that doesn't match what your client code expects.
    • Why:

      • To allow collaboration between objects with incompatible interfaces without modifying their source code.

      • This promotes reusability of existing components.

    • How:

      • Create an Adapter class that implements the target interface your client expects.

      • The adapter holds a reference to the Adaptee (the object with the incompatible interface) and translates the client's calls into calls on the adaptee.

  • Structure:

  • Consequences: It allows a single adapter to work with many adaptees—that is, the adaptee itself and all of its subclasses (if any).

  • Python Snippet:

class OldLogger:
    def log_message(self, msg): print(f"Legacy Log: {msg}")

class NewLogger:
    def log(self, text): pass

class LoggerAdapter(NewLogger):
    def __init__(self, old_logger): self._old_logger = old_logger
    def log(self, text): self._old_logger.log_message(text)

adapter = LoggerAdapter(OldLogger())
adapter.log("This is a test.")

7. Bridge

  • Intent: Decouple an abstraction from its implementation so that the two can vary independently.

  • Motivation: When you have a class that has both an abstraction (e.g., Shape) and multiple possible implementations (e.g., DrawingAPI1, DrawingAPI2), using inheritance can lead to a class explosion (CircleAPI1, SquareAPI1, CircleAPI2, etc.). The Bridge pattern separates the two into separate class hierarchies.

  • Applicability: Use this pattern when you want to avoid a permanent binding between an abstraction and its implementation, and when both abstractions and their implementations can be extended by subclassing.

  • Explanation:

    • When: Use when you need to vary both an abstraction and its implementation independently. Often used with GUIs to support multiple platforms.

    • Why: To avoid a "class explosion" from combining multiple abstractions and implementations through inheritance. It allows you to change the implementation details without recompiling the abstraction and its clients.

    • How: Split the logic into two separate hierarchies: Abstraction and Implementation. The Abstraction holds a reference to an Implementor object and delegates work to it. Clients only interact with the Abstraction.

  • Structure:

  • Consequences:

    • It decouples the interface and implementation, improving extensibility, but can increase complexity by adding another level of indirection.
  • Python Snippet:

class DrawingAPI(ABC):
    @abstractmethod
    def draw_circle(self): 
        pass

class DrawingAPI1(DrawingAPI):
    def draw_circle(self): 
        return "API1.circle"

class Shape(ABC):
    def __init__(self, drawing_api): self._drawing_api = drawing_api

    @abstractmethod
    def draw(self): 
        pass

class Circle(Shape):
    def draw(self): 
         return f"Circle drawn by {self._drawing_api.draw_circle()}"

circle = Circle(DrawingAPI1())
print(circle.draw())

8. Composite

  • Intent:

    • Compose objects into tree structures to represent part-whole hierarchies.

    • Composite lets clients treat individual objects and compositions of objects uniformly.

  • Motivation:

    • In graphics applications, a drawing can be composed of simple shapes (leaves) and other, more complex drawings (composites).

    • The Composite pattern allows you to treat both simple and complex objects uniformly through a common interface.

  • Applicability:

    • Use this when you want to represent part-whole hierarchies of objects and you want clients to be able to treat individual and composite objects uniformly.
  • Explanation:

    • When:

      • Use when your objects can be structured in a tree-like hierarchy, and you want to perform operations on the entire tree or its sub-parts in a uniform way.
    • Why:

      • To simplify client code.

      • The client doesn't need to know if it's dealing with a single object (a leaf) or a group of objects (a composite).

      • It just calls methods on the common interface.

    • How:

      • Define a common Component interface for both Leaf (individual) and Composite objects.

      • The Composite class holds a list of child Component objects and implements its interface methods by delegating to its children.

  • Structure:

  • Consequences:

    • It makes it easier to add new kinds of components.

    • However, it can make the design overly general, as you might try to perform meaningless operations (like adding) on a leaf.

  • Python Snippet:

class Graphic(ABC):

@abstractmethod
def render(self): pass

class Circle(Graphic):
    def render(self): return "Circle"

class Picture(Graphic):

    def __init__(self): 
         self._children = []
    def add(self, graphic): 
         self._children.append(graphic)
    def render(self): 
          return f"Picture({', '.join(c.render() for c in self._children)})"

pic = Picture()
pic.add(Circle())
pic.add(Circle())
print(pic.render())

9. Decorator

  • Intent:

    • Attach additional responsibilities to an object dynamically.

    • Decorators provide a flexible alternative to subclassing for extending functionality.

  • Motivation:

    • You want to add functionality to an object, but you don't want to affect other objects of the same class.

    • A decorator wraps the original object and adds the new functionality, like putting layers on an onion.

  • Applicability:

    • Use it to add responsibilities to individual objects dynamically and transparently, or when extension by subclassing is impractical due to a large number of independent extensions.
  • Explanation:

    • When:

      • Use when you want to add behavior to objects at runtime without changing their class.

      • For example, adding features like compression or encryption to a data stream object.

    • Why:

      • To adhere to the "Single Responsibility" and "Open/Closed" principles.

      • You can add new functionalities without changing the core component's code, and each decorator class has only one responsibility.

    • How:

      • Create a Component interface.

      • Your ConcreteComponent implements it.

      • Create an abstract Decorator class that also implements the Component interface and holds a reference to a Component object.

      • ConcreteDecorator classes add their own behavior before or after delegating the call to the wrapped component.

  • Structure:

  • Consequences:

    • It provides more flexibility than static inheritance but can lead to a large number of small, hard-to-debug objects in the system.
  • Python Snippet:

class Notifier:
    def send(self, message): return f"Email: {message}"

class SMSDecorator:
    def __init__(self, notifier): self._notifier = notifier
    def send(self, message): return f"{self._notifier.send(message)} & SMS: {message}"

notifier = Notifier()
notifier_with_sms = SMSDecorator(notifier)
print(notifier_with_sms.send("Hello!"))

10. Facade

  • Intent: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

  • Motivation: A complex system may have many interacting parts. A Facade provides a simplified, single entry point to the subsystem, shielding clients from its internal complexity.

  • Applicability: Use it when you want to provide a simple interface to a complex subsystem or when you want to layer your subsystems to reduce dependencies.

  • Explanation:

    • When:

      • Use when you have a complex subsystem with many moving parts, and you want to provide a simple way for clients to interact with it.
    • Why:

      • To decouple the client from the complexities of the subsystem.

      • This makes the client code cleaner and makes the subsystem easier to use and maintain.

    • How:

      • Create a Facade class that knows which subsystem classes are responsible for a request.

      • The facade delegates client requests to the appropriate subsystem objects. The facade itself does little work.

  • Structure:

  • Consequences:

    • It shields clients from subsystem components and promotes weak coupling between the subsystem and its clients.

    • However, it doesn't prevent clients from accessing the underlying subsystem classes directly if they need more flexibility.

  • Python Snippet:
class CPU: def execute(self): print("Executing.")
class Memory: def load(self): print("Loading data.")

class ComputerFacade:
    def __init__(self): self.cpu, self.memory = CPU(), Memory()
    def start(self):
        self.memory.load()
        self.cpu.execute()

computer = ComputerFacade()
computer.start()

11. Flyweight

  • Intent:

    • Use sharing to support large numbers of fine-grained objects efficiently.
  • Motivation:

    • Some applications require a huge number of objects that have some shared state (intrinsic) and some unique state (extrinsic).

    • The Flyweight pattern saves memory by sharing the common intrinsic state among multiple objects instead of storing it in each object.

  • Applicability:

    • Use this pattern when an application uses a large number of objects, storage costs are high because of the sheer quantity of objects, and most object state can be made extrinsic.
  • Explanation:

    • When:

      • Use when your application needs to spawn a vast number of similar objects, and memory usage is a concern.

      • For example, rendering millions of trees in a forest or characters in a text editor.

    • Why:

      • To drastically reduce memory consumption by sharing the common parts of object state among multiple objects instead of keeping it in each object.
    • How:

      • Separate an object's state into intrinsic (shared, context-independent) and extrinsic (unique, context-dependent).

      • The intrinsic state is stored in a Flyweight object.

      • A FlyweightFactory creates and manages these flyweights.

      • Clients pass the extrinsic state to the flyweight's methods when they need to perform an operation.

  • Structure:

  • Consequences:

    • Reduces the total number of objects and memory usage.

    • However, it may increase runtime costs for transferring and computing extrinsic state.

  • Python Snippet:

class TreeType: # Flyweight
    def __init__(self, name): self.name = name # Intrinsic state
    def draw(self, x, y): print(f"Drawing a {self.name} tree at ({x}, {y})")

class TreeFactory:
    _tree_types = {}
    def get_tree_type(cls, name):
        if name not in cls._tree_types:
            cls._tree_types[name] = TreeType(name)
        return cls._tree_types[name]

# Client uses extrinsic state (x, y)
oak_type = TreeFactory.get_tree_type("Oak")
oak_type.draw(10, 20)
oak_type.draw(30, 40)

12. Proxy

  • Intent:

    • Provide a surrogate or placeholder for another object to control access to it.
  • Motivation:

    • You need to control access to an object.

    • A proxy can act as a stand-in, performing actions before or after the request gets to the original object.

    • Examples include virtual proxies for lazy loading, protection proxies for access control, and remote proxies for network communication.

  • Applicability:

    • Use it when you need a more versatile or sophisticated reference to an object than a simple pointer, for reasons like lazy initialization, access control, or logging.
  • Explanation:

    • When:

      • Use when you want to add a layer of control over access to an object.

      • This is useful for remote objects, large objects that are expensive to create, or objects requiring security checks.

    • Why:

      • To manage the lifecycle of a service object without the client knowing.

      • The proxy can handle tasks like network connections, caching, or access validation, keeping the client and service object's code cleaner.

    • How:

      • Create a Proxy class that implements the same interface as the RealSubject (the service object).

      • The proxy holds a reference to the real subject. When a client calls a method on the proxy, the proxy can perform its extra logic and then delegate the call to the real subject.

  • Structure:

  • Consequences:

    • It can introduce a level of indirection when accessing an object, but it provides a great deal of flexibility in managing the object's lifecycle and access.
  • Python Snippet:

class Database(ABC): 
@abstractmethod
def execute_query(self, query): pass

class RealDatabase(Database):
    def execute_query(self, query): return f"Executing {query}"

class DatabaseProxy(Database):
    def __init__(self, user):
        self.user = user
        self._db = RealDatabase()
    def execute_query(self, query):
        if self.user == "admin": return self._db.execute_query(query)
        else: return "Access Denied"

db = DatabaseProxy(user="admin")
print(db.execute_query("SELECT *"))

Behavioral Design Patterns

Design patterns in Software, photo realistic, stunning image, a man and a woman, beautiful, in a company with computers,...

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


13. Chain of Responsibility

  • Intent:

    • Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.

    • Chain the receiving objects and pass the request along the chain until an object handles it.

  • Motivation:

    • Imagine a system for processing help requests.

    • Different handlers might be responsible for different types of requests (e.g., technical, billing, general).

    • The Chain of Responsibility pattern creates a chain of these handlers, and the request travels along the chain until it's handled.

  • Applicability:

    • Use it when more than one object may handle a request, and the handler isn't known beforehand.

    • Also, when you want to issue a request to one of several objects without specifying the receiver explicitly.

  • Explanation:

    • When:

      • Use when a request can be handled by one of several potential handlers, and you don't want the client to be coupled to a specific handler.
    • Why:

      • To decouple the sender of a request from its receiver.

      • It also allows you to dynamically modify the chain of handlers at runtime.

    • How:

      • Define a Handler interface with a method to handle requests and a reference to the next handler in the chain.

      • ConcreteHandler classes implement this interface.

      • If a handler can process the request, it does so.

      • Otherwise, it passes the request to its successor in the chain.

  • Structure:

  • Consequences:

    • Reduced coupling and added flexibility in assigning responsibilities.

    • However, receipt isn't guaranteed; a request can fall off the end of the chain if no handler processes it.

  • Python Snippet:

class Handler(ABC):
    def __init__(self, successor=None): self._successor = successor
    def handle(self, request):
        if self._successor: return self._successor.handle(request)
        return None

class PositiveHandler(Handler):
    def handle(self, request):
        if request > 0: return f"PositiveHandler handled {request}"
        else: return super().handle(request)

handler = PositiveHandler()
print(handler.handle(10))

14. Command

  • Intent:

    • Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
  • Motivation:

    • Sometimes it's necessary to issue requests to objects without knowing anything about the operation being requested or the receiver of the request.

    • The Command pattern turns a request into a stand-alone object containing all information about the request.

  • Applicability:

    • Use it to parameterize objects by an action to perform, to specify, queue, and execute requests at different times, and to support undo.
  • Explanation:

    • When:

      • Use when you want to issue requests that can be queued, logged, or undone.

      • Also useful for implementing callback functionality.

    • Why:

      • To decouple the object that invokes an operation (Invoker) from the object that performs it (Receiver).

      • This allows you to create generic components that can be configured with different commands.

    • How:

      • Create a Command interface with an execute() method.

      • ConcreteCommand classes implement this method and hold a reference to a Receiver object, which does the actual work.

      • The Invoker holds a command object and calls its execute() method when needed.

  • Structure:

  • Consequences:

    • It decouples the object that invokes the operation from the one that knows how to perform it.

    • It's also easy to add new Commands because you don't have to change existing classes.

  • Python Snippet:

class Light:
    def turn_on(self): print("Light is ON")

class Command(ABC): @abstractmethod
def execute(self): pass

class TurnOnCommand(Command):
    def __init__(self, light): self._light = light
    def execute(self): self._light.turn_on()

class RemoteControl:
    def __init__(self, command): self._command = command
    def press_button(self): self._command.execute()

remote = RemoteControl(TurnOnCommand(Light()))
remote.press_button()

15. Iterator

  • Intent:

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

    • You need to traverse a collection of objects (like a list, tree, or graph), but you want to decouple the traversal algorithm from the collection's implementation.

    • The Iterator pattern provides a uniform way to traverse different collections.

  • Applicability:

    • Use this pattern to access an aggregate object's contents without exposing its internal structure, to support multiple traversals of aggregate objects, and to provide a uniform interface for traversing different aggregate structures.
  • Explanation:

    • When:

      • Use when you have a complex data structure and want to provide a simple way to traverse its elements, or when you want your traversal algorithm to be independent of the data structure.
    • Why:

      • To simplify the interface of the collection and to allow for different kinds of iterators (e.g., forward, backward) to coexist.

      • It cleans up client code and centralizes traversal logic.

    • How:

      • The Aggregate (collection) class provides a method to create an Iterator.

      • The Iterator interface defines methods for traversal, such as has_next() and next().

      • The client gets an iterator from the collection and uses it to traverse the elements.

  • Structure:

  • Consequences:

    • It supports variations in the traversal of an aggregate and simplifies the aggregate's interface.

    • Multiple iterators can traverse the same aggregate simultaneously.

  • Python Snippet:

# Python's `for` loop and iterables are a built-in implementation of the Iterator pattern.

class WordCollection:
    def __init__(self): self._words = []
    def add_item(self, item): self._words.append(item)
    def __iter__(self): # This method makes the class an iterable
        return iter(self._words)

collection = WordCollection()
collection.add_item("First")
collection.add_item("Second")
for item in collection:
    print(item)

16. Mediator

  • Intent:

    • Define an object that encapsulates how a set of objects interact.

    • Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

  • Motivation:

    • When you have a system of interacting objects (colleagues), their direct connections can become complex and tangled.

    • The Mediator acts as a central hub, managing the communication between them.

  • Applicability:

    • Use this pattern when a set of objects communicate in well-defined but complex ways, and you want to customize the behavior without a lot of subclassing.
  • Explanation:

    • When:

      • Use when you find a group of objects communicating in a chaotic, "many-to-many" fashion.

      • A common example is coordinating dialog controls in a GUI form.

    • Why:

      • To reduce the coupling between interacting objects (colleagues), making them easier to reuse and modify independently.

      • It centralizes complex communication logic in one place.

    • How:

      • Create a Mediator interface.

      • Colleague objects hold a reference to the mediator.

      • When a colleague needs to communicate, it notifies the mediator.

      • The mediator then decides which other colleagues should be informed and how.

  • Structure:

  • Consequences: It limits subclassing and decouples colleagues. However, the mediator itself can become a monolithic, complex object that is hard to maintain.

  • Python Snippet:

class ChatRoom: # Mediator
    def display_message(self, user, message):
        print(f"[{user.name}]: {message}")

class User: # Colleague
    def __init__(self, name, chat_room):
        self.name = name
        self.chat_room = chat_room
    def send(self, message):
        self.chat_room.display_message(self, message)

room = ChatRoom()
john = User("John", room)
jane = User("Jane", room)
john.send("Hi Jane!")

17. Memento

  • Intent:

    • Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
  • Motivation:

    • You need to implement a checkpoint or "undo" mechanism.

    • The Memento pattern allows you to save and restore the previous state of an object (Originator) without making its internal structure public.

  • Applicability:

    • Use it when a snapshot of an object's state must be saved so that it can be restored later, and when a direct interface to obtaining the state would expose implementation details.
  • Explanation:

    • When:

      • Use when you need to provide an "undo" or "rollback" feature.

      • Also useful for creating snapshots of an object's state for transactions.

    • Why:

      • To preserve encapsulation.

      • The object's internal state is saved without exposing it to the outside world.

      • The responsibility for managing the saved states is delegated to a separate Caretaker object.

    • How:

      • The Originator (the object whose state needs saving) creates a Memento object that stores a snapshot of its state.

      • The Caretaker requests this memento from the originator and holds onto it.

      • To restore the state, the caretaker passes the memento back to the originator.

  • Structure:

  • Consequences: It can be expensive if the originator has a lot of state. It also simplifies the originator by letting the caretaker manage the memento's lifecycle.

  • Python Snippet:

class Editor: # Originator
    def __init__(self): self.content = ""
    def create_memento(self): return Memento(self.content)
    def restore(self, memento): self.content = memento.get_content()

class Memento:
    def __init__(self, content): self._content = content
    def get_content(self): return self._content

editor = Editor()
editor.content = "Hello"
memento = editor.create_memento()
editor.content = "World"
editor.restore(memento) # Undo
print(editor.content)

18. Observer

  • Intent:

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

    • When a change in one object requires changing others, and you don't know how many objects need to be changed, the Observer pattern keeps these objects loosely coupled.

    • It is the foundation of event-driven programming.

  • Applicability:

    • Use it when a change to one object requires changing others, and you don't know how many objects need to be changed or who they are.
  • Explanation:

    • When:

      • Use when changes to the state of one object may require changing other objects, and the set of interested objects can change dynamically.
    • Why:

      • To establish loose coupling between the object being observed (Subject) and the objects that observe it (Observers).

      • You can add new observers without modifying the subject.

    • How:

      • The Subject object maintains a list of Observer objects.

      • It has methods to attach and detach observers.

      • When the subject's state changes, it calls its notify method, which loops through the observers and calls their update method.

  • Structure:

  • Consequences:

    • It provides abstract coupling between Subject and Observer.

    • However, it can result in unexpected or inefficient updates if not managed carefully, especially in complex dependency chains.

  • Python Snippet:

class Subject:
    def __init__(self): self._observers = []
    def attach(self, observer): self._observers.append(observer)
    def notify(self, message):
        for observer in self._observers: observer.update(message)

class ConcreteObserver:
    def update(self, message): print(f"Observer got: {message}")

subject = Subject()
subject.attach(ConcreteObserver())
subject.notify("State changed!")

19. Strategy

  • Intent:

    • Define a family of algorithms, encapsulate each one, and make them interchangeable.

    • Strategy lets the algorithm vary independently from clients that use it.

  • Motivation:

    • You have different algorithms for a specific task (e.g., sorting, validation), and you want to be able to switch between them at runtime.

    • The Strategy pattern encapsulates each algorithm into a separate class with a common interface.

  • Applicability:

    • Use it when you have many related classes that differ only in their behavior or when you need different variants of an algorithm that you can swap easily.
  • Explanation:

    • When:

      • Use when you want to use different algorithms within an object and be able to switch from one algorithm to another during runtime.
    • Why:

      • To avoid conditional statements (if-else or switch) for selecting an algorithm.

      • It follows the "Open/Closed Principle," as you can introduce new strategies without changing the context's code.

    • How:

      • Define a Strategy interface for the family of algorithms.

      • Each ConcreteStrategy implements this interface.

      • The Context class holds a reference to a strategy object and delegates the algorithmic work to it.

  • Structure:

  • Consequences:

    • It's a good alternative to subclassing and can eliminate conditional statements.

    • However, clients must be aware of the different strategies to select the right one.

  • Python Snippet:

class Sorter:
    def __init__(self, strategy): self._strategy = strategy
    def sort(self, data): return self._strategy(data)

def quick_sort(data): return sorted(data) # Simplified for brevity
def bubble_sort(data): return sorted(data, reverse=True) # Simplified

sorter = Sorter(quick_sort)
print(sorter.sort([3, 1, 2]))
sorter._strategy = bubble_sort
print(sorter.sort([3, 1, 2]))

20. Template Method

  • Intent:

    • Define the skeleton of an algorithm in an operation, deferring some steps to subclasses.

    • Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

  • Motivation:

    • You have several classes that perform similar steps in the same order, but the details of those steps differ.

    • The Template Method pattern creates a base class that defines the overall algorithm structure and allows subclasses to provide the implementation for the variable steps.

  • Applicability:

    • Use it to implement the invariant parts of an algorithm once and leave it up to subclasses to implement the behavior that can vary.
  • Explanation:

    • When:

      • Use when you have multiple algorithms that have a similar structure but differ in their individual steps.
    • Why:

      • To avoid code duplication.

      • The common algorithm structure is defined once in a base class, and subclasses only need to implement the parts that vary.

      • This is a very common and straightforward pattern.

    • How:

      • Create an abstract base class with a template_method().

      • This method calls a series of abstract "primitive" methods in a specific order.

      • Concrete subclasses implement these primitive methods to define the varying parts of the algorithm.

  • Structure:

  • Consequences:

    • It's a fundamental technique for code reuse.

    • However, it can be difficult to change the overall algorithm structure, as it is fixed in the base class.

  • Python Snippet:

class DataProcessor(ABC):

    def process(self): # Template Method
        data = self.read_data()
        return self.process_data(data)

    @abstractmethod
    def read_data(self): pass

    @abstractmethod
    def process_data(self, data): pass

class TextFileProcessor(DataProcessor):
    def read_data(self): return "some text"
    def process_data(self, data): return data.upper()

processor = TextFileProcessor()
print(processor.process())

Conclusion

Our journey through these 20 foundational design patterns from the Gang of Four is more than just an academic exercise.

It's an initiation into a shared vocabulary that enables developers to communicate complex architectural ideas with precision.

These patterns are not rigid rules but time-tested blueprints for building software that is not only functional but also flexible, scalable, and easier to maintain.

However, a word of caution is essential.

The goal is not to force a pattern into every corner of your codebase.

True mastery lies in recognizing the specific problem a pattern solves and applying it judiciously.

The best-designed systems often use patterns so elegantly that they feel like a natural part of the solution, not a complex addition.

The hammer of a design pattern shouldn't turn every problem into a nail.

Start by identifying these patterns in the wild—in the frameworks and libraries you use every day.

Then, look for opportunities in your own projects to refactor towards a pattern where it clarifies intent and simplifies structure.

Integrating these concepts into your development practice is a significant step toward mastering the craft of software engineering.

Ultimately, design patterns are tools for thought that empower you to build not just working software, but elegant and enduring solutions.

Design patterns in Software, photo realistic, stunning image, a man and a woman, beautiful, in a company with computers ...

Google AI Studio was used in the outlining and research for this article.

You can find it here:

https://aistudio.google.com

All images in this article are AI-generated by the author with NightCafe Studio.

0
Subscribe to my newsletter

Read articles from Thomas Cherickal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Thomas Cherickal
Thomas Cherickal

Profile: https://thomascherickal.com Portfolio: https://hackernoon.com/u/thomascherickal Presence: https://linktr.ee/thomascherickal LinkedIn: https://linkedin.com/in/thomascherickal GitHub: https://github.com/thomascherickal Email: thomascherickal@gmail.com