Introduction

Welcome to this comprehensive guide on Design Patterns! As software developers, we often encounter recurring design problems that require elegant and efficient solutions. Design patterns provide us with tested and proven solutions to these common problems, helping us write more maintainable, scalable, and robust code.

In this blog, we will explore various design patterns categorised under Creational, Structural, and Behavioural patterns. Each section includes explanations and sample Python code to illustrate the concepts.

Why Design Patterns?

Design patterns are like templates for solving problems. They provide a standard terminology and are specific to particular scenarios. By understanding and using these patterns, you can:

  • Enhance your coding skills: Recognise common problems and apply suitable solutions efficiently.
  • Improve communication: Share solutions using common terms that other developers can understand.
  • Write maintainable code: Create flexible and reusable code that is easier to understand and modify.

About the Code Samples

You will find that the code samples provided for each pattern may seem a bit cumbersome or abstract when viewed in isolation. This is because the primary goal of these examples is to illustrate the core concepts and mechanics of the design patterns, rather than to solve specific, real-world problems.

Understanding the Context

In actual applications, these patterns would be implemented within the context of a larger problem domain, making their utility more apparent. For example, a Factory Method pattern might be used in a game to create different types of characters, or a Strategy pattern could be applied in a sorting algorithm to switch between different sorting techniques dynamically.

How to Use This Guide

  1. Learn the Patterns: Study the explanations and code samples to understand how each pattern works.
  2. Identify Use Cases: Think about problems you’ve encountered in your own projects and how these patterns could provide solutions.
  3. Apply and Adapt: Use the patterns in your code, adapting them to fit your specific needs and problem domains.

Remember, the power of design patterns lies not just in knowing them, but in recognising when and how to apply them effectively. By practicing and integrating these patterns into your work, you’ll be well-equipped to tackle complex design challenges with confidence.

Hierarchy of Patterns

  1. Creational Patterns
    • Factory Method
    • Abstract Factory
    • Singleton
    • Builder
    • Prototype
  2. Structural Patterns
    • Adapter
    • Bridge
    • Composite
    • Decorator
    • Facade
    • Flyweight
    • Proxy
  3. Behavioural Patterns
    • Chain of Responsibility
    • Command
    • Interpreter
    • Iterator
    • Mediator
    • Memento
    • Observer
    • State
    • Strategy
    • Template Method
    • Visitor

1. Creational Patterns

Factory Method

The Factory Method is a creational pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

Explanation:

  • Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.
  • Problem: A class cannot anticipate the class of objects it needs to create.
  • Solution: Create a method in the base class that will create objects. Let derived classes override this method to create objects of specific types.

Use Case: Document Management System

Imagine you are designing a document management system that can handle different types of documents (e.g., Word, PDF). The Factory Method pattern allows you to create a framework where the specific type of document is created by the subclasses, making it easy to extend the system to support new document types in the future.

Sample Python Code:

from abc import ABC, abstractmethod

# Product Interface
class Document(ABC):
    @abstractmethod
    def open(self) -> str:
        pass

# Concrete Products
class WordDocument(Document):
    def open(self) -> str:
        return "Opening Word document."

class PDFDocument(Document):
    def open(self) -> str:
        return "Opening PDF document."

# Creator Interface
class Application(ABC):
    @abstractmethod
    def create_document(self) -> Document:
        pass

    def open_document(self) -> str:
        document = self.create_document()
        result = f"Application: The application is working with {document.open()}"
        return result

# Concrete Creators
class WordApplication(Application):
    def create_document(self) -> Document:
        return WordDocument()

class PDFApplication(Application):
    def create_document(self) -> Document:
        return PDFDocument()

# Client code
def client_code(application: Application) -> None:
    print(f"Client: I'm not aware of the application's class, but it still works.\n"
          f"{application.open_document()}")

if __name__ == "__main__":
    print("App: Launched with WordApplication.")
    client_code(WordApplication())
    print("\n")
    print("App: Launched with PDFApplication.")
    client_code(PDFApplication())

Abstract Factory

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Explanation:

  • Intent: Provide an interface for creating families of related or dependent objects.
  • Problem: A system should be independent of how its products are created, composed, and represented.
  • Solution: Declare an abstract base factory class with methods to create abstract product types.

Use Case: UI Elements for Different Operating Systems

Imagine you are designing a cross-platform UI library. You want to create UI elements like buttons and checkboxes, but these elements need to look and behave differently on different operating systems (e.g., Windows and MacOS). The Abstract Factory pattern allows you to create these UI elements without coupling your code to specific implementations for each operating system.

Sample Python Code:

from abc import ABC, abstractmethod

# Abstract Product Interfaces
class Button(ABC):
    @abstractmethod
    def paint(self) -> str:
        pass

class Checkbox(ABC):
    @abstractmethod
    def paint(self) -> str:
        pass

# Concrete Products for Windows
class WindowsButton(Button):
    def paint(self) -> str:
        return "WindowsButton: Painted Windows Button."

class WindowsCheckbox(Checkbox):
    def paint(self) -> str:
        return "WindowsCheckbox: Painted Windows Checkbox."

# Concrete Products for MacOS
class MacOSButton(Button):
    def paint(self) -> str:
        return "MacOSButton: Painted MacOS Button."

class MacOSCheckbox(Checkbox):
    def paint(self) -> str:
        return "MacOSCheckbox: Painted MacOS Checkbox."

# Abstract Factory Interface
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass

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

# Concrete Factories for Windows
class WindowsFactory(GUIFactory):
    def create_button(self) -> Button:
        return WindowsButton()

    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()

# Concrete Factories for MacOS
class MacOSFactory(GUIFactory):
    def create_button(self) -> Button:
        return MacOSButton()

    def create_checkbox(self) -> Checkbox:
        return MacOSCheckbox()

# Client Code
def client_code(factory: GUIFactory) -> None:
    button = factory.create_button()
    checkbox = factory.create_checkbox()

    print(button.paint())
    print(checkbox.paint())

if __name__ == "__main__":
    print("Client: Testing client code with the Windows factory:")
    client_code(WindowsFactory())
    print("\n")
    print("Client: Testing the same client code with the MacOS factory:")
    client_code(MacOSFactory())

Singleton

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

Explanation:

  • Intent: Ensure a class has only one instance, and provide a global point of access to it.
  • Problem: Controlling access to a single instance.
  • Solution: Hide the constructor and provide a static method to get the instance.

Use Case: Logger

Imagine you are developing a logging system where you want to ensure that only one instance of the logger exists throughout the application. This Singleton instance can be accessed globally to log messages.

