Design Patterns🔗
- Design Pattern serves as typical solution to common problems in software design
- All patterns can categorized by their intent * Creational Pattern - deals with object creation * Structural Pattern - deals organisation of objects/classes * Behavioural Patterns - deals with object/classes communications
- Refactoring Guru
Creational Patterns🔗
Factory Method ⭐🔗
- also known as virtual constructor
- provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created
from abc import ABC, abstractmethod # Step 1: Define an abstract product class Vehicle(ABC): @abstractmethod def create(self): pass # Step 2: Implement concrete products class Car(Vehicle): def create(self): return "Car is created 🚗" class Bike(Vehicle): def create(self): return "Bike is created 🏍️" # Step 3: Define a factory class with a factory method class VehicleFactory(ABC): @abstractmethod def get_vehicle(self) -> Vehicle: pass # Step 4: Implement concrete factories for each vehicle type class CarFactory(VehicleFactory): def get_vehicle(self) -> Vehicle: return Car() class BikeFactory(VehicleFactory): def get_vehicle(self) -> Vehicle: return Bike() # Step 5: Client code def client_code(factory: VehicleFactory): vehicle = factory.get_vehicle() print(vehicle.create()) # Example Usage if __name__ == "__main__": car_factory = CarFactory() bike_factory = BikeFactory() client_code(car_factory) # Output: Car is created 🚗 client_code(bike_factory) # Output: Bike is created 🏍️
- NOTES * Decouples Object Creation - Client Classes don't need to know how objects are created * Encapsulation - Factory class has the logic to create objects * Extensionability - Adding a new Vehicle requires just adding a new subclass, without modifying client code
- Why you don't want user to create objects directly ?? * Client codes should not be affected with changes in the code. Let's say your process to create object becomes complicated(db calls, using different configurations), then client has no need to know about object creations. * Easier Dependency Injection & Testing * If you create Car() directly, testing requires changing the entire class. * With a factory, you can inject dependencies, making testing more modular.
Abstract Factory🔗
Builder ⭐🔗
- lets you construct complex objects step by step
- The pattern allows you to produce different types and representations of an object using the same construction code.
Why Use Builder PatternTest
- Better Readability – Instead of a constructor with too many parameters, we build the object step by step.
- Flexibility – Can construct different variations of an object (e.g., Car, SportsCar, SUV).
- Encapsulation – The construction logic is separate from the object representation.
# without builder pattern
class Car:
def __init__(self, brand, engine, seats, sunroof):
self.brand = brand
self.engine = engine
self.seats = seats
self.sunroof = sunroof
def __str__(self):
return f"Car({self.brand}, {self.engine}, {self.seats} seats, Sunroof: {self.sunroof})"
# Creating a car object with a long constructor
car = Car("Tesla", "Electric", 5, True)
print(car)
Above Implementation has following issues
- Long Constructor
- Optional Parameter
- Hard to Extend
#using builder pattern
class Car:
def __init__(self, brand=None, engine=None, seats=None, sunroof=None):
self.brand = brand
self.engine = engine
self.seats = seats
self.sunroof = sunroof
def __str__(self):
return f"Car({self.brand}, {self.engine}, {self.seats} seats, Sunroof: {self.sunroof})"
class CarBuilder:
def __init__(self):
self.car = Car()
def set_brand(self, brand):
self.car.brand = brand
return self # Enables method chaining
def set_engine(self, engine):
self.car.engine = engine
return self
def set_seats(self, seats):
self.car.seats = seats
return self
def set_sunroof(self, sunroof):
self.car.sunroof = sunroof
return self
def build(self):
return self.car
# Using the builder pattern
car = CarBuilder().set_brand("Tesla").set_engine("Electric").set_seats(5).set_sunroof(True).build()
print(car) # ✅ Car(Tesla, Electric, 5 seats, Sunroof: True)
- Readable & Flexible: No need to remember constructor parameters.
- Handles Optional Parameters: Can omit sunroof, engine, etc.
- Method Chaining: Allows easy, fluent object creation.
- Scalability: Easily add new features without modifying existing code.
Prototype🔗
Singleton ⭐🔗
- lets you ensure that a class has only one instance, while providing a global access point to this instance
- Advantages
- Prevents multiple instances of a resource-heavy class.
- Centralized access to a shared instance across the application.
- Ensures consistency when only one instance should exist (e.g., one DB connection)
- Examples Use Cases are one-root logger or one spark context, because spark initialization is costly.
class Singleton: _instance = None # Holds the single instance def __new__(cls, *args, **kwargs): if cls._instance is None: # The __new__ method ensures only one instance is created cls._instance = super(Singleton, cls).__new__(cls) return cls._instance # Usage obj1 = Singleton() obj2 = Singleton() print(obj1 is obj2) # ✅ True (Same instance)
Other Interesting ways to create Singleton Classes in Python
# using decorator
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Logger:
def log(self, msg):
print(f"[LOG]: {msg}")
# Usage
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # ✅ True (Same instance)
# using metaclasses
# Ensures **any subclass** automatically follows the Singleton pattern.
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 Database(metaclass=SingletonMeta):
def connect(self):
return "Connected to database"
# Usage
db1 = Database()
db2 = Database()
print(db1 is db2) # ✅ True (Same instance)
Approach | Pros | Cons |
---|---|---|
new | Simple, widely used | Not extendable |
Decorator | Clean, reusable | Harder debugging |
Metaclass | Works for subclasses | Complex |
Structural Patterns🔗
Adapter ⭐🔗
- allows objects with incompatible interfaces to collaborate
- usecases
- helps integrate 3rd-party library without modifying their code
- makes incompatible classes work together
- Using Adapter to modify this class
class MP3Player: def play_mp3(self, filename): print(f"Playing MP3 file: {filename}") # Adapter to support other formats class MediaAdapter: def __init__(self, media_type): self.media_type = media_type self.player = MP3Player() # Uses existing player def play(self, filename): if self.media_type == "mp3": self.player.play_mp3(filename) elif self.media_type == "mp4": print(f"Converting {filename} to MP3 format... 🎵") self.player.play_mp3(filename.replace(".mp4", ".mp3")) else: print(f"Error: Unsupported format {self.media_type} ❌") # Client Code player = MediaAdapter("mp4") player.play("video.mp4") # ✅ Plays after conversion
Bridge🔗
- decouples an abstraction from its implementation, allowing them to evolve independently
- When to Use
- When you want to avoid a rigid class hierarchy – Prevents class explosion due to multiple variations.
- When you need to support multiple implementations – Example: Different platforms (Windows, Linux, macOS).
- When abstraction and implementation should vary independently – Example: Devices and their remote controls.
- Key Components
- Abstraction – Defines a high-level interface (e.g., RemoteControl).
- Refined Abstraction – Extends abstraction with additional behavior.
- Implementation Interface – Defines the low-level details (e.g., Device).
- Concrete Implementations – Provide specific implementations.
from abc import ABC, abstractmethod # Implementation Interface (Device) class Device(ABC): """Defines a common interface for all devices.""" @abstractmethod def turn_on(self): pass @abstractmethod def turn_off(self): pass # Concrete Implementations (TV & Radio) class TV(Device): def turn_on(self): print("📺 TV is now ON") def turn_off(self): print("📺 TV is now OFF") class Radio(Device): def turn_on(self): print("📻 Radio is now ON") def turn_off(self): print("📻 Radio is now OFF") # Abstraction (Remote Control) class RemoteControl: """Bridge between the abstraction (Remote) and implementation (Device).""" def __init__(self, device: Device): self.device = device def toggle_power(self): print("🔘 Toggling Power...") self.device.turn_on() if isinstance(self.device, TV) else self.device.turn_off() # Client Code tv_remote = RemoteControl(TV()) radio_remote = RemoteControl(Radio()) tv_remote.toggle_power() # 📺 TV is now ON radio_remote.toggle_power() # 📻 Radio is now OFF
Composite🔗
Decorator ⭐🔗
- lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
- Usage
- logging, security, caching & UI improvements
- why use it ?
- Extends functionality without modifying the original class.
- Follows Open-Closed Principle (open for extension, closed for modification).
- Allows multiple decorators to be combined flexibly.
# without Decorator, adding milk to coffee is cumbersome class Coffee: def cost(self): return 5 def description(self): return "Basic Coffee" # Adding features by modifying the class (Not scalable ❌) class CoffeeWithMilk(Coffee): def cost(self): return super().cost() + 2 def description(self): return super().description() + " + Milk" coffee = CoffeeWithMilk() print(coffee.description()) # Basic Coffee + Milk print(coffee.cost()) # 7
- creating an ingredient decorator
# Base Component class Coffee: def cost(self): return 5 def description(self): return "Basic Coffee" # Decorator Base Class class CoffeeDecorator: def __init__(self, coffee): self._coffee = coffee def cost(self): return self._coffee.cost() def description(self): return self._coffee.description() # Concrete Decorators class MilkDecorator(CoffeeDecorator): def cost(self): return super().cost() + 2 def description(self): return super().description() + " + Milk" class SugarDecorator(CoffeeDecorator): def cost(self): return super().cost() + 1 def description(self): return super().description() + " + Sugar" # Client Code coffee = Coffee() print(coffee.description(), "->", coffee.cost()) # Basic Coffee -> 5 coffee = MilkDecorator(coffee) print(coffee.description(), "->", coffee.cost()) # Basic Coffee + Milk -> 7 coffee = SugarDecorator(coffee) print(coffee.description(), "->", coffee.cost()) # Basic Coffee + Milk + Sugar -> 8
- Flexible & Scalable
- Combinable - decorators can be combines
- Follows SOLID principles – No unnecessary subclasses or modifications.
Facade🔗
Flyweight🔗
Proxy ⭐🔗
- lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.
- Advantages
- Lazy Initialization - Virtual Proxy
- Access Proxy (Control) - Restriction to access original object
- Logging/monitoring Proxy - record requests for analytics and debugging
- Caching Proxy - store results to avoid recomputation
- Remote Proxy - Interface for calling methods on a remote object
- Virtual Proxy
class RealImage: """Heavy object that loads an image from disk.""" def __init__(self, filename): self.filename = filename self.load_from_disk() def load_from_disk(self): print(f"Loading image: {self.filename}") def display(self): print(f"Displaying image: {self.filename}") class ProxyImage: """Proxy that delays the creation of RealImage until display is called.""" def __init__(self, filename): self.filename = filename self.real_image = None def display(self): if self.real_image is None: self.real_image = RealImage(self.filename) # Lazy Initialization self.real_image.display() # Client Code image = ProxyImage("test_image.jpg") # Image not loaded yet image.display() # Loads image only when needed image.display() # Second call does not reload image
- logging Proxy
class RealService: def operation(self): print("Performing an operation in RealService") class LoggingProxy: """Logs requests before calling the actual object.""" def __init__(self, real_service): self.real_service = real_service def operation(self): print("Logging: Operation is about to be executed") self.real_service.operation() print("Logging: Operation executed successfully") # Client Code service = RealService() proxy = LoggingProxy(service) proxy.operation()
Behavioural Patterns🔗
Chain of Responsibility ⭐🔗
- lets you pass requests along a chain of handlers. Upon receiving a request,
- Each handler decides
- ✅ Process the request OR
- ✅ Forward it to the next handler
- When to Use
- Logging and Debugging – Different loggers (file, console, database) handle messages.
- Event Handling – UI elements process events (buttons, forms, popups).
- Request Validation – Middleware authentication in web frameworks.
- Customer Support System – Requests escalate from agent → supervisor → manager.
- Key Components
- Handler (abstract class) - Defines the method to handle requests.
- Concrete Handlers – Implement request processing & decide whether to pass it forward.
- Client – Sends requests to the first handler in the chain.
# logging System class Logger: """Base Handler""" def __init__(self, next_handler=None): self.next_handler = next_handler def log(self, level, message): if self.next_handler: self.next_handler.log(level, message) class DebugLogger(Logger): def log(self, level, message): if level == "DEBUG": print(f"[DEBUG] {message}") else: super().log(level, message) class WarningLogger(Logger): def log(self, level, message): if level == "WARNING": print(f"[WARNING] {message}") else: super().log(level, message) class ErrorLogger(Logger): def log(self, level, message): if level == "ERROR": print(f"[ERROR] {message}") else: super().log(level, message) # Setting up the chain logger_chain = DebugLogger(WarningLogger(ErrorLogger())) # Client Code logger_chain.log("DEBUG", "This is a debug message.") logger_chain.log("WARNING", "This is a warning message.") logger_chain.log("ERROR", "This is an error message.")
# web middleware - auth -> role -> log
class Handler:
"""Base Handler"""
def __init__(self, next_handler=None):
self.next_handler = next_handler
def handle(self, request):
if self.next_handler:
return self.next_handler.handle(request)
return "Request reached the end of the chain"
class AuthHandler(Handler):
"""Authentication Middleware"""
def handle(self, request):
if not request.get("user"):
return "Authentication Failed"
return super().handle(request)
class RoleHandler(Handler):
"""Authorization Middleware"""
def handle(self, request):
if request.get("role") != "admin":
return "Access Denied"
return super().handle(request)
class LoggingHandler(Handler):
"""Logging Middleware"""
def handle(self, request):
print(f"Logging request: {request}")
return super().handle(request)
# Setting up the chain
middleware_chain = AuthHandler(RoleHandler(LoggingHandler()))
# Client Code
request1 = {"user": "Alice", "role": "admin"}
print(middleware_chain.handle(request1)) # Success
request2 = {"user": "Bob", "role": "guest"}
print(middleware_chain.handle(request2)) # Access Denied
request3 = {"role": "admin"} # Missing user
print(middleware_chain.handle(request3)) # Authentication Failed
Command ⭐🔗
- encapsulates a request as an object, allowing for delayed execution, undo/redo functionality, and queuing commands.
- When to Use
- Undo/Redo functionality – Text editors, Photoshop.
- Job Scheduling – Task execution in threads.
- Remote Control Devices – TV remote buttons, IoT devices.
- Key Components
- Command Interface – Declares an execution method.
- Concrete Commands – Implement specific actions.
- Invoker – Triggers commands.
- Receiver – Performs the actual work.
# tv remote from abc import ABC, abstractmethod class Command(ABC): """Command Interface""" @abstractmethod def execute(self): pass class TV: """Receiver""" def turn_on(self): print("TV is ON") def turn_off(self): print("TV is OFF") class TurnOnCommand(Command): """Concrete Command: Turn ON""" def __init__(self, tv: TV): self.tv = tv def execute(self): self.tv.turn_on() class TurnOffCommand(Command): """Concrete Command: Turn OFF""" def __init__(self, tv: TV): self.tv = tv def execute(self): self.tv.turn_off() class RemoteControl: """Invoker""" def __init__(self): self.command = None def set_command(self, command: Command): self.command = command def press_button(self): if self.command: self.command.execute() # Client Code tv = TV() remote = RemoteControl() turn_on = TurnOnCommand(tv) turn_off = TurnOffCommand(tv) remote.set_command(turn_on) remote.press_button() # TV is ON remote.set_command(turn_off) remote.press_button() # TV is OFF
Iterator🔗
Mediator🔗
Memento🔗
- lets you save and restore the previous state of an object without revealing the details of its implementation
- When to use
- Undo/Redo operations* – Text editors, games, drawing applications.
- State recovery – Crash recovery in software.
- Checkpointing – Saving progress in a game.
- Key Components
- Memento – Stores the state of an object.
- Originator – Creates and restores mementos.
- Caretaker – Manages mementos and handles state restoration.
class Memento: """Memento stores the state of an object.""" def __init__(self, state): self._state = state def get_saved_state(self): return self._state class TextEditor: """Originator - Creates and restores mementos.""" def __init__(self): self._text = "" def write(self, text): self._text = text def save(self): return Memento(self._text) def restore(self, memento): self._text = memento.get_saved_state() def show(self): print(f"Current Text: {self._text}") class Caretaker: """Caretaker - Manages saved states.""" def __init__(self): self._history = [] def save_state(self, memento): self._history.append(memento) def restore_state(self): if self._history: return self._history.pop() return None # Client Code editor = TextEditor() caretaker = Caretaker() editor.write("Hello, World!") caretaker.save_state(editor.save()) # Save state editor.show() # Output: Current Text: Hello, World! editor.write("New Text") editor.show() # Output: Current Text: New Text # Restore previous state editor.restore(caretaker.restore_state()) editor.show() # Output: Current Text: Hello, World!
Observer ⭐🔗
- The Observer Pattern allows multiple objects (observers) to listen to and react to changes in another object (subject). When the subject’s state changes, all registered observers are notified automatically.
- When to Use
- Event-driven programming – UI elements react to user actions.
- Publish-Subscribe systems – Notification services, message brokers.
- Data Binding – React.js, Vue.js frameworks.
- Stock Market Updates – Multiple clients get real-time stock prices.
- Key Components
- Subject (Publisher) – Maintains a list of observers and notifies them when state changes.
- Observer (Subscriber) – Listens for updates from the subject.
- Concrete Subject – Implements state changes and observer management.
class StockMarket: """Subject (Publisher)""" def __init__(self): self.observers = [] self.stock_price = 0 def add_observer(self, observer): self.observers.append(observer) def remove_observer(self, observer): self.observers.remove(observer) def notify_observers(self): for observer in self.observers: observer.update(self.stock_price) def set_price(self, price): self.stock_price = price self.notify_observers() class Investor: """Observer (Subscriber)""" def __init__(self, name): self.name = name def update(self, price): print(f"{self.name} received stock price update: {price}") # Client Code market = StockMarket() investor1 = Investor("Alice") investor2 = Investor("Bob") market.add_observer(investor1) market.add_observer(investor2) market.set_price(100) # Both investors get notified market.set_price(120) # Another update is sent
Stage🔗
- models an object’s behavior as a finite set of states, with each state defining its own behavior.
- When to Use
- When an object has different modes or stages** – Traffic lights, vending machines.
- State-dependent behavior – Objects act differently in different states.
- Reducing complex if-else logic – Avoids conditionals in methods.
- Key Components
- State Interface – Defines behavior for all states.
- Concrete States – Implement specific behavior for each state.
- Context (Object) – Maintains current state & delegates actions.
from abc import ABC, abstractmethod class TrafficLightState(ABC): """Abstract state class defining state-specific behavior.""" @abstractmethod def handle(self, light): pass class RedLight(TrafficLightState): def handle(self, light): print("🚦 Red Light - Stop!") light.state = GreenLight() class GreenLight(TrafficLightState): def handle(self, light): print("🚦 Green Light - Go!") light.state = YellowLight() class YellowLight(TrafficLightState): def handle(self, light): print("🚦 Yellow Light - Slow Down!") light.state = RedLight() class TrafficLight: """Context class maintaining the current state.""" def __init__(self): self.state = RedLight() # Initial state def change(self): self.state.handle(self) # Client Code traffic_light = TrafficLight() for _ in range(4): traffic_light.change()
Strategy ⭐🔗
- define a family of algorithms, put them in separate classes, and make them interchangeable at runtime.
- When to use * Multiple algorithms for the same task – Sorting, Compression. * Reducing conditional logic (if-else/switch) – Payment methods, Authentication. * Behavior modification at runtime – Game difficulty levels.
- Key Components
* Context – Maintains a reference to a strategy object.
* Strategy Interface – Defines a common interface for all strategies.
* Concrete Strategies – Implement different algorithms.
# payment strategy from abc import ABC, abstractmethod class PaymentStrategy(ABC): """Strategy Interface""" @abstractmethod def pay(self, amount): pass class CreditCardPayment(PaymentStrategy): """Concrete Strategy: Credit Card""" def pay(self, amount): print(f"Paid ${amount} using Credit Card.") class PayPalPayment(PaymentStrategy): """Concrete Strategy: PayPal""" def pay(self, amount): print(f"Paid ${amount} using PayPal.") class PaymentContext: """Context that uses a strategy""" def __init__(self, strategy: PaymentStrategy): self.strategy = strategy def set_strategy(self, strategy: PaymentStrategy): self.strategy = strategy def checkout(self, amount): self.strategy.pay(amount) # Client Code context = PaymentContext(CreditCardPayment()) context.checkout(100) # Paid using Credit Card context.set_strategy(PayPalPayment()) context.checkout(200) # Paid using PayPal
Template Method🔗
- defines the skeleton of an algorithm in a base class, allowing subclasses to override specific steps without modifying the structure of the algorithm.
- When to Use
- Common workflow with variations – Report generation, data processing.
- Code reuse – Avoids duplicate code in similar processes.
- Standardized behavior – Ensures steps are executed in a defined order.
- Key Components
- Abstract Class (Template) – Defines the algorithm structure.
- Concrete Class – Implements missing steps of the algorithm.
from abc import ABC, abstractmethod class ReportGenerator(ABC): """Abstract class defining the template method.""" def generate_report(self): """Template method defining the report generation process.""" self.collect_data() self.analyze_data() self.format_report() self.print_report() @abstractmethod def collect_data(self): pass @abstractmethod def analyze_data(self): pass def format_report(self): """Common implementation.""" print("Formatting report in PDF format.") def print_report(self): """Common implementation.""" print("Printing report...") class SalesReport(ReportGenerator): """Concrete class implementing specific steps.""" def collect_data(self): print("Collecting sales data.") def analyze_data(self): print("Analyzing sales trends.") # Client Code report = SalesReport() report.generate_report()
Visitor🔗
- add new behaviors to objects without modifying their structure, by separating the operation from the object itself.
- When to Use
- Extending behavior without modifying existing classes – Syntax tree traversal.
- Applying different operations to a group of objects – Compilers, AST manipulation
- Avoiding clutter in existing classes – Separates logic from data structures.
- Components
- Visitor – Defines new operations on elements.
- Concrete Visitors – Implement specific behavior.
- Element – Accepts a visitor and allows it to operate on itself.
# We **separate operations (size calculation & compression)** from the **file structure** from abc import ABC, abstractmethod class FileElement(ABC): """Abstract element accepting visitors.""" @abstractmethod def accept(self, visitor): pass class File(FileElement): """Concrete file class.""" def __init__(self, name, size): self.name = name self.size = size def accept(self, visitor): visitor.visit_file(self) class Folder(FileElement): """Concrete folder class.""" def __init__(self, name, children): self.name = name self.children = children def accept(self, visitor): visitor.visit_folder(self) class Visitor(ABC): """Abstract visitor defining operations.""" @abstractmethod def visit_file(self, file): pass @abstractmethod def visit_folder(self, folder): pass class SizeCalculator(Visitor): """Concrete visitor calculating total size.""" def visit_file(self, file): print(f"File: {file.name}, Size: {file.size} KB") def visit_folder(self, folder): print(f"Folder: {folder.name} contains:") for child in folder.children: child.accept(self) # Client Code file1 = File("Document.txt", 120) file2 = File("Photo.jpg", 450) folder = Folder("MyFolder", [file1, file2]) size_calculator = SizeCalculator() folder.accept(size_calculator)