Design Patterns in Python: The Complete Reference
Design patterns are a cornerstone of software engineering. They provide proven solutions to recurring design problems and enable developers to build robust, scalable, and maintainable systems. This comprehensive guide covers all 23 GoF (Gang of Four) Design Patterns, complete with theory, conceptual understanding, and Python implementations.
Table of Contents
Introduction to Design Patterns
What Are Design Patterns?
Why Use Design Patterns?
Thinking About Design Patterns
Types of Design Patterns
Creational Patterns
Structural Patterns
Behavioral Patterns
Creational Patterns
Factory Method
Abstract Factory
Builder
Prototype
Singleton
Structural Patterns
Adapter
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Behavioral Patterns
Chain of Responsibility
Command
Interpreter
Iterator
Mediator
Memento
Observer
State
Strategy
Template Method
Visitor
Real-World Applications of Patterns
Summary and Best Practices
1. Introduction to Design Patterns
What Are Design Patterns?
A design pattern is a general, reusable solution to a common problem in software design. It is not a finished design that can be directly implemented but a template for solving problems in various contexts.
Why Use Design Patterns?
Reusability: Save time by using established solutions.
Maintainability: Code is easier to understand and modify.
Scalability: Solutions remain effective as the system grows.
Standardization: Shared vocabulary among developers.
Thinking About Design Patterns
Design patterns solve recurring problems by adhering to object-oriented principles such as:
Encapsulation
Inheritance
Polymorphism
Separation of concerns
Patterns are conceptual tools—apply them judiciously where they fit naturally.
2. Types of Design Patterns
1. Creational Patterns
Focus on how objects are created and instantiated, promoting flexibility in object creation.
2. Structural Patterns
Focus on object composition and relationships, ensuring that the structure is efficient and scalable.
3. Behavioral Patterns
Focus on communication and interaction between objects.
3. Creational Patterns
Factory Method
Concept
Define an interface for creating objects, but let subclasses decide which class to instantiate. Useful when the exact type of object to create isn’t known until runtime.
Theoretical Explanation
The Factory Method pattern introduces a level of abstraction in object creation, decoupling the client code from specific implementations. It adheres to the Open/Closed Principle, allowing the addition of new types without altering existing code.
Basic Implementation
from abc import ABC, abstractmethod
# Product Interface
class Animal(ABC):
@abstractmethod
def speak(self):
pass
# Concrete Products
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
# Creator
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
return None
# Usage
animal = AnimalFactory.create_animal("dog")
print(animal.speak()) # Output: Woof!
Real-World Example: Logistics Application
from abc import ABC, abstractmethod
# Product Interface
class Transport(ABC):
@abstractmethod
def deliver(self):
pass
# Concrete Products
class Truck(Transport):
def deliver(self):
return "Delivering by land in a truck."
class Ship(Transport):
def deliver(self):
return "Delivering by sea in a ship."
# Creator Interface
class Logistics(ABC):
@abstractmethod
def create_transport(self):
pass
def plan_delivery(self):
transport = self.create_transport()
return transport.deliver()
# Concrete Creators
class RoadLogistics(Logistics):
def create_transport(self):
return Truck()
class SeaLogistics(Logistics):
def create_transport(self):
return Ship()
# Usage
logistics = RoadLogistics()
print(logistics.plan_delivery()) # Output: Delivering by land in a truck.
logistics = SeaLogistics()
print(logistics.plan_delivery()) # Output: Delivering by sea in a ship.
Abstract Factory
Concept
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Theoretical Explanation
The Abstract Factory pattern creates a higher level of abstraction by grouping related factory methods. It ensures compatibility between related products, making it especially useful for systems that require cross-compatible objects.
Basic Implementation
from abc import ABC, abstractmethod
# Abstract Products
class Button(ABC):
@abstractmethod
def render(self):
pass
class Checkbox(ABC):
@abstractmethod
def render(self):
pass
# Concrete Products
class WindowsButton(Button):
def render(self):
return "Windows Button"
class MacOSButton(Button):
def render(self):
return "MacOS Button"
# Abstract Factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self):
pass
@abstractmethod
def create_checkbox(self):
pass
# Concrete Factories
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return "Windows Checkbox"
class MacOSFactory(GUIFactory):
def create_button(self):
return MacOSButton()
def create_checkbox(self):
return "MacOS Checkbox"
# Usage
factory = WindowsFactory()
print(factory.create_button().render()) # Output: Windows Button
print(factory.create_checkbox()) # Output: Windows Checkbox
Real-World Example: GUI Framework
class WindowsCheckbox(Checkbox):
def render(self):
return "Rendering Windows Checkbox."
class MacOSCheckbox(Checkbox):
def render(self):
return "Rendering MacOS Checkbox."
# Usage
factory = MacOSFactory()
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render()) # Output: Rendering MacOS Button.
print(checkbox.render()) # Output: Rendering MacOS Checkbox.
Builder
Concept
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
Theoretical Explanation
The Builder pattern is particularly useful for creating complex objects with many optional parameters or configurations. It encapsulates the construction logic, adhering to the Single Responsibility Principle.
Basic Implementation
# Product
class House:
def __init__(self):
self.floor = None
self.walls = None
self.roof = None
def __str__(self):
return f"House with {self.floor}, {self.walls}, and {self.roof}."
# Builder Interface
class HouseBuilder:
def build_floor(self, floor_type):
pass
def build_walls(self, wall_type):
pass
def build_roof(self, roof_type):
pass
# Concrete Builder
class ConcreteHouseBuilder(HouseBuilder):
def __init__(self):
self.house = House()
def build_floor(self):
self.house.floor = "Concrete Floor"
def build_walls(self):
self.house.walls = "Brick Walls"
def build_roof(self):
self.house.roof = "Metal Roof"
def get_house(self):
return self.house
# Director
class Director:
def __init__(self, builder):
self.builder = builder
def construct_house(self):
self.builder.build_floor()
self.builder.build_walls()
self.builder.build_roof()
# Usage
builder = ConcreteHouseBuilder()
director = Director(builder)
director.construct_house()
house = builder.get_house()
print(house)
# Output: House with Concrete Floor, Brick Walls, and Metal Roof.
Prototype
Concept
The Prototype pattern is used to create new objects by copying an existing object (prototype) instead of creating from scratch. It’s particularly useful when object creation is costly or complex.
Theoretical Explanation
A prototype instance is used as a blueprint for creating new objects.
Provides an alternative to constructors.
Useful for scenarios involving deep copying or custom initialization.
Basic Implementation
import copy
class Prototype:
def __init__(self):
self._objects = {}
def register_object(self, name, obj):
self._objects[name] = obj
def unregister_object(self, name):
del self._objects[name]
def clone(self, name, **attributes):
obj = copy.deepcopy(self._objects.get(name))
obj.__dict__.update(attributes)
return obj
# Usage
class Car:
def __init__(self, model, color):
self.model = model
self.color = color
def __str__(self):
return f"{self.color} {self.model}"
prototype = Prototype()
car = Car("Sedan", "Red")
prototype.register_object("base_car", car)
new_car = prototype.clone("base_car", color="Blue")
print(new_car) # Output: Blue Sedan
Real-World Example: Graphic Editor
Scenario: A graphic design app that duplicates shapes.
class Shape:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
def clone(self):
return copy.deepcopy(self)
def __str__(self):
return f"Shape({self.x}, {self.y}, {self.color})"
# Usage
circle = Shape(10, 20, "Red")
circle_clone = circle.clone()
circle_clone.color = "Blue"
print(circle) # Output: Shape(10, 20, Red)
print(circle_clone) # Output: Shape(10, 20, Blue)
Singleton
Concept
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
Theoretical Explanation
Enforces a single instance across the system.
Useful for managing shared resources (e.g., database connections, logging).
Basic Implementation
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
def connect(self):
return "Database connected."
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # Output: True
Real-World Example: Logger
Scenario: A logging service where multiple modules write to the same log.
class Logger(metaclass=SingletonMeta):
def __init__(self):
self.log_file = "app.log"
def write_log(self, message):
print(f"Writing to {self.log_file}: {message}")
# Usage
logger1 = Logger()
logger2 = Logger()
logger1.write_log("Starting application...")
logger2.write_log("Application error.")
print(logger1 is logger2) # Output: True
4. Structural Patterns
Adapter
Concept
The Adapter pattern allows incompatible interfaces to work together by wrapping an existing class with a new interface.
Theoretical Explanation
Converts one interface into another expected by the client.
Useful when integrating third-party libraries or legacy code.
Basic Implementation
class EuropeanSocket:
def provide_electricity(self):
return "230V AC"
class Adapter:
def __init__(self, socket):
self.socket = socket
def provide_110v(self):
return "Converting 230V to 110V."
# Usage
european_socket = EuropeanSocket()
adapter = Adapter(european_socket)
print(adapter.provide_110v()) # Output: Converting 230V to 110V.
Real-World Example: Payment Gateway
Scenario: An e-commerce platform supporting multiple payment methods.
class Stripe:
def make_payment(self, amount):
return f"Paid {amount} using Stripe."
class PayPal:
def send_money(self, amount):
return f"Paid {amount} using PayPal."
class PaymentAdapter:
def __init__(self, payment_system):
self.payment_system = payment_system
def pay(self, amount):
if isinstance(self.payment_system, Stripe):
return self.payment_system.make_payment(amount)
elif isinstance(self.payment_system, PayPal):
return self.payment_system.send_money(amount)
# Usage
stripe = PaymentAdapter(Stripe())
paypal = PaymentAdapter(PayPal())
print(stripe.pay(100)) # Output: Paid 100 using Stripe.
print(paypal.pay(200)) # Output: Paid 200 using PayPal.
Bridge
Concept
The Bridge pattern decouples an abstraction from its implementation, allowing the two to vary independently.
Theoretical Explanation
Separates what an object does from how it does it.
Useful when implementations can change dynamically.
Basic Implementation
from abc import ABC, abstractmethod
class DrawingAPI(ABC):
@abstractmethod
def draw_circle(self, x, y, radius):
pass
class VectorAPI(DrawingAPI):
def draw_circle(self, x, y, radius):
return f"VectorAPI: Drawing circle at ({x}, {y}) with radius {radius}."
class RasterAPI(DrawingAPI):
def draw_circle(self, x, y, radius):
return f"RasterAPI: Drawing circle at ({x}, {y}) with radius {radius}."
class Shape:
def __init__(self, drawing_api):
self.drawing_api = drawing_api
class Circle(Shape):
def __init__(self, x, y, radius, drawing_api):
super().__init__(drawing_api)
self.x = x
self.y = y
self.radius = radius
def draw(self):
return self.drawing_api.draw_circle(self.x, self.y, self.radius)
# Usage
circle1 = Circle(5, 10, 15, VectorAPI())
circle2 = Circle(7, 14, 21, RasterAPI())
print(circle1.draw()) # Output: VectorAPI: Drawing circle at (5, 10) with radius 15.
print(circle2.draw()) # Output: RasterAPI: Drawing circle at (7, 14) with radius 21.
Composite
Concept
The Composite pattern allows you to treat individual objects and compositions of objects uniformly. It represents part-whole hierarchies, making it easy to work with both simple and complex structures.
Theoretical Explanation
Simplifies handling tree-like structures (e.g., file systems, organization charts).
Composite objects contain both leaf and composite children.
Basic Implementation
from abc import ABC, abstractmethod
class Component(ABC):
@abstractmethod
def operation(self):
pass
class Leaf(Component):
def __init__(self, name):
self.name = name
def operation(self):
return f"Leaf: {self.name}"
class Composite(Component):
def __init__(self, name):
self.name = name
self.children = []
def add(self, component):
self.children.append(component)
def operation(self):
results = [child.operation() for child in self.children]
return f"Composite: {self.name} containing [{', '.join(results)}]"
# Usage
leaf1 = Leaf("Leaf1")
leaf2 = Leaf("Leaf2")
composite = Composite("Composite1")
composite.add(leaf1)
composite.add(leaf2)
print(composite.operation())
# Output: Composite: Composite1 containing [Leaf: Leaf1, Leaf: Leaf2]
Real-World Example: File System
Scenario: A directory can contain files or other directories.
class FileSystemComponent(ABC):
@abstractmethod
def show_details(self, indent=0):
pass
class File(FileSystemComponent):
def __init__(self, name, size):
self.name = name
self.size = size
def show_details(self, indent=0):
return f"{' ' * indent}File: {self.name} ({self.size} KB)"
class Directory(FileSystemComponent):
def __init__(self, name):
self.name = name
self.children = []
def add(self, component):
self.children.append(component)
def show_details(self, indent=0):
details = f"{' ' * indent}Directory: {self.name}\n"
for child in self.children:
details += child.show_details(indent + 2) + "\n"
return details.strip()
# Usage
root = Directory("root")
sub_dir = Directory("sub_dir")
root.add(File("file1.txt", 100))
root.add(sub_dir)
sub_dir.add(File("file2.txt", 200))
sub_dir.add(File("file3.txt", 300))
print(root.show_details())
# Output:
# Directory: root
# File: file1.txt (100 KB)
# Directory: sub_dir
# File: file2.txt (200 KB)
# File: file3.txt (300 KB)
Decorator
Concept
The Decorator pattern adds responsibilities to objects dynamically, providing a flexible alternative to subclassing for extending functionality.
Theoretical Explanation
Allows behavior to be added to individual objects without affecting others.
Adheres to the Open/Closed Principle: Open for extension but closed for modification.
Basic Implementation
class Component(ABC):
@abstractmethod
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"
class Decorator(Component):
def __init__(self, component):
self.component = component
def operation(self):
return self.component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self.component.operation()})"
class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({self.component.operation()})"
# Usage
component = ConcreteComponent()
decorated = ConcreteDecoratorA(ConcreteDecoratorB(component))
print(decorated.operation())
# Output: ConcreteDecoratorA(ConcreteDecoratorB(ConcreteComponent))
Real-World Example: Pizza Toppings
Scenario: Dynamically add toppings to a pizza.
class Pizza:
def cost(self):
return 10
def description(self):
return "Plain Pizza"
class ToppingDecorator(Pizza):
def __init__(self, pizza):
self.pizza = pizza
def cost(self):
return self.pizza.cost()
def description(self):
return self.pizza.description()
class Cheese(ToppingDecorator):
def cost(self):
return self.pizza.cost() + 2
def description(self):
return self.pizza.description() + ", Cheese"
class Olives(ToppingDecorator):
def cost(self):
return self.pizza.cost() + 1.5
def description(self):
return self.pizza.description() + ", Olives"
# Usage
pizza = Pizza()
pizza = Cheese(pizza)
pizza = Olives(pizza)
print(pizza.description(), "Cost:", pizza.cost())
# Output: Plain Pizza, Cheese, Olives Cost: 13.5
Facade
Concept
The Facade pattern provides a simplified interface to a complex subsystem, making it easier to use.
Theoretical Explanation
Hides the complexity of the system from the client.
Encapsulates a set of interfaces into a single higher-level interface.
Basic Implementation
class SubsystemA:
def operation_a(self):
return "SubsystemA: Ready!"
class SubsystemB:
def operation_b(self):
return "SubsystemB: Go!"
class Facade:
def __init__(self):
self.subsystem_a = SubsystemA()
self.subsystem_b = SubsystemB()
def operation(self):
return f"{self.subsystem_a.operation_a()} + {self.subsystem_b.operation_b()}"
# Usage
facade = Facade()
print(facade.operation())
# Output: SubsystemA: Ready! + SubsystemB: Go!
Real-World Example: Video Conversion
Scenario: Simplify video conversion that involves multiple subsystems.
class VideoFile:
def __init__(self, filename):
self.filename = filename
class AudioMixer:
def fix(self, filename):
return f"Audio fixed for {filename}."
class VideoEditor:
def crop(self, filename):
return f"Video cropped for {filename}."
class CodecConverter:
def convert(self, filename, format):
return f"{filename} converted to {format} format."
class VideoConverterFacade:
def convert_video(self, filename, format):
audio_mixer = AudioMixer()
video_editor = VideoEditor()
codec_converter = CodecConverter()
audio = audio_mixer.fix(filename)
video = video_editor.crop(filename)
conversion = codec_converter.convert(filename, format)
return f"{audio}\n{video}\n{conversion}"
# Usage
converter = VideoConverterFacade()
result = converter.convert_video("movie.mp4", "AVI")
print(result)
# Output:
# Audio fixed for movie.mp4.
# Video cropped for movie.mp4.
# movie.mp4 converted to AVI format.
Flyweight
Concept
The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects.
Theoretical Explanation
Reduces memory usage by sharing immutable data.
Useful for applications where many objects are created (e.g., graphical apps).
Basic Implementation
class Flyweight:
def __init__(self, shared_state):
self.shared_state = shared_state
def operation(self, unique_state):
return f"Flyweight: Shared({self.shared_state}), Unique({unique_state})"
class FlyweightFactory:
_flyweights = {}
@staticmethod
def get_flyweight(shared_state):
if shared_state not in FlyweightFactory._flyweights:
FlyweightFactory._flyweights[shared_state] = Flyweight(shared_state)
return FlyweightFactory._flyweights[shared_state]
# Usage
factory = FlyweightFactory()
flyweight1 = factory.get_flyweight("Shared1")
flyweight2 = factory.get_flyweight("Shared1")
print(flyweight1.operation("Unique1"))
print(flyweight2.operation("Unique2"))
# Output:
# Flyweight: Shared(Shared1), Unique(Unique1)
# Flyweight: Shared(Shared1), Unique(Unique2)
This covers Composite, Decorator, Facade, and Flyweight. I’ll proceed with Proxy and then move to Behavioral Patterns. Let me know if you’d like adjustments!
Proxy
Concept
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Proxies are often used to defer resource-intensive operations, add security layers, or perform logging.
Theoretical Explanation
Proxies act as intermediaries, controlling access to a target object.
Types of proxies include:
Virtual Proxy: Delays creation or initialization.
Protection Proxy: Adds access control.
Logging Proxy: Tracks interactions.
Basic Implementation
from abc import ABC, abstractmethod
class Subject(ABC):
@abstractmethod
def request(self):
pass
class RealSubject(Subject):
def request(self):
return "RealSubject: Handling request."
class Proxy(Subject):
def __init__(self, real_subject):
self.real_subject = real_subject
def request(self):
print("Proxy: Logging access to the real subject.")
return self.real_subject.request()
# Usage
real_subject = RealSubject()
proxy = Proxy(real_subject)
print(proxy.request())
# Output:
# Proxy: Logging access to the real subject.
# RealSubject: Handling request.
Real-World Example: Virtual Proxy
Scenario: An image viewer application that delays loading high-resolution images until needed.
class HighResolutionImage:
def __init__(self, filename):
self.filename = filename
self.load_image_from_disk()
def load_image_from_disk(self):
print(f"Loading high-resolution image: {self.filename}")
def display(self):
print(f"Displaying high-resolution image: {self.filename}")
class ImageProxy:
def __init__(self, filename):
self.filename = filename
self.real_image = None
def display(self):
if not self.real_image:
self.real_image = HighResolutionImage(self.filename)
self.real_image.display()
# Usage
image = ImageProxy("large_photo.jpg")
print("Image proxy created. High-resolution image not loaded yet.")
image.display() # Image is loaded and displayed now.
image.display() # Image is displayed again without loading.
# Output:
# Image proxy created. High-resolution image not loaded yet.
# Loading high-resolution image: large_photo.jpg
# Displaying high-resolution image: large_photo.jpg
# Displaying high-resolution image: large_photo.jpg
5. Behavioral Patterns
Chain of Responsibility
Concept
The Chain of Responsibility pattern allows multiple objects to handle a request without coupling the sender to the receiver. Each object in the chain can either handle the request or pass it to the next object.
Theoretical Explanation
Decouples the sender of a request from its receivers.
Avoids hard-coding handlers into the request sender.
Particularly useful for handling logging, UI events, or validation workflows.
Basic Implementation
from abc import ABC, abstractmethod
class Handler(ABC):
def __init__(self, next_handler=None):
self.next_handler = next_handler
@abstractmethod
def handle_request(self, request):
pass
class ConcreteHandler1(Handler):
def handle_request(self, request):
if 0 < request <= 10:
return f"Handler1 handled request {request}."
elif self.next_handler:
return self.next_handler.handle_request(request)
class ConcreteHandler2(Handler):
def handle_request(self, request):
if 10 < request <= 20:
return f"Handler2 handled request {request}."
elif self.next_handler:
return self.next_handler.handle_request(request)
class DefaultHandler(Handler):
def handle_request(self, request):
return f"No handler could process request {request}."
# Usage
handler_chain = ConcreteHandler1(ConcreteHandler2(DefaultHandler()))
print(handler_chain.handle_request(5)) # Output: Handler1 handled request 5.
print(handler_chain.handle_request(15)) # Output: Handler2 handled request 15.
print(handler_chain.handle_request(25)) # Output: No handler could process request 25.
Real-World Example: Customer Support System
Scenario: A customer support system where requests are handled at different levels (basic, advanced, supervisor).
class SupportHandler(ABC):
def __init__(self, next_handler=None):
self.next_handler = next_handler
@abstractmethod
def handle_request(self, request):
pass
class BasicSupport(SupportHandler):
def handle_request(self, request):
if request == "basic":
return "BasicSupport: Handled basic request."
elif self.next_handler:
return self.next_handler.handle_request(request)
class AdvancedSupport(SupportHandler):
def handle_request(self, request):
if request == "advanced":
return "AdvancedSupport: Handled advanced request."
elif self.next_handler:
return self.next_handler.handle_request(request)
class SupervisorSupport(SupportHandler):
def handle_request(self, request):
return f"SupervisorSupport: Escalated request - {request}."
# Usage
support_chain = BasicSupport(AdvancedSupport(SupervisorSupport()))
print(support_chain.handle_request("basic")) # Output: BasicSupport: Handled basic request.
print(support_chain.handle_request("advanced")) # Output: AdvancedSupport: Handled advanced request.
print(support_chain.handle_request("critical")) # Output: SupervisorSupport: Escalated request - critical.
Command
Concept
The Command pattern encapsulates a request as an object, allowing parameterization, queuing, and logging of requests.
Theoretical Explanation
Encapsulates actions or operations as objects.
Decouples the sender from the receiver.
Commonly used for undo/redo functionality.
Basic Implementation
class Command(ABC):
@abstractmethod
def execute(self):
pass
class Receiver:
def action(self):
return "Receiver: Executing action."
class ConcreteCommand(Command):
def __init__(self, receiver):
self.receiver = receiver
def execute(self):
return self.receiver.action()
class Invoker:
def set_command(self, command):
self.command = command
def execute_command(self):
return self.command.execute()
# Usage
receiver = Receiver()
command = ConcreteCommand(receiver)
invoker = Invoker()
invoker.set_command(command)
print(invoker.execute_command())
# Output: Receiver: Executing action.
Real-World Example: Smart Home System
Scenario: A smart home system controlling lights and devices.
class Light:
def turn_on(self):
return "Light turned ON."
def turn_off(self):
return "Light turned OFF."
class TurnOnLightCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
return self.light.turn_on()
class TurnOffLightCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
return self.light.turn_off()
class RemoteControl:
def __init__(self):
self.commands = []
def set_command(self, command):
self.commands.append(command)
def press_button(self):
return [command.execute() for command in self.commands]
# Usage
light = Light()
remote = RemoteControl()
remote.set_command(TurnOnLightCommand(light))
remote.set_command(TurnOffLightCommand(light))
print(remote.press_button())
# Output:
# ['Light turned ON.', 'Light turned OFF.']
Interpreter
Concept
The Interpreter pattern defines a grammar for a language and an interpreter that parses and evaluates sentences in that language. It’s useful for creating scripting languages or expression evaluators.
Theoretical Explanation
Represents grammar rules as classes.
Each grammar rule has an
interpret
method for evaluation.Useful for building mathematical expression evaluators, parsers, or query engines.
Basic Implementation
class Expression:
def interpret(self):
pass
class Number(Expression):
def __init__(self, value):
self.value = value
def interpret(self):
return self.value
class Add(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self):
return self.left.interpret() + self.right.interpret()
class Subtract(Expression):
def __init__(self, left, right):
self.left = left
self.right = right
def interpret(self):
return self.left.interpret() - self.right.interpret()
# Usage
expression = Add(Number(10), Subtract(Number(20), Number(5)))
print(expression.interpret()) # Output: 25
Real-World Example: Calculator for Expressions
Scenario: A calculator for simple mathematical expressions.
class Context:
def __init__(self):
self.variables = {}
def set_variable(self, name, value):
self.variables[name] = value
def get_variable(self, name):
return self.variables.get(name, 0)
class Variable(Expression):
def __init__(self, name):
self.name = name
def interpret(self, context):
return context.get_variable(self.name)
# Usage
context = Context()
context.set_variable("x", 10)
context.set_variable("y", 5)
expression = Add(Variable("x"), Subtract(Number(20), Variable("y")))
print(expression.interpret(context)) # Output: 25
Iterator
Concept
The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying representation.
Theoretical Explanation
Encapsulates the iteration logic.
Decouples iteration from the collection, adhering to the Single Responsibility Principle.
Commonly used in Python (
for
loops inherently use iterators).
Basic Implementation
class NumberIterator:
def __init__(self, numbers):
self.numbers = numbers
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.numbers):
value = self.numbers[self.index]
self.index += 1
return value
else:
raise StopIteration
# Usage
numbers = NumberIterator([1, 2, 3, 4])
for num in numbers:
print(num)
# Output:
# 1
# 2
# 3
# 4
Real-World Example: Playlist Iterator
Scenario: A music playlist iterates over songs.
class Song:
def __init__(self, title, artist):
self.title = title
self.artist = artist
def __str__(self):
return f"{self.title} by {self.artist}"
class Playlist:
def __init__(self):
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __iter__(self):
return iter(self.songs)
# Usage
playlist = Playlist()
playlist.add_song(Song("Song A", "Artist 1"))
playlist.add_song(Song("Song B", "Artist 2"))
for song in playlist:
print(song)
# Output:
# Song A by Artist 1
# Song B by Artist 2
Mediator
Concept
The Mediator pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by preventing objects from referring to each other explicitly.
Theoretical Explanation
Simplifies communication between objects by introducing a mediator.
Adheres to the Single Responsibility Principle by centralizing communication logic.
Basic Implementation
class Mediator:
def notify(self, sender, event):
pass
class ConcreteMediator(Mediator):
def __init__(self, component1, component2):
self.component1 = component1
self.component2 = component2
def notify(self, sender, event):
if event == "A":
return f"Mediator reacts to A and triggers B."
elif event == "B":
return f"Mediator reacts to B and triggers A."
class Component1:
def __init__(self, mediator):
self.mediator = mediator
def do_a(self):
return self.mediator.notify(self, "A")
class Component2:
def __init__(self, mediator):
self.mediator = mediator
def do_b(self):
return self.mediator.notify(self, "B")
# Usage
mediator = ConcreteMediator(Component1, Component2)
component1 = Component1(mediator)
component2 = Component2(mediator)
print(component1.do_a()) # Output: Mediator reacts to A and triggers B.
print(component2.do_b()) # Output: Mediator reacts to B and triggers A.
Real-World Example: Chat Room
Scenario: A chat room where users interact through a mediator.
class ChatRoom:
def show_message(self, user, message):
print(f"[{user.name}]: {message}")
class User:
def __init__(self, name, chat_room):
self.name = name
self.chat_room = chat_room
def send(self, message):
self.chat_room.show_message(self, message)
# Usage
chat_room = ChatRoom()
user1 = User("Alice", chat_room)
user2 = User("Bob", chat_room)
user1.send("Hello, Bob!")
user2.send("Hi, Alice!")
# Output:
# [Alice]: Hello, Bob!
# [Bob]: Hi, Alice!
Memento
Concept
The Memento pattern captures and externalizes an object’s internal state without violating encapsulation. It allows state restoration later.
Theoretical Explanation
Useful for implementing undo/redo functionality.
Adheres to the Single Responsibility Principle by separating state storage from other logic.
Basic Implementation
class Memento:
def __init__(self, state):
self.state = state
class Originator:
def __init__(self):
self._state = ""
def set_state(self, state):
self._state = state
def save(self):
return Memento(self._state)
def restore(self, memento):
self._state = memento.state
# Usage
originator = Originator()
originator.set_state("State1")
memento = originator.save()
originator.set_state("State2")
print(originator._state) # Output: State2
originator.restore(memento)
print(originator._state) # Output: State1
Real-World Example: Text Editor
Scenario: A text editor with undo functionality.
class EditorState:
def __init__(self, content):
self.content = content
class Editor:
def __init__(self):
self.content = ""
def type(self, words):
self.content += words
def save(self):
return EditorState(self.content)
def restore(self, state):
self.content = state.content
# Usage
editor = Editor()
editor.type("Hello, ")
state = editor.save()
editor.type("world!")
print(editor.content) # Output: Hello, world!
editor.restore(state)
print(editor.content) # Output: Hello,
Observer
Concept
The Observer pattern establishes a one-to-many dependency between objects, ensuring that when one object changes state, all its dependents are notified and updated automatically.
Theoretical Explanation
Promotes loose coupling between the subject and observers.
Useful in event-driven systems, where multiple components react to a single event.
Basic Implementation
class Subject:
def __init__(self):
self._observers = []
def add_observer(self, observer):
self._observers.append(observer)
def notify_observers(self):
for observer in self._observers:
observer.update(self)
class Observer:
def update(self, subject):
pass
class ConcreteObserver(Observer):
def update(self, subject):
print("Observer notified!")
# Usage
subject = Subject()
observer1 = ConcreteObserver()
observer2 = ConcreteObserver()
subject.add_observer(observer1)
subject.add_observer(observer2)
subject.notify_observers()
# Output:
# Observer notified!
# Observer notified!
Real-World Example: Stock Price Tracker
Scenario: A stock price tracker notifies investors of changes in stock prices.
class Stock:
def __init__(self, name):
self.name = name
self.price = 0
self._observers = []
def add_observer(self, observer):
self._observers.append(observer)
def set_price(self, price):
self.price = price
self.notify_observers()
def notify_observers(self):
for observer in self._observers:
observer.update(self)
class Investor:
def update(self, stock):
print(f"Investor notified: {stock.name} price changed to {stock.price}.")
# Usage
apple_stock = Stock("Apple")
investor1 = Investor()
investor2 = Investor()
apple_stock.add_observer(investor1)
apple_stock.add_observer(investor2)
apple_stock.set_price(150)
# Output:
# Investor notified: Apple price changed to 150.
# Investor notified: Apple price changed to 150.
State
Concept
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
Theoretical Explanation
Encapsulates state-specific behavior and transitions into separate classes.
Eliminates complex conditionals in the object.
Basic Implementation
class State:
def handle(self, context):
pass
class ConcreteStateA(State):
def handle(self, context):
print("State A handling request. Switching to State B.")
context.state = ConcreteStateB()
class ConcreteStateB(State):
def handle(self, context):
print("State B handling request. Switching to State A.")
context.state = ConcreteStateA()
class Context:
def __init__(self):
self.state = ConcreteStateA()
def request(self):
self.state.handle(self)
# Usage
context = Context()
context.request() # Output: State A handling request. Switching to State B.
context.request() # Output: State B handling request. Switching to State A.
Real-World Example: Document Workflow
Scenario: A document goes through different states: Draft, Moderation, Published.
class DocumentState:
def next_state(self, document):
pass
def get_status(self):
pass
class DraftState(DocumentState):
def next_state(self, document):
document.state = ModerationState()
def get_status(self):
return "Draft"
class ModerationState(DocumentState):
def next_state(self, document):
document.state = PublishedState()
def get_status(self):
return "Under Moderation"
class PublishedState(DocumentState):
def next_state(self, document):
print("Document is already published!")
def get_status(self):
return "Published"
class Document:
def __init__(self):
self.state = DraftState()
def next_state(self):
self.state.next_state(self)
def get_status(self):
return self.state.get_status()
# Usage
doc = Document()
print(doc.get_status()) # Output: Draft
doc.next_state()
print(doc.get_status()) # Output: Under Moderation
doc.next_state()
print(doc.get_status()) # Output: Published
Strategy
Concept
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Theoretical Explanation
Encapsulates behaviors or algorithms into separate classes.
Adheres to the Open/Closed Principle by allowing algorithms to be added without modifying the context.
Basic Implementation
class Strategy:
def execute(self):
pass
class ConcreteStrategyA(Strategy):
def execute(self):
return "Strategy A executed."
class ConcreteStrategyB(Strategy):
def execute(self):
return "Strategy B executed."
class Context:
def __init__(self, strategy):
self.strategy = strategy
def set_strategy(self, strategy):
self.strategy = strategy
def execute_strategy(self):
return self.strategy.execute()
# Usage
context = Context(ConcreteStrategyA())
print(context.execute_strategy()) # Output: Strategy A executed.
context.set_strategy(ConcreteStrategyB())
print(context.execute_strategy()) # Output: Strategy B executed.
Real-World Example: Payment System
Scenario: A payment system supports multiple payment methods (e.g., PayPal, credit card).
class PaymentStrategy:
def pay(self, amount):
pass
class PayPalStrategy(PaymentStrategy):
def pay(self, amount):
return f"Paid {amount} using PayPal."
class CreditCardStrategy(PaymentStrategy):
def pay(self, amount):
return f"Paid {amount} using Credit Card."
class PaymentContext:
def __init__(self, strategy):
self.strategy = strategy
def set_strategy(self, strategy):
self.strategy = strategy
def execute_payment(self, amount):
return self.strategy.pay(amount)
# Usage
paypal = PayPalStrategy()
credit_card = CreditCardStrategy()
context = PaymentContext(paypal)
print(context.execute_payment(100)) # Output: Paid 100 using PayPal.
context.set_strategy(credit_card)
print(context.execute_payment(200)) # Output: Paid 200 using Credit Card.
Template Method
Concept
The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses.
Theoretical Explanation
Allows specific steps of an algorithm to be overridden without altering the algorithm's structure.
Promotes reuse by extracting common behavior into a template.
Basic Implementation
class AbstractClass:
def template_method(self):
self.step_one()
self.step_two()
self.hook()
def step_one(self):
print("AbstractClass: Step one.")
def step_two(self):
pass
def hook(self):
pass
class ConcreteClass(AbstractClass):
def step_two(self):
print("ConcreteClass: Step two.")
# Usage
concrete = ConcreteClass()
concrete.template_method()
# Output:
# AbstractClass: Step one.
# ConcreteClass: Step two.
Real-World Example: Data Analysis Pipeline
Scenario: A data analysis pipeline with common preprocessing steps.
class DataPipeline:
def run_pipeline(self):
self.load_data()
self.clean_data()
self.analyze_data()
self.visualize_results()
def load_data(self):
pass
def clean_data(self):
print("Cleaning data...")
def analyze_data(self):
print("Analyzing data...")
def visualize_results(self):
print("Visualizing results...")
class CSVDataPipeline(DataPipeline):
def load_data(self):
print("Loading data from CSV.")
class JSONDataPipeline(DataPipeline):
def load_data(self):
print("Loading data from JSON.")
# Usage
csv_pipeline = CSVDataPipeline()
csv_pipeline.run_pipeline()
# Output:
# Loading data from CSV.
# Cleaning data...
# Analyzing data...
# Visualizing results...
json_pipeline = JSONDataPipeline()
json_pipeline.run_pipeline()
# Output:
# Loading data from JSON.
# Cleaning data...
# Analyzing data...
# Visualizing results...
Visitor
Concept
The Visitor pattern separates algorithms from the objects on which they operate, allowing new operations to be added without modifying existing classes.
Theoretical Explanation
Adheres to the Single Responsibility Principle by separating operations from object structures.
Allows adding new operations without modifying the object structure.
Basic Implementation
class Visitor:
def visit(self, element):
pass
class ConcreteVisitor(Visitor):
def visit(self, element):
print(f"Visited {element.name}")
class Element:
def accept(self, visitor):
pass
class ConcreteElement(Element):
def __init__(self, name):
self.name = name
def accept(self, visitor):
visitor.visit(self)
# Usage
visitor = ConcreteVisitor()
element = ConcreteElement("Element1")
element.accept(visitor)
# Output: Visited Element1
Real-World Example: Shopping Cart Discounts
Scenario: Apply different discounts to product categories.
class DiscountVisitor:
def visit_electronics(self, item):
return f"10% off on {item.name}: {item.price * 0.9}"
def visit_clothing(self, item):
return f"20% off on {item.name}: {item.price * 0.8}"
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def accept(self, visitor):
pass
class Electronics(Product):
def accept(self, visitor):
return visitor.visit_electronics(self)
class Clothing(Product):
def accept(self, visitor):
return visitor.visit_clothing(self)
# Usage
visitor = DiscountVisitor()
tv = Electronics("TV", 1000)
shirt = Clothing("Shirt", 50)
print(tv.accept(visitor)) # Output: 10% off on TV: 900.0
print(shirt.accept(visitor)) # Output: 20% off on Shirt: 40.0
6. Real-World Applications of Patterns
Design patterns are widely used across industries to address common challenges in software development. Here’s how these patterns shine in various domains:
1. Web Development
Singleton: Managing database connections or a global configuration object in frameworks like Django or Flask.
Factory Method: Creating dynamic components such as form fields or widgets based on user input or database schema.
Template Method: Standardizing workflows like request validation, processing, and response in REST APIs.
2. Game Development
Observer: Tracking player events, such as health updates, inventory changes, or quest progression.
Flyweight: Optimizing memory usage for rendering a large number of game objects (e.g., trees, enemies).
State: Managing player states such as walking, running, or attacking.
3. E-Commerce
Strategy: Implementing dynamic pricing strategies, such as discounts, taxes, or shipping calculations.
Command: Managing user actions like adding items to a cart, processing payments, or undoing actions.
Visitor: Applying different discount rules to product categories like electronics or clothing.
4. Financial Applications
Decorator: Adding additional functionality to transaction systems, such as logging, encryption, or fraud detection.
Proxy: Implementing access control or lazy loading for sensitive financial data.
Chain of Responsibility: Processing user requests for loan approvals, where each step involves credit checks, income verification, and risk assessment.
5. Artificial Intelligence
Interpreter: Parsing and evaluating mathematical or logical expressions in AI models.
Composite: Representing decision trees or hierarchical data structures in machine learning.
Builder: Constructing complex neural networks with configurable layers and hyperparameters.
6. Enterprise Software
Adapter: Integrating third-party services (e.g., payment gateways, APIs) with existing systems.
Facade: Simplifying access to complex subsystems like reporting engines or authentication services.
Bridge: Supporting multiple back-end technologies (e.g., SQL and NoSQL) with a consistent interface.
By tailoring patterns to specific challenges, developers can significantly improve the robustness and scalability of their applications.
7. Summary and Best Practices
Key Takeaways from Design Patterns
Decouple Dependencies: Many patterns promote loose coupling, making systems more modular and easier to maintain.
Promote Reusability: Patterns like Singleton and Factory Method encourage code reuse and avoid duplication.
Enhance Scalability: Structural patterns such as Flyweight and Composite help scale applications efficiently.
Simplify Complexity: Patterns like Facade and Mediator abstract away intricate subsystems, providing a cleaner interface.
Best Practices for Using Design Patterns
Understand the Problem First:
Identify the recurring challenge before deciding on a pattern.
Don’t use a pattern just because it exists—solve the right problem.
Start Simple:
- Focus on a working solution first, then refactor into patterns when the design evolves.
Combine Patterns When Necessary:
- Many complex systems benefit from combining patterns (e.g., Composite with Visitor for hierarchical processing).
Stay Flexible:
- Patterns are guidelines, not rigid rules. Adapt them to suit your project’s needs.
Leverage Python Features:
Python’s dynamic typing, first-class functions, and decorators often simplify the implementation of patterns.
Use built-in tools like iterators, context managers, and meta-programming where applicable.
Design patterns are not just about memorizing templates or copying code snippets—they are about cultivating a mindset that makes your code more modular, flexible, and maintainable. With these 23 patterns, you now have a comprehensive toolkit for tackling recurring design challenges in Python development.
When to Use Design Patterns
While design patterns are powerful, they should be used judiciously:
Don't over-engineer: Avoid using patterns for simple problems.
Start with simplicity: Focus on solving the problem first; refactor to patterns when needed.
Understand the problem domain: Patterns are not a one-size-fits-all solution; they shine in specific contexts.
Key Lessons
Understand the Context: Each pattern solves a specific type of problem. Identify the challenge before applying a pattern.
Refactor When Needed: Introduce patterns during refactoring to simplify complex logic.
Collaborate Effectively: Patterns are a shared language among developers, helping teams communicate more effectively.
Applying Patterns in Real Projects
Iterate Gradually: Don’t try to force patterns into your project from the beginning. Let the design evolve naturally, and use patterns to refactor and simplify.
Learn by Practice: Theoretical understanding is crucial, but the real power of patterns comes from implementing them in real-world scenarios.
Combine Patterns: In complex systems, multiple patterns often work together. For example, a Composite pattern for a hierarchy can use a Visitor for operations like traversal.
Final Thoughts
Design patterns are tools to empower developers. They aren't meant to constrain creativity but to provide proven strategies for solving common problems. As you grow in your software engineering journey:
Keep revisiting these patterns.
Explore how they integrate with modern programming paradigms like functional programming and asynchronous systems.
Experiment with variations of these patterns to address your unique challenges.
Further Reading and Exploration
Books to Explore:
Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al. (the "GoF" book).
Head First Design Patterns by Eric Freeman and Elisabeth Robson.
Python Design Patterns and Best Practices by Arun Ravindran.
Hands-On Practice:
Refactor old projects by introducing design patterns.
Solve coding challenges with patterns in mind (e.g., LeetCode, Codewars).
Explore Advanced Topics:
Combining patterns in a single architecture.
Implementing patterns in modern Python paradigms like asyncio and type annotations.
Using patterns in frameworks like Django or Flask.
This should serve as a good base for you to start writing cleaner, more maintainable, and scalable Python code. Patterns aren’t just about solving problems—they’re about solving them beautifully. Happy coding! 😊
Feel free to reach out to me at AhmadWKhan.com
Subscribe to my newsletter
Read articles from Ahmad W Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by