Sample Python Code:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.log = []

    def add_log(self, message: str) -> None:
        self.log.append(message)
        print(f"Log added: {message}")

    def show_log(self) -> None:
        print("Log history:")
        for message in self.log:
            print(message)

# Client Code
if __name__ == "__main__":
    logger1 = Logger()
    logger1.add_log("First log message.")
    logger1.add_log("Second log message.")

    logger2 = Logger()
    logger2.add_log("Third log message.")

    if id(logger1) == id(logger2):
        print("Singleton works, both variables contain the same instance.")
    else:
        print("Singleton failed, variables contain different instances.")

    logger1.show_log()

Builder

The Builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations.

Explanation:

  • Intent: Separate the construction of a complex object from its representation.
  • Problem: A complex object needs to be created.
  • Solution: Define a Builder class that constructs parts of a Product object step by step.

Use Case: Building a House

Imagine a scenario where we are building different types of houses. A house can have various parts like walls, doors, and windows. The construction process for different types of houses (e.g., a basic house vs. a luxury house) can be different. The Builder pattern allows us to separate the construction process from the final house representation.

Sample Python Code:

from abc import ABC, abstractmethod

# Product
class House:
    def __init__(self):
        self.parts = []

    def add(self, part: str) -> None:
        self.parts.append(part)

    def list_parts(self) -> None:
        print(f"House parts: {', '.join(self.parts)}")

# Builder Interface
class Builder(ABC):
    @abstractmethod
    def build_walls(self) -> None:
        pass

    @abstractmethod
    def build_doors(self) -> None:
        pass

    @abstractmethod
    def build_windows(self) -> None:
        pass

    @abstractmethod
    def build_roof(self) -> None:
        pass

# Concrete Builder
class HouseBuilder(Builder):
    def __init__(self) -> None:
        self.reset()

    def reset(self) -> None:
        self._house = House()

    def build_walls(self) -> None:
        self._house.add("Walls")

    def build_doors(self) -> None:
        self._house.add("Doors")

    def build_windows(self) -> None:
        self._house.add("Windows")

    def build_roof(self) -> None:
        self._house.add("Roof")

    def get_product(self) -> House:
        house = self._house
        self.reset()  # Reset the builder for the next product
        return house

# Director
class Director:
    def __init__(self) -> None:
        self._builder = None

    @property
    def builder(self) -> Builder:
        return self._builder

    @builder.setter
    def builder(self, builder: Builder) -> None:
        self._builder = builder

    def build_basic_house(self) -> None:
        self.builder.build_walls()
        self.builder.build_doors()

    def build_luxury_house(self) -> None:
        self.builder.build_walls()
        self.builder.build_doors()
        self.builder.build_windows()
        self.builder.build_roof()

# Client Code
if __name__ == "__main__":
    director = Director()
    builder = HouseBuilder()
    director.builder = builder

    print("Building a basic house:")
    director.build_basic_house()
    basic_house = builder.get_product()
    basic_house.list_parts()

    print("\nBuilding a luxury house:")
    director.build_luxury_house()
    luxury_house = builder.get_product()
    luxury_house.list_parts()

    print("\nBuilding a custom house:")
    builder.build_walls()
    builder.build_windows()
    custom_house = builder.get_product()
    custom_house.list_parts()

Prototype

The Prototype pattern is used to create a new object by copying an existing object, known as the prototype.

Explanation:

  • Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
  • Problem: When the cost of creating a new object is expensive.
  • Solution: Clone the prototype instead of creating new instances from scratch.

Use Case: Document Management System

Imagine a document management system where creating new documents involves a lot of setup and configuration. Using the Prototype pattern, we can create new documents by copying existing ones, making it easier and faster to create new documents.

Sample Python Code:

import copy

class Prototype:
    def __init__(self):
        self._objects = {}

    def register_object(self, name, obj):
        self._objects[name] = obj

    def unregister_object(self, name):
        if name in self._objects:
            del self._objects[name]

    def clone(self, name, **attrs):
        if name not in self._objects:
            raise ValueError(f"Object named '{name}' is not registered.")
        obj = copy.deepcopy(self._objects[name])
        obj.__dict__.update(attrs)
        return obj

# Example Usage
class Document:
    def __init__(self, title: str, content: str) -> None:
        self.title = title
        self.content = content

    def __str__(self) -> str:
        return f"Document: {self.title}, Content: {self.content}"

if __name__ == "__main__":
    prototype = Prototype()

    # Register a default document prototype
    default_doc = Document("Default Title", "Default Content")
    prototype.register_object('default_doc', default_doc)

    # Clone the default document with new attributes
    new_doc = prototype.clone('default_doc', title="New Title", content="New Content")

    print(f"Original document: {default_doc}")
    print(f"Cloned document: {new_doc}")

2. Structural Patterns

Adapter

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

Explanation:

  • Intent: Convert the interface of a class into another interface clients expect.
  • Problem: Incompatible interfaces.
  • Solution: Create an adapter class that translates calls from the interface of a client to the interface of an incompatible class.

Use Case: Media Player

Imagine you have a media player application that can play audio files. The media player expects all audio files to be in a specific format, but you have a new type of audio file with a different interface. The Adapter pattern allows the media player to play this new type of audio file without modifying the existing media player code.

Sample Python Code:

class MediaPlayer:
    def play(self) -> str:
        return "MediaPlayer: Playing audio."

class AdvancedAudioPlayer:
    def play_advanced_audio(self) -> str:
        return "AdvancedAudioPlayer: Playing advanced audio format."

# Adapter Class
class AudioAdapter(MediaPlayer):
    def __init__(self, advanced_audio_player: AdvancedAudioPlayer) -> None:
        self._advanced_audio_player = advanced_audio_player

    def play(self) -> str:
        return f"AudioAdapter: {self._advanced_audio_player.play_advanced_audio()}"

# Client Code
def client_code(media_player: MediaPlayer) -> None:
    print(media_player.play())

if __name__ == "__main__":
    # Using the MediaPlayer directly
    print("Client: Using the MediaPlayer directly:")
    media_player = MediaPlayer()
    client_code(media_player)
    print("\n")

    # Using the AdvancedAudioPlayer via the Adapter
    advanced_audio_player = AdvancedAudioPlayer()
    adapter = AudioAdapter(advanced_audio_player)
    print("Client: Using the AdvancedAudioPlayer via the Adapter:")
    client_code(adapter)

Bridge

The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently.

