Comprehensive Guide to Design Patterns in Python: Detailed Explanations and Code Examples
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
- Learn the Patterns: Study the explanations and code samples to understand how each pattern works.
- Identify Use Cases: Think about problems you’ve encountered in your own projects and how these patterns could provide solutions.
- 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
- Creational Patterns
- Factory Method
- Abstract Factory
- Singleton
- Builder
- Prototype
- Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- 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.