Explanation:

  • Intent: Decouple an abstraction from its implementation.
  • Problem: An abstraction and its implementation should not be tightly coupled.
  • Solution: Use a bridge to separate the abstraction from its implementation.

Use Case: Remote Controls for Different Devices

Imagine you are designing remote controls for various devices (e.g., TVs and Radios). The functionality of the remote control (e.g., turning on/off, changing channels) can be abstracted from the specific device it controls. The Bridge pattern allows you to separate the abstraction (remote control) from its implementation (the specific device).

Sample Python Code:

from abc import ABC, abstractmethod

# Abstraction
class RemoteControl:
    def __init__(self, device: 'Device') -> None:
        self._device = device

    def turn_on(self) -> str:
        return self._device.turn_on()

    def turn_off(self) -> str:
        return self._device.turn_off()

    def set_channel(self, channel: int) -> str:
        return self._device.set_channel(channel)

# Refined Abstraction
class AdvancedRemoteControl(RemoteControl):
    def mute(self) -> str:
        return self._device.mute()

# Implementation Interface
class Device(ABC):
    @abstractmethod
    def turn_on(self) -> str:
        pass

    @abstractmethod
    def turn_off(self) -> str:
        pass

    @abstractmethod
    def set_channel(self, channel: int) -> str:
        pass

    @abstractmethod
    def mute(self) -> str:
        pass

# Concrete Implementations
class TV(Device):
    def turn_on(self) -> str:
        return "TV: Turning on"

    def turn_off(self) -> str:
        return "TV: Turning off"

    def set_channel(self, channel: int) -> str:
        return f"TV: Setting channel to {channel}"

    def mute(self) -> str:
        return "TV: Muting"

class Radio(Device):
    def turn_on(self) -> str:
        return "Radio: Turning on"

    def turn_off(self) -> str:
        return "Radio: Turning off"

    def set_channel(self, channel: int) -> str:
        return f"Radio: Setting channel to {channel}"

    def mute(self) -> str:
        return "Radio: Muting"

# Client Code
def client_code(remote: RemoteControl) -> None:
    print(remote.turn_on())
    print(remote.set_channel(5))
    print(remote.turn_off())

def client_code_advanced(remote: AdvancedRemoteControl) -> None:
    print(remote.turn_on())
    print(remote.set_channel(5))
    print(remote.mute())
    print(remote.turn_off())

if __name__ == "__main__":
    tv = TV()
    tv_remote = RemoteControl(tv)
    print("Client: Testing TV Remote Control:")
    client_code(tv_remote)

    print("\n")

    radio = Radio()
    radio_remote = RemoteControl(radio)
    print("Client: Testing Radio Remote Control:")
    client_code(radio_remote)

    print("\n")

    advanced_tv_remote = AdvancedRemoteControl(tv)
    print("Client: Testing Advanced TV Remote Control:")
    client_code_advanced(advanced_tv_remote)

Composite

The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

Explanation:

  • Intent: Compose objects into tree structures to represent part-whole hierarchies.
  • Problem: Treating individual objects and compositions of objects uniformly.
  • Solution: Define a composite object that can hold individual objects or other composite objects.

Use Case: Organisation Structure

Imagine an organisation where you have employees and managers. Each manager can manage multiple employees and other managers. The Composite pattern allows us to represent this structure in a tree form, where both employees and managers are treated uniformly.

Sample Python Code:

from abc import ABC, abstractmethod
from typing import List

# Component Interface
class Employee(ABC):
    @abstractmethod
    def show_details(self) -> str:
        pass

    def add(self, employee: 'Employee') -> None:
        pass

    def remove(self, employee: 'Employee') -> None:
        pass

    def is_composite(self) -> bool:
        return False

# Leaf
class Developer(Employee):
    def __init__(self, name: str, position: str) -> None:
        self.name = name
        self.position = position

    def show_details(self) -> str:
        return f"{self.name} ({self.position})"

# Leaf
class Designer(Employee):
    def __init__(self, name: str, position: str) -> None:
        self.name = name
        self.position = position

    def show_details(self) -> str:
        return f"{self.name} ({self.position})"

# Composite
class Manager(Employee):
    def __init__(self, name: str, position: str) -> None:
        self.name = name
        self.position = position
        self._employees: List[Employee] = []

    def add(self, employee: Employee) -> None:
        self._employees.append(employee)

    def remove(self, employee: Employee) -> None:
        self._employees.remove(employee)

    def is_composite(self) -> bool:
        return True

    def show_details(self) -> str:
        results = [f"{self.name} ({self.position})"]
        for employee in self._employees:
            results.append(f"  {employee.show_details()}")
        return "\n".join(results)

# Client Code
def client_code(employee: Employee) -> None:
    print(employee.show_details())

if __name__ == "__main__":
    # Leaf nodes
    dev1 = Developer("John Doe", "Senior Developer")
    dev2 = Developer("Jane Smith", "Junior Developer")
    designer = Designer("Emily Davis", "Senior Designer")

    # Composite node
    manager = Manager("Alice Johnson", "Development Manager")
    manager.add(dev1)
    manager.add(dev2)
    manager.add(designer)

    print("Client: I've got a composite manager with employees:")
    client_code(manager)

Decorator

The Decorator pattern allows behaviour to be added to an individual object, dynamically, without affecting the behaviour of other objects from the same class.

Explanation:

  • Intent: Attach additional responsibilities to an object dynamically.
  • Problem: Extending functionality without subclassing.
  • Solution: Use composition to add new behaviour.

Use Case: Coffee Shop

Imagine a coffee shop where you can order different types of coffee and add various condiments to them. The condiments are like decorators that add functionality (or flavour) to the coffee.

Sample Python Code:

from abc import ABC, abstractmethod

# Component Interface
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass

    @abstractmethod
    def description(self) -> str:
        pass

# Concrete Component
class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 5.0  # Base cost of the coffee

    def description(self) -> str:
        return "Simple Coffee"

# Base Decorator
class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee) -> None:
        self._coffee = coffee

    def cost(self) -> float:
        return self._coffee.cost()

    def description(self) -> str:
        return self._coffee.description()

# Concrete Decorators
class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 1.0  # Milk costs extra

    def description(self) -> str:
        return f"{self._coffee.description()}, Milk"

class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5  # Sugar costs extra

    def description(self) -> str:
        return f"{self._coffee.description()}, Sugar"

# Client Code
def client_code(coffee: Coffee) -> None:
    print(f"Description: {coffee.description()}")
    print(f"Cost: {coffee.cost()}")

if __name__ == "__main__":
    # Simple Coffee
    simple = SimpleCoffee()
    print("Client: I've got a simple coffee:")
    client_code(simple)
    print("\n")

    # Coffee with Milk
    coffee_with_milk = MilkDecorator(simple)
    print("Client: Now I've got a coffee with milk:")
    client_code(coffee_with_milk)
    print("\n")

    # Coffee with Milk and Sugar
    coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
    print("Client: Now I've got a coffee with milk and sugar:")
    client_code(coffee_with_milk_and_sugar)

Facade

The Facade pattern provides a simplified interface to a complex subsystem.

Explanation:

  • Intent: Provide a unified interface to a set of interfaces in a subsystem.
  • Problem: A system is very complex and difficult to use.
  • Solution: Use a facade to provide a simplified interface.

Use Case: Home Theatre System

Imagine a home theatre system with several components such as a DVD player, projector, sound system, and lights. Each component has its own interface with multiple methods, making it cumbersome to operate them individually. The Facade pattern can provide a unified interface to simplify these operations.

Sample Python Code:

class DVDPlayer:
    def on(self) -> str:
        return "DVD Player: On"

    def play(self, movie: str) -> str:
        return f"DVD Player: Playing '{movie}'"

class Projector:
    def on(self) -> str:
        return "Projector: On"

    def wide_screen_mode(self) -> str:
        return "Projector: Set to widescreen mode (16x9 aspect ratio)"

class SoundSystem:
    def on(self) -> str:
        return "Sound System: On"

    def set_volume(self, volume: int) -> str:
        return f"Sound System: Volume set to {volume}"

class Lights:
    def dim(self, level: int) -> str:
        return f"Lights: Dimming to {level}%"

class HomeTheaterFacade:
    def __init__(self, dvd: DVDPlayer, projector: Projector, sound: SoundSystem, lights: Lights) -> None:
        self._dvd = dvd
        self._projector = projector
        self._sound = sound
        self._lights = lights

    def watch_movie(self, movie: str) -> str:
        results = []
        results.append("Home Theater: Get ready to watch a movie...")
        results.append(self._lights.dim(10))
        results.append(self._projector.on())
        results.append(self._projector.wide_screen_mode())
        results.append(self._sound.on())
        results.append(self._sound.set_volume(5))
        results.append(self._dvd.on())
        results.append(self._dvd.play(movie))
        return "\n".join(results)

# Client Code
def client_code(facade: HomeTheaterFacade) -> None:
    print(facade.watch_movie("Inception"))

if __name__ == "__main__":
    dvd = DVDPlayer()
    projector = Projector()
    sound = SoundSystem()
    lights = Lights()
    home_theater = HomeTheaterFacade(dvd, projector, sound, lights)
    client_code(home_theater)

Flyweight

The Flyweight pattern is used to minimise memory usage or computational expenses by sharing as much as possible with similar objects.

Explanation:

  • Intent: Use sharing to support large numbers of fine-grained objects efficiently.
  • Problem: Too many objects can consume too much memory.
  • Solution: Share common parts of objects instead of creating new ones.

Use Case: Text Editor Character Rendering

Imagine you are developing a text editor. Each character on the screen can be considered an object. If we create an object for every single character with all its attributes (font, size, colour, position), it would consume a lot of memory. The Flyweight pattern can help by sharing the common attributes among characters.

Sample Python Code:

class Flyweight:
    def __init__(self, shared_state: str) -> None:
        self._shared_state = shared_state

    def operation(self, unique_state: str) -> None:
        print(f"Flyweight: Displaying shared ({self._shared_state}) and unique ({unique_state}) state.")

class FlyweightFactory:
    _flyweights = {}

    def get_flyweight(self, shared_state: str) -> Flyweight:
        if shared_state not in self._flyweights:
            print(f"FlyweightFactory: Can't find a flyweight, creating new one.")
            self._flyweights[shared_state] = Flyweight(shared_state)
        else:
            print(f"FlyweightFactory: Reusing existing flyweight.")
        return self._flyweights[shared_state]

    def list_flyweights(self) -> None:
        count = len(self._flyweights)
        print(f"FlyweightFactory: I have {count} flyweights:")
        for key in self._flyweights.keys():
            print(key)

# Client Code
def add_character_to_document(factory: FlyweightFactory, character: str, font: str, size: int, color: str, position: tuple) -> None:
    shared_state = f"{character}_{font}_{size}_{color}"
    unique_state = f"Position: {position}"
    flyweight = factory.get_flyweight(shared_state)
    flyweight.operation(unique_state)

if __name__ == "__main__":
    factory = FlyweightFactory()

    add_character_to_document(factory, "H", "Arial", 12, "black", (0, 0))
    add_character_to_document(factory, "e", "Arial", 12, "black", (1, 0))
    add_character_to_document(factory, "l", "Arial", 12, "black", (2, 0))
    add_character_to_document(factory, "l", "Arial", 12, "black", (3, 0))
    add_character_to_document(factory, "o", "Arial", 12, "black", (4, 0))

    add_character_to_document(factory, "H", "Arial", 14, "blue", (0, 1))
    add_character_to_document(factory, "e", "Arial", 14, "blue", (1, 1))
    add_character_to_document(factory, "l", "Arial", 14, "blue", (2, 1))
    add_character_to_document(factory, "l", "Arial", 14, "blue", (3, 1))
    add_character_to_document(factory, "o", "Arial", 14, "blue", (4, 1))

    factory.list_flyweights()

Proxy

The Proxy pattern provides a surrogate or placeholder for another object to control access to it.

Explanation:

  • Intent: Provide a surrogate or placeholder for another object to control access to it.
  • Problem: Control access to an object.
  • Solution: Use a proxy that delegates the requests to the real subject.

Use Case: Access Control for a Secure File Server

Imagine you are developing a secure file server that handles file access operations. You want to ensure that only authorised users can access this server, and you want to log all access attempts. The Proxy pattern can help by providing a proxy that performs access control and logging before delegating requests to the actual file server.

Sample Python Code:

from abc import ABC, abstractmethod

# Abstract File Server Interface
class FileServer(ABC):
    @abstractmethod
    def access_file(self, filename: str) -> None:
        pass

# Real File Server
class SecureFileServer(FileServer):
    def access_file(self, filename: str) -> None:
        print(f"SecureFileServer: Accessing the file {filename}.")

# Proxy
class FileServerProxy(FileServer):
    def __init__(self, real_server: SecureFileServer) -> None:
        self._real_server = real_server

    def access_file(self, filename: str) -> None:
        if self.check_access():
            self._real_server.access_file(filename)
            self.log_access(filename)

    def check_access(self) -> bool:
        print("FileServerProxy: Checking access before accessing the file.")
        # Simulate access control logic
        return True

    def log_access(self, filename: str) -> None:
        print(f"FileServerProxy: Logging access to the file {filename}.")

# Client Code
def client_code(file_server: FileServer) -> None:
    file_server.access_file("important_document.txt")

if __name__ == "__main__":
    print("Client: Accessing the file server directly:")
    real_server = SecureFileServer()
    client_code(real_server)
    print("\n")

    print("Client: Accessing the file server through the proxy:")
    proxy = FileServerProxy(real_server)
    client_code(proxy)

3. Behavioural Patterns

Chain of Responsibility

The Chain of Responsibility pattern allows an object to pass the request along a chain of potential handlers until one of them handles the request.

Explanation:

  • Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.
  • Problem: Decouple request senders from receivers.
  • Solution: Pass the request along a chain of handlers.

Use Case: Customer Support Ticket System

Imagine you are developing a customer support ticket system where different levels of support handle different types of issues. Simple issues are handled by a first-level support agent, more complex issues are handled by a second-level support agent, and the most complex issues are handled by a specialist. The Chain of Responsibility pattern allows you to pass the ticket through different support levels until it is handled.

Sample Python Code:

from abc import ABC, abstractmethod
from typing import Optional

# Handler Interface
class Handler(ABC):
    @abstractmethod
    def set_next(self, handler: 'Handler') -> 'Handler':
        pass

    @abstractmethod
    def handle(self, request: str) -> Optional[str]:
        pass

# Abstract Handler
class AbstractHandler(Handler):
    _next_handler: Handler = None

    def set_next(self, handler: Handler) -> Handler:
        self._next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request: str) -> Optional[str]:
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

# Concrete Handlers
class FirstLevelSupportHandler(AbstractHandler):
    def handle(self, request: str) -> Optional[str]:
        if request == "Password reset":
            return f"FirstLevelSupport: I'll handle the {request}."
        return super().handle(request)

class SecondLevelSupportHandler(AbstractHandler):
    def handle(self, request: str) -> Optional[str]:
        if request == "Software installation":
            return f"SecondLevelSupport: I'll handle the {request}."
        return super().handle(request)

class SpecialistSupportHandler(AbstractHandler):
    def handle(self, request: str) -> Optional[str]:
        if request == "Server outage":
            return f"SpecialistSupport: I'll handle the {request}."
        return super().handle(request)

# Client Code
def client_code(handler: Handler) -> None:
    for issue in ["Password reset", "Software installation", "Server outage", "Unknown issue"]:
        print(f"\nClient: Who can handle a {issue}?")
        result = handler.handle(issue)
        if result:
            print(f"  {result}")
        else:
            print(f"  {issue} was not handled.")

if __name__ == "__main__":
    first_level = FirstLevelSupportHandler()
    second_level = SecondLevelSupportHandler()
    specialist = SpecialistSupportHandler()

    first_level.set_next(second_level).set_next(specialist)

    print("Chain: First Level > Second Level > Specialist")
    client_code(first_level)
    print("\n")

    print("Subchain: Second Level > Specialist")
    client_code(second_level)

Command

The Command pattern encapsulates a request as an object, thereby allowing for parameterisation of clients with different requests, queuing of requests, and logging the requests.

Explanation:

  • Intent: Encapsulate a request as an object.
  • Problem: Parameterise clients with different requests, queue requests, and log requests.
  • Solution: Create command objects with a common interface to execute the requests.

Use Case: Home Automation System

Imagine you are developing a home automation system where you can control various devices like lights, thermostats, and security systems. Each device can have different commands such as turning on, turning off, setting a temperature, etc. The Command pattern allows you to encapsulate these requests as objects and execute them as needed.

Sample Python Code:

from abc import ABC, abstractmethod

# Command Interface
class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

# Simple Command
class TurnOnLightCommand(Command):
    def __init__(self, payload: str) -> None:
        self._payload = payload

    def execute(self) -> None:
        print(f"TurnOnLightCommand: Turning on the light ({self._payload})")

# Complex Command
class AdjustThermostatCommand(Command):
    def __init__(self, receiver: 'Thermostat', temperature: int) -> None:
        self._receiver = receiver
        self._temperature = temperature

    def execute(self) -> None:
        print("AdjustThermostatCommand: Adjusting the thermostat", end="")
        self._receiver.set_temperature(self._temperature)

class Thermostat:
    def set_temperature(self, temperature: int) -> None:
        print(f"\nThermostat: Setting temperature to {temperature} degrees.")

class Invoker:
    _on_start = None
    _on_finish = None

    def set_on_start(self, command: Command) -> None:
        self._on_start = command

    def set_on_finish(self, command: Command) -> None:
        self._on_finish = command

    def do_something_important(self) -> None:
        print("Invoker: Does anybody want something done before I begin?")
        if isinstance(self._on_start, Command):
            self._on_start.execute()

        print("Invoker: ...doing something really important...")

        print("Invoker: Does anybody want something done after I finish?")
        if isinstance(self._on_finish, Command):
            self._on_finish.execute()

# Client Code
if __name__ == "__main__":
    invoker = Invoker()
    invoker.set_on_start(TurnOnLightCommand("Living Room Light"))
    thermostat = Thermostat()
    invoker.set_on_finish(AdjustThermostatCommand(thermostat, 22))
    invoker.do_something_important()

Interpreter

The Interpreter pattern provides a way to evaluate language grammar or expressions. This pattern involves implementing an expression interface which tells to interpret a particular context.

Explanation:

  • Intent: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
  • Problem: Designing a grammar and an interpreter for it.
  • Solution: Define classes that represent grammar rules and interpret context.

Use Case: Simple Chatbot Command Interpretation

Imagine you are developing a simple chatbot that can understand and respond to basic commands. The chatbot needs to evaluate user input based on predefined grammar rules and interpret the commands accordingly. The Interpreter pattern can help by defining these grammar rules and implementing the interpretation logic.

Sample Python Code:

from abc import ABC, abstractmethod

# Abstract Expression
class AbstractExpression(ABC):
    @abstractmethod
    def interpret(self, context: str) -> bool:
        pass

# Terminal Expression
class TerminalExpression(AbstractExpression):
    def __init__(self, data: str) -> None:
        self._data = data

    def interpret(self, context: str) -> bool:
        return self._data in context

# Or Expression
class OrExpression(AbstractExpression):
    def __init__(self, expr1: AbstractExpression, expr2: AbstractExpression) -> None:
        self._expr1 = expr1
        self._expr2 = expr2

    def interpret(self, context: str) -> bool:
        return self._expr1.interpret(context) or self._expr2.interpret(context)

# And Expression
class AndExpression(AbstractExpression):
    def __init__(self, expr1: AbstractExpression, expr2: AbstractExpression) -> None:
        self._expr1 = expr1
        self._expr2 = expr2

    def interpret(self, context: str) -> bool:
        return self._expr1.interpret(context) and self._expr2.interpret(context)

# Client Code
def client_code() -> None:
    # Grammar rules for interpreting commands
    greet = TerminalExpression("Hello")
    bye = TerminalExpression("Goodbye")
    is_greeting = OrExpression(greet, bye)

    admin = TerminalExpression("Admin")
    password = TerminalExpression("Password")
    is_admin_login = AndExpression(admin, password)

    # Example input
    input1 = "Hello John"
    input2 = "Goodbye John"
    input3 = "Admin Password"
    input4 = "Admin John"

    print(f"Is '{input1}' a greeting? {is_greeting.interpret(input1)}")
    print(f"Is '{input2}' a greeting? {is_greeting.interpret(input2)}")
    print(f"Is '{input3}' an admin login? {is_admin_login.interpret(input3)}")
    print(f"Is '{input4}' an admin login? {is_admin_login.interpret(input4)}")

if __name__ == "__main__":
    client_code()

Iterator

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Explanation:

  • Intent: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
  • Problem: Need to traverse a collection without exposing its internal structure.
  • Solution: Define an iterator class with methods to traverse the collection.

Use Case: Traversing a Collection of Words

Imagine you are developing a text processing application that handles a collection of words. You want to be able to traverse this collection in both forward and reverse order without exposing the internal representation of the collection. The Iterator pattern can help by defining an iterator that provides sequential access to the elements.

Sample Python Code:

from collections.abc import Iterator, Iterable
from typing import Any, List

# Concrete Iterator
class AlphabeticalOrderIterator(Iterator):
    _position: int = None

    def __init__(self, collection: List[Any], reverse: bool = False) -> None:
        self._collection = collection
        self._reverse = reverse
        self._position = -1 if reverse else 0

    def __next__(self) -> Any:
        try:
            value = self._collection[self._position]
            self._position += -1 if self._reverse else 1
        except IndexError:
            raise StopIteration()

        return value

# Concrete Iterable Collection
class WordsCollection(Iterable):
    def __init__(self, collection: List[Any] = []) -> None:
        self._collection = collection

    def __iter__(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection)

    def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection, True)

    def add_item(self, item: Any) -> None:
        self._collection.append(item)

# Client Code
if __name__ == "__main__":
    collection = WordsCollection()
    collection.add_item("First")
    collection.add_item("Second")
    collection.add_item("Third")

    print("Straight traversal:")
    for item in collection:
        print(item)
    print("\n")

    print("Reverse traversal:")
    for item in collection.get_reverse_iterator():
        print(item)

Mediator

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

Explanation:

  • Intent: Define an object that encapsulates how a set of objects interact.
  • Problem: Tight coupling between objects.
  • Solution: Use a mediator object to handle interactions.

Use Case: Air Traffic Control System

Imagine you are developing an air traffic control system where multiple airplanes (components) need to coordinate their actions such as landing, taking off, and taxiing. Instead of each airplane communicating directly with every other airplane, which can lead to a complex and tightly coupled system, you can use a mediator (the control tower) to manage the interactions.

Sample Python Code:

from __future__ import annotations
from abc import ABC, abstractmethod

# Mediator Interface
class Mediator(ABC):
    @abstractmethod
    def notify(self, sender: object, event: str) -> None:
        pass

# Concrete Mediator
class ControlTower(Mediator):
    def __init__(self, airplane1: Airplane1, airplane2: Airplane2) -> None:
        self._airplane1 = airplane1
        self._airplane1.mediator = self
        self._airplane2 = airplane2
        self._airplane2.mediator = self

    def notify(self, sender: object, event: str) -> None:
        if event == "Landing":
            print("ControlTower: Airplane1 is landing, informing Airplane2 to hold.")
            self._airplane2.hold_position()
        elif event == "TakingOff":
            print("ControlTower: Airplane2 is taking off, informing Airplane1 to hold.")
            self._airplane1.hold_position()

# Base Component
class Airplane:
    def __init__(self, mediator: Mediator = None) -> None:
        self._mediator = mediator

    @property
    def mediator(self) -> Mediator:
        return self._mediator

    @mediator.setter
    def mediator(self, mediator: Mediator) -> None:
        self._mediator = mediator

# Concrete Components
class Airplane1(Airplane):
    def land(self) -> None:
        print("Airplane1 is landing.")
        self.mediator.notify(self, "Landing")

    def hold_position(self) -> None:
        print("Airplane1 is holding position.")

class Airplane2(Airplane):
    def take_off(self) -> None:
        print("Airplane2 is taking off.")
        self.mediator.notify(self, "TakingOff")

    def hold_position(self) -> None:
        print("Airplane2 is holding position.")

# Client Code
if __name__ == "__main__":
    airplane1 = Airplane1()
    airplane2 = Airplane2()
    control_tower = ControlTower(airplane1, airplane2)

    print("Client triggers airplane1 to land.")
    airplane1.land()

    print("\nClient triggers airplane2 to take off.")
    airplane2.take_off()

Memento

The Memento pattern provides the ability to restore an object to its previous state. It is used for saving and restoring the internal state of an object.

Explanation:

  • Intent: Capture and externalise an object’s internal state without violating encapsulation.
  • Problem: Need to restore an object to its previous state.
  • Solution: Use a memento object to store the state.

Use Case: Text Editor Undo Functionality

Imagine you are developing a text editor that allows users to write and edit documents. The editor should have an undo feature that lets users revert to previous versions of the document. The Memento pattern can help by capturing the state of the document at various points and allowing the user to restore it.

Sample Python Code:

from datetime import datetime
from typing import List

class Memento:
    def __init__(self, state: str) -> None:
        self._state = state
        self._date = str(datetime.now())[:19]

    def get_state(self) -> str:
        return self._state

    def get_name(self) -> str:
        return f"{self._date} / ({self._state})"

    def get_date(self) -> str:
        return self._date

class Originator:
    def __init__(self, state: str) -> None:
        self._state = state
        print(f"Originator: My initial state is: {self._state}")

    def do_something(self) -> None:
        print("Originator: I'm doing something important.")
        self._state = self._generate_random_string(30)
        print(f"Originator: and my state has changed to: {self._state}")

    def _generate_random_string(self, length: int = 10) -> str:
        import random
        import string
        return ''.join(random.choices(string.ascii_letters, k=length))

    def save(self) -> Memento:
        return Memento(self._state)

    def restore(self, memento: Memento) -> None:
        self._state = memento.get_state()
        print(f"Originator: My state has changed to: {self._state}")

class Caretaker:
    def __init__(self, originator: Originator) -> None:
        self._mementos: List[Memento] = []
        self._originator = originator

    def backup(self) -> None:
        print("\nCaretaker: Saving Originator's state...")
        self._mementos.append(self._originator.save())

    def undo(self) -> None:
        if not self._mementos:
            return

        memento = self._mementos.pop()
        print(f"Caretaker: Restoring state to: {memento.get_name()}")
        self._originator.restore(memento)

    def show_history(self) -> None:
        print("Caretaker: Here's the list of mementos:")
        for memento in self._mementos:
            print(memento.get_name())

# Client Code
if __name__ == "__main__":
    originator = Originator("Initial document content.")
    caretaker = Caretaker(originator)

    caretaker.backup()
    originator.do_something()

    caretaker.backup()
    originator.do_something()

    caretaker.backup()
    originator.do_something()

    print()
    caretaker.show_history()

    print("\nClient: Now, let's rollback!\n")
    caretaker.undo()

    print("\nClient: Once more!\n")
    caretaker.undo()

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.

Explanation:

  • Intent: Define a one-to-many dependency between objects.
  • Problem: Need to notify multiple objects when the state of one object changes.
  • Solution: Use an observer interface and concrete observer classes.

Use Case: Weather Monitoring System

Imagine you are developing a weather monitoring system where multiple display elements (e.g., temperature display, humidity display) need to be updated whenever the weather data changes. The Observer pattern can help by defining a subject (weather station) and observers (display elements) that are notified whenever the weather data changes.

Sample Python Code:

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
    @abstractmethod
    def update(self, weather_data: WeatherData) -> None:
        pass

class WeatherData:
    def __init__(self) -> None:
        self._observers: List[Observer] = []
        self._temperature: float = None
        self._humidity: float = None

    def attach(self, observer: Observer) -> None:
        print("WeatherData: Attached an observer.")
        self._observers.append(observer)

    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)

    def notify(self) -> None:
        print("WeatherData: Notifying observers...")
        for observer in self._observers:
            observer.update(self)

    def set_measurements(self, temperature: float, humidity: float) -> None:
        print(f"\nWeatherData: Setting measurements to Temperature: {temperature}, Humidity: {humidity}")
        self._temperature = temperature
        self._humidity = humidity
        self.notify()

    @property
    def temperature(self) -> float:
        return self._temperature

    @property
    def humidity(self) -> float:
        return self._humidity

class TemperatureDisplay(Observer):
    def update(self, weather_data: WeatherData) -> None:
        print(f"TemperatureDisplay: The current temperature is {weather_data.temperature}°C")

class HumidityDisplay(Observer):
    def update(self, weather_data: WeatherData) -> None:
        print(f"HumidityDisplay: The current humidity is {weather_data.humidity}%")

# Client Code
if __name__ == "__main__":
    weather_data = WeatherData()

    temperature_display = TemperatureDisplay()
    weather_data.attach(temperature_display)

    humidity_display = HumidityDisplay()
    weather_data.attach(humidity_display)

    weather_data.set_measurements(25.0, 65.0)
    weather_data.set_measurements(30.0, 70.0)

    weather_data.detach(temperature_display)
    weather_data.set_measurements(20.0, 60.0)

State

The State pattern allows an object to alter its behaviour when its internal state changes. The object will appear to change its class.

Explanation:

  • Intent: Allow an object to alter its behaviour when its internal state changes.
  • Problem: Need to change the behaviour of an object based on its state.
  • Solution: Use state objects to represent different states and change the state object when the state changes.
  • Use Case: Document Workflow

    Imagine you are developing a document management system where a document can be in different states such as Draft, Moderation, and Published. The behaviour of the document changes based on its state. For example, a Draft document can be edited and submitted for moderation, whereas a Published document cannot be edited.

Sample Python Code:

from abc import ABC, abstractmethod

# Context Class
class Document:
    _state = None

    def __init__(self, state: DocumentState) -> None:
        self.transition_to(state)

    def transition_to(self, state: DocumentState) -> None:
        print(f"Document: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self

    def edit(self) -> None:
        self._state.handle_edit()

    def publish(self) -> None:
        self._state.handle_publish()

# State Interface
class DocumentState(ABC):
    @property
    def context(self) -> Document:
        return self._context

    @context.setter
    def context(self, context: Document) -> None:
        self._context = context

    @abstractmethod
    def handle_edit(self) -> None:
        pass

    @abstractmethod
    def handle_publish(self) -> None:
        pass

# Concrete States
class DraftState(DocumentState):
    def handle_edit(self) -> None:
        print("DraftState: Document is being edited.")
    
    def handle_publish(self) -> None:
        print("DraftState: Document is submitted for moderation.")
        self.context.transition_to(ModerationState())

class ModerationState(DocumentState):
    def handle_edit(self) -> None:
        print("ModerationState: Cannot edit the document in moderation.")
    
    def handle_publish(self) -> None:
        print("ModerationState: Document is published.")
        self.context.transition_to(PublishedState())

class PublishedState(DocumentState):
    def handle_edit(self) -> None:
        print("PublishedState: Cannot edit the published document.")
    
    def handle_publish(self) -> None:
        print("PublishedState: Document is already published.")

# Client Code
if __name__ == "__main__":
    document = Document(DraftState())
    document.edit()
    document.publish()

    document.edit()
    document.publish()

    document.edit()
    document.publish()

Strategy

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from clients that use it.

Explanation:

  • Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
  • Problem: Need to use different algorithms based on context.
  • Solution: Define a strategy interface and concrete strategy classes.

Use Case: Sorting Algorithms

Imagine you are developing an application that needs to sort data in different ways. For example, you might need to sort data in ascending order for some operations and in descending order for others. The Strategy pattern can help by defining different sorting strategies and allowing the client to choose which one to use at runtime.

Sample Python Code:

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List

# Context Class
class Context:
    def __init__(self, strategy: SortingStrategy) -> None:
        self._strategy = strategy

    @property
    def strategy(self) -> SortingStrategy:
        return self._strategy

    @strategy.setter
    def strategy(self, strategy: SortingStrategy) -> None:
        self._strategy = strategy

    def do_some_business_logic(self) -> None:
        print("Context: Sorting data using the strategy (not sure how it'll do it)")
        result = self._strategy.sort(["e", "b", "a", "d", "c"])
        print(",".join(result))

# Strategy Interface
class SortingStrategy(ABC):
    @abstractmethod
    def sort(self, data: List[str]) -> List[str]:
        pass

# Concrete Strategies
class AscendingSortStrategy(SortingStrategy):
    def sort(self, data: List[str]) -> List[str]:
        return sorted(data)

class DescendingSortStrategy(SortingStrategy):
    def sort(self, data: List[str]) -> List[str]:
        return sorted(data, reverse=True)

# Client Code
if __name__ == "__main__":
    context = Context(AscendingSortStrategy())
    print("Client: Strategy is set to ascending sorting.")
    context.do_some_business_logic()
    print()

    print("Client: Strategy is set to descending sorting.")
    context.strategy = DescendingSortStrategy()
    context.do_some_business_logic()

Template Method

The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

Explanation:

  • Intent: Define the skeleton of an algorithm, deferring some steps to subclasses.
  • Problem: Need to allow subclasses to redefine certain steps of an algorithm without changing the algorithm’s structure.
  • Solution: Use a template method that calls abstract methods.

Use Case: Data Processing Pipeline

Imagine you are developing a data processing pipeline where different types of data need to be processed in slightly different ways. The overall structure of the pipeline is the same, but some steps need to be customised for each data type. The Template Method pattern can help by defining the skeleton of the pipeline and allowing subclasses to customise specific steps.

Sample Python Code:

from abc import ABC, abstractmethod

# Abstract Class
class DataProcessor(ABC):
    def template_method(self) -> None:
        self.extract_data()
        self.transform_data()
        self.load_data()
        self.hook1()
        self.validate_data()
        self.hook2()

    def extract_data(self) -> None:
        print("DataProcessor: Extracting data")

    def transform_data(self) -> None:
        print("DataProcessor: Transforming data")

    def load_data(self) -> None:
        print("DataProcessor: Loading data")

    @abstractmethod
    def validate_data(self) -> None:
        pass

    def hook1(self) -> None:
        pass

    def hook2(self) -> None:
        pass

# Concrete Classes
class JSONDataProcessor(DataProcessor):
    def validate_data(self) -> None:
        print("JSONDataProcessor: Validating JSON data")

class CSVDataProcessor(DataProcessor):
    def validate_data(self) -> None:
        print("CSVDataProcessor: Validating CSV data")

    def hook1(self) -> None:
        print("CSVDataProcessor: Optional Hook1 for CSV data")

# Client Code
def client_code(data_processor: DataProcessor) -> None:
    data_processor.template_method()

if __name__ == "__main__":
    print("Client: Using JSON data processor:")
    client_code(JSONDataProcessor())
    print("\n")
    print("Client: Using CSV data processor:")
    client_code(CSVDataProcessor())

Visitor

The Visitor pattern lets you define a new operation without changing the classes of the elements on which it operates. This pattern is particularly useful for managing operations on objects of complex structures.

Explanation:

  • Intent: Define a new operation without changing the classes of the elements on which it operates.
  • Problem: Need to perform operations on objects without changing their classes.
  • Solution: Use a visitor interface and concrete visitor classes.

Use Case: File System Operations

Imagine you are developing a file system management application. The file system contains various elements like files and directories. You need to perform different operations such as calculating the total size, generating a detailed report, or checking for specific file types. The Visitor pattern can help by defining these operations as visitors that can be applied to file system elements without modifying their classes.

Sample Python Code:

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List

# Visitor Interface
class Visitor(ABC):
    @abstractmethod
    def visit_file(self, element: File) -> None:
        pass

    @abstractmethod
    def visit_directory(self, element: Directory) -> None:
        pass

# Concrete Visitors
class SizeVisitor(Visitor):
    def visit_file(self, element: File) -> None:
        print(f"File: {element.name}, Size: {element.size} KB")

    def visit_directory(self, element: Directory) -> None:
        print(f"Directory: {element.name}, Total Size: {element.get_total_size()} KB")

class ReportVisitor(Visitor):
    def visit_file(self, element: File) -> None:
        print(f"Report: File {element.name} has size {element.size} KB")

    def visit_directory(self, element: Directory) -> None:
        print(f"Report: Directory {element.name} contains {len(element.children)} items")

# Component Interface
class FileSystemComponent(ABC):
    @abstractmethod
    def accept(self, visitor: Visitor) -> None:
        pass

# Concrete Components
class File(FileSystemComponent):
    def __init__(self, name: str, size: int) -> None:
        self.name = name
        self.size = size

    def accept(self, visitor: Visitor) -> None:
        visitor.visit_file(self)

class Directory(FileSystemComponent):
    def __init__(self, name: str) -> None:
        self.name = name
        self.children: List[FileSystemComponent] = []

    def add(self, component: FileSystemComponent) -> None:
        self.children.append(component)

    def get_total_size(self) -> int:
        total_size = 0
        for child in self.children:
            if isinstance(child, File):
                total_size += child.size
            elif isinstance(child, Directory):
                total_size += child.get_total_size()
        return total_size

    def accept(self, visitor: Visitor) -> None:
        visitor.visit_directory(self)
        for child in self.children:
            child.accept(visitor)

# Client Code
def client_code(components: List[FileSystemComponent], visitor: Visitor) -> None:
    for component in components:
        component.accept(visitor)

if __name__ == "__main__":
    # Creating file system structure
    file1 = File("file1.txt", 100)
    file2 = File("file2.txt", 200)
    sub_directory = Directory("subdir")
    sub_directory.add(file1)
    sub_directory.add(file2)

    root_directory = Directory("root")
    root_directory.add(sub_directory)
    root_directory.add(File("file3.txt", 300))

    components = [root_directory]

    print("Client: Generating size report with SizeVisitor:")
    size_visitor = SizeVisitor()
    client_code(components, size_visitor)

    print("\nClient: Generating detailed report with ReportVisitor:")
    report_visitor = ReportVisitor()
    client_code(components, report_visitor)

This comprehensive guide includes details, explanations, and sample Python code for each of the design patterns listed in the table of contents. These patterns are widely used in software engineering to solve various design problems in a structured and reusable manner.