Intermediate

Design Patterns in Python 3

This course is aimed at both experienced Python developers and beginners who want to enrich their toolbox with ready-to-use solutions.

Python version: 3.9.2+ (applicable to Python 3.x) IDE used: Visual Studio Code with Python extension


Table of Contents

  1. Course Overview
  2. Introduction to Design Patterns
  1. Creational Patterns: Factory
  1. Creational Patterns: Abstract Factory
  1. Creational Patterns: Builder
  1. Creational Patterns: Prototype
  1. Creational Patterns: Singleton
  1. Structural Patterns: Adapt
  1. Structural Patterns: Bridge
  1. Structural Patterns: Composite
  1. Structural Patterns: Decorator
  1. Structural Patterns: Facade
  1. Structural Patterns: Flyweight
  1. Structural Patterns: Proxy
  1. Behavioral Patterns: Strategy
  1. Behavioral Patterns: Command
  1. Behavioral Patterns: State
  1. Behavioral Patterns: Observe
  1. Behavioral Patterns: Visitor
  1. Behavioral Patterns: Chain of Responsibility
  1. Behavioral Patterns: Mediator
  1. Behavioral Patterns: Memento
  1. Behavioral Patterns: Null
  1. Behavioral Patterns: Template
  1. Behavioral Patterns: Iterator
  1. Behavioral Patterns: Interpreter
  1. Course Summary

1. Course Overview

Welcome to the course Design Patterns in Python 3, presented by Gerald Britton on Pluralsight. This course is aimed at both experienced Python developers and beginners who want to enrich their toolbox with ready-to-use solutions.

Thanks to the famous work of the Gang of Four, there are 24 essential design patterns that can easily be used in Python. This course explores these patterns, the problems they solve, and how to implement them in Python, with numerous examples and demonstrations.

Major Topics Covered

  • The principles of object-oriented programming (OOP)
  • The classification of design patterns
  • Using abstract base classes in Python to create interfaces
  • Application of the DRY (Don’t Repeat Yourself) principle

Prerequisites

  • Knowledge of Python basics, including classes
  • Visual Studio Code installed with Python extension

2. Introduction to Design Patterns

What is a design pattern?

A design pattern is a template solution to a common design problem. This simple definition hides a profound richness. As Christopher Alexander said:

“Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.”

This concept comes from architecture, but applies perfectly to software. Software design patterns are largely taken from the reference work “Design Patterns: Elements of Reusable Object-Oriented Software” (1995) by the Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides.

Real-world examples

Patterns are omnipresent:

  • Architecture: each municipality has construction codes (plumbing, electricity)
  • The automobile: industrial norms and standards
  • Mobile phones: standardized interfaces

Why do we need design patterns?

  • To ensure our work is consistent, reliable and understandable
  • To avoid reinventing the wheel with each new program
  • To use proven solutions to problems seen hundreds or thousands of times
  • To facilitate maintenance and future developments by other developers

Classification of design patterns

Design patterns are divided into three main categories:

CategoryDescriptionExamples
CreativeConcern the creation of objectsFactory, Builder, Singleton
StructuralDefine relationships between objectsAdapt, Facade, Composite
BehavioralManage inter-object communicationCommand, Observer, Strategy

Note: There are other categories not covered in this course, including concurrency patterns (multithreading).

The SOLID principles

SOLID is an acronym to remember the 5 fundamental principles of OOP:

LetterPrincipleDescription
SSingle ResponsibilityA class should only have one responsibility
OOpen/ClosedA class must be open to extension, but closed to modification
LLiskov SubstitutionSubclasses must be able to override their parent class without breaking the program
ISegregation interfaceSeveral specific interfaces are better than one general interface
DDependency InversionProgram towards abstractions, not implementations

Tools needed

To take this course, you need:

  • Python 3.x (3.9.2 or newer recommended) — downloadable from python.org
  • Visual Studio Code — downloadable from code.visualstudio.com
  • The Python extension for VS Code — supports IntelliSense, Linting, Debugging, code navigation, formatting

Create interfaces in Python — Abstract Base Classes

In Python, interfaces are implemented via Abstract Base Classes (ABC), introduced by PEP 3119. Support for ABCs appeared in Python 2.6 and 3.0 in 2008.

Define an Abstract Base Class:

# MyABC.py
import abc

class MyABC(abc.ABC):
    """Définition de la classe de base abstraite"""

    @abc.abstractmethod
    def do_something(self, value):
        """Méthode requise"""

    @abc.abstractproperty
    def some_property(self):
        """Propriété requise"""

Implement an ABC:

# MyClass.py
from MyABC import MyABC

class MyClass(MyABC):
    """Implémentation de la classe de base abstraite"""

    def __init__(self, value=None):
        self._myprop = value

    def do_something(self, value):
        """Implémentation de la méthode abstraite"""
        self._myprop *= value

    @property
    def some_property(self):
        """Implémentation de la propriété abstraite"""
        return self._myprop

Important points:

  • To define an ABC, import the abc module and inherit from abc.ABC
  • Use @abc.abstractmethod to declare abstract methods
  • Use @abc.abstractproperty for abstract properties
  • A class that inherits from an ABC must implement all abstract methods and properties
  • Python is a dynamic language — introspection is always available, and it is technically possible to bypass the ABC mechanism, but this violates the implicit agreement between good Python developers

Module Summary Introduction:

  • Design patterns are model solutions to recurring problems
  • They help build more reliable, consistent and maintainable programs
  • The Gang of Four has formalized 24 essential patterns
  • SOLID principles guide object-oriented design
  • In Python, interfaces are implemented via Abstract Base Classes (ABCs)

3. Creational Patterns: Factory

Introduction to the Factory pattern

The Factory Pattern is a creation pattern. Factories are places where things are created — that’s exactly what this pattern does. He :

  • Defines an interface to create an object
  • Let subclasses decide which object to build
  • Uses a factory method to delegate instantiation to subclasses

The Factory Pattern is also known as Virtual Constructor Pattern.

Motivating example: brute force approach

Consider the problem of creating an object representing different car models. It is unclear which one will be needed before execution.

Naive approach with if/elif/else:

# BeforeFactory/__main__.py
from chevyvolt import ChevyVolt
from fordfusion import FordFusion
from jeepsahara import JeepSahara
from nullcar import NullCar

def getcar(carname):
    if carname == 'Chevy':
        return ChevyVolt()
    elif carname == 'Ford':
        return FordFusion()
    elif carname == 'Jeep':
        return JeepSahara()
    else:
        return NullCar(carname)

for carname in 'Chevy', 'Ford', 'Jeep', 'Tesla':
    car = getcar(carname)
    car.start()
    car.stop()

Problem: The long if/elif/else structure is a signal that there is probably a better approach. Adding a new model requires modifying this code (violation of the Open/Closed principle).

Note: We use NullCar here — an example of the Null Pattern: return a dummy instance that still implements all the required methods, thus avoiding testing for null values ​​at runtime.

Simple Factory Pattern

The Simple Factory Pattern uses a dedicated class with a dictionary to map class names to the classes themselves.

UML structure:

AbsAuto (interface)
    ├── ChevyVolt
    ├── FordFusion
    ├── JeepSahara
    └── NullCar
AutoFactory ──> crée instances de AbsAuto

AutoFactory implementation:

# SimpleFactory/autofactory.py
from inspect import getmembers, isclass, isabstract
import autos

class AutoFactory(object):
    autos = {}  # Clé = nom du modèle, Valeur = classe du modèle

    def __init__(self):
        self.load_autos()

    def load_autos(self):
        classes = getmembers(autos,
                             lambda m: isclass(m) and not isabstract(m))
        for name, _type in classes:
            if isclass(_type) and issubclass(_type, autos.AbsAuto):
                self.autos.update([[name, _type]])

    def create_instance(self, carname):
        if carname in self.autos:
            return self.autos[carname]()
        else:
            return autos.NullCar(carname)

Important points:

  • We use the Python inspect module for introspection
  • Importing the autos package executes the import statements in the __init__ module, adding the classes to the AutoFactory namespace
  • Dictionary maps model name to corresponding class
  • If no match is found, NullCar is returned

Full Factory Pattern (Gang of Four)

The Full Factory Pattern also abstracts the factory itself.

UML structure:

AbsProduct ──────────────── AbsFactory
    └── ConcreteProduct         └── ConcreteFactory
                                        └── create_product() → ConcreteProduct

Abstract base class for factories:

# factories/abs_factory.py (exemple)
import abc

class AbsFactory(abc.ABC):
    @abc.abstractmethod
    def create_auto(self):
        pass

Dynamic loading of factories:

# loader.py (exemple)
import importlib
import inspect

def load_factory(factory_name):
    try:
        module = importlib.import_module(f'factories.{factory_name}')
    except ImportError:
        module = importlib.import_module('factories.null_factory')
    
    classes = inspect.getmembers(module, inspect.isclass)
    for name, _type in classes:
        if inspect.isclass(_type) and \
           issubclass(_type, abs_factory.AbsFactory) and \
           not inspect.isabstract(_type):
            return _type

Advantages:

  • Uses dynamic imports — factory is loaded at runtime
  • If loading fails, import the null_factory instead

Factory module summary

The Factory Pattern brings several advantages:

  1. Encapsulates object instantiation — no need to instantiate classes directly
  2. Supports the Dependency Inversion Principle — clients no longer depend on implementations
  3. Clients depend on an abstraction — they know that all objects returned by the factory respect the ABC

Two variations:

  • Simple Factory: a single factory, often sufficient
  • Full Factory: also abstracts the factory — more flexible, but more complex

4. Creational Patterns: Abstract Factory

Introduction to the Abstract Factory pattern

The Abstract Factory Pattern is a creative pattern, a close cousin of the Factory Pattern. It allows you to:

  • Create related or dependent object families without specifying their concrete classes
  • Apply dependencies between concrete classes
  • Delegate creation of objects to concrete subclasses

The Abstract Factory is also known as Pattern Kit.

Difference with Factory:

  • A Factory creates a single product type
  • An Abstract Factory can produce a family of classes

Motivating example

We have a collection of automobile factories. Each factory makes cars for a manufacturer, but can make them in economy, sport and luxury editions.

Problem with naive approach:

# Approche naïve avec imports massifs et if/elif/else imbriqués
from gm_economy import GmEconomy
from gm_sport import GmSport
from gm_luxury import GmLuxury
from ford_economy import FordEconomy
# ... et bien d'autres imports

if manufacturer == 'gm':
    if car_type == 'economy':
        car = GmEconomy()
    elif car_type == 'sport':
        car = GmSport()
    # ...
elif manufacturer == 'ford':
    # ... même chose pour Ford

Problem: Open/Closed violation — adding Honda requires opening the main program, importing more classes, and modifying the long if structure.

Structure of the Abstract Factory pattern

AbstractFactory (ABC)
    ├── create_economy() [abstrait]
    ├── create_sport() [abstrait]
    └── create_luxury() [abstrait]

FordFactory (ConcreteFactory)        GMFactory (ConcreteFactory)
    ├── create_economy() → FordFiesta    ├── create_economy() → GmEconomy
    ├── create_sport() → FordMustang     ├── create_sport() → GmSport
    └── create_luxury() → LincolnMKS     └── create_luxury() → GmLuxury

AbstractProduct (ABC)
    ├── FordFiesta, FordMustang, LincolnMKS
    └── GmEconomy, GmSport, GmLuxury

Abstract Factory implementation

Abstract factory base class:

# factories/abs_factory.py
import abc

class AbsFactory(abc.ABC):
    @abc.abstractstaticmethod
    def create_economy():
        pass

    @abc.abstractstaticmethod
    def create_sport():
        pass

    @abc.abstractstaticmethod
    def create_luxury():
        pass

Concrete Factory for Ford:

# factories/ford_factory.py
from .abs_factory import AbsFactory
from autos.ford.fiesta import FordFiesta
from autos.ford.mustang import FordMustang
from autos.ford.lincoln import LincolnMKS

class FordFactory(AbsFactory):
    @staticmethod
    def create_economy():
        return FordFiesta()

    @staticmethod
    def create_sport():
        return FordMustang()

    @staticmethod
    def create_luxury():
        return LincolnMKS()

Main program:

# __main__.py (exemple)
from factories.ford_factory import FordFactory
from factories.gm_factory import GmFactory

def test_factory(factory):
    car = factory.create_economy()
    car.start()
    car.stop()
    car = factory.create_sport()
    car.start()
    car.stop()
    car = factory.create_luxury()
    car.start()
    car.stop()

for factory in [FordFactory, GmFactory]:
    test_factory(factory)

Note: Methods are @staticmethod because the class does not maintain state. This is not strictly required by the pattern, it is a simplification for this example.

Abstract Abstract Factory

  • Like the Factory Pattern, it encapsulates instantiation and supports the Dependency Inversion Principle
  • It goes further by supporting linked object families
  • When to choose Factory vs Abstract Factory?
  • Factory: when we do not know which concrete class we will need
  • Abstract Factory: when you want to support object families

5. Creational Patterns: Builder

Introduction to Pattern Builder

The Builder Pattern is a creative pattern that helps build complex objects. He :

  • Separates the construction of a complex object from its representation
  • Encapsulates the construction of the object (corollary of the Single Responsibility Principle)
  • Enables a multi-step build process
  • Allows implementations to vary — build a powerful workstation or a budget box, without changing the client interface

Motivating example: the problem of parameter lists

First approach — too many parameters in constructor:

# BeforeBuilder1/computer.py (exemple)
class Computer:
    def __init__(self, case, mainboard, cpu, memory, hard_drive, video_card):
        self.case = case
        self.mainboard = mainboard
        self.cpu = cpu
        self.memory = memory
        self.hard_drive = hard_drive
        self.video_card = video_card

    def display(self):
        print(f'Case: {self.case}')
        print(f'Mainboard: {self.mainboard}')
        # ...

# Usage — difficile à lire et à maintenir
computer = Computer('Antec', 'Asus', 'Intel Core i7', '16 GB', '1 TB', 'GeForce')

Problem: Long parameter lists are difficult to understand and maintain — which are required, which are optional? Clear violation of the Open/Closed principle.

Second approach — expose attributes directly:

# BeforeBuilder2 (exemple)
class Computer:
    def display(self):
        print(f'Case: {self.case}')
        # ...

computer = Computer()
computer.case = 'Antec'
computer.mainboard = 'Asus'
# ...

Problem: Still no control over construction order and code duplication.

Implementing the Builder pattern

UML structure:

AbsBuilder (ABC)
    ├── get_computer()
    ├── new_computer()
    ├── build_mainboard() [abstrait]
    ├── get_case() [abstrait]
    ├── install_mainboard() [abstrait]
    ├── install_hard_drive() [abstrait]
    └── install_video_card() [abstrait]

MyComputerBuilder (ConcreteBuilder)
    └── implémente toutes les méthodes abstraites

Director
    ├── __init__(builder)
    ├── build_computer()  ← ordonne les étapes
    └── get_computer()

Abstract base class of the builder:

# Builder/abs_builder.py
import abc
from computer import Computer

class AbsBuilder(abc.ABC):
    def get_computer(self):
        return self._computer

    def new_computer(self):
        self._computer = Computer()

    @abc.abstractmethod
    def build_mainboard(self):
        pass

    @abc.abstractmethod
    def get_case(self):
        pass

    @abc.abstractmethod
    def install_mainboard(self):
        pass

    @abc.abstractmethod
    def install_hard_drive(self):
        pass

    @abc.abstractmethod
    def install_video_card(self):
        pass

The Director class:

# Builder/director.py
class Director(object):

    def __init__(self, builder):
        self._builder = builder

    def build_computer(self):
        self._builder.new_computer()
        self._builder.get_case()
        self._builder.build_mainboard()
        self._builder.install_mainboard()
        self._builder.install_hard_drive()
        self._builder.install_video_card()

    def get_computer(self):
        return self._builder.get_computer()

Main program:

# Builder/__main__.py (exemple)
from director import Director
from mycomputer_builder import MyComputerBuilder
from budget_box_builder import BudgetBoxBuilder

builder = MyComputerBuilder()
director = Director(builder)
director.build_computer()
computer = director.get_computer()
computer.display()

Builder module summary

The Builder Pattern:

  • Separates the “How” from the “What” — assembly is separated from components
  • Encapsulates what varies — the components — while allowing different representations
  • The client creates a Director object, which uses a ConcreteBuilder and builds the product in the required order
  • Ideal when you have long parameter lists or complex build processes

6. Creational Patterns: Prototype

Introduction to the Prototype pattern

The Prototype Pattern is a creative pattern which:

  • Starts with an existing instance of an object (the prototype)
  • Creates a clone of this object which can be given different attribute values
  • Reduces the number of classes needed in the source code
  • Can be seen as run-time inheritance

Use case: Instead of creating a class for each variant of an object (budget laptop, gaming laptop, professional laptop), we create a basic prototype and clone it.

UML structure:

AbsPrototype (interface)
    └── clone() [abstrait]

Laptop (implémente AbsPrototype + AbsComputer)
    ├── clone() → copie du laptop
    └── display()

Tower (implémente AbsPrototype + AbsComputer)
    ├── clone() → copie de la tour
    └── display()

Shallow Cloning

The shallow clone copies the references of the objects contained in the instance to be cloned.

# laptop.py (exemple simplifié)
import copy

class Laptop(AbsComputer, AbsPrototype):
    def __init__(self, model_id, processor, memory, storage, display):
        self.model_id = model_id
        self.processor = processor
        self.memory = memory
        self.storage = storage
        self.display = display

    def clone(self):
        return copy.copy(self)  # Shallow copy

    def display_specs(self):
        print(f'Model: {self.model_id}')
        print(f'Processor: {self.processor}')
        # ...
# Programme principal (exemple)
laptop_L1 = Laptop('L1', 'Intel Core i7', '16 GB', '512 GB SSD', '15"')
laptop_L2 = laptop_L1.clone()
laptop_L2.model_id = 'L2'
laptop_L2.processor = 'AMD Ryzen 7'  # Seul le processeur diffère
laptop_L1.display_specs()
laptop_L2.display_specs()

Warning: The shallow clone copies the references of nested objects. If the Tower contains a MainBoard object, modifying the clone’s MainBoard will also affect the original.

Deep Cloning

The deep clone constructs a new compound object and recursively inserts copies of the objects found into the original.

# tower.py (exemple simplifié)
import copy

class Tower(AbsComputer, AbsPrototype):
    def __init__(self, model_id, mainboard, processor, memory):
        self.model_id = model_id
        self.mainboard = mainboard  # Objet imbriqué !
        self.processor = processor
        self.memory = memory

    def clone(self):
        return copy.deepcopy(self)  # Deep copy — résout le problème des objets imbriqués

    def display_specs(self):
        print(f'Model: {self.model_id}')
        print(f'Mainboard: {self.mainboard.manufacturer} {self.mainboard.model}')
        # ...

Precautions with deep clone:

  • If objects are recursive (objects that form a hierarchy of the same type), the deep clone can cause a recursive loop exceeding limits
  • As the deep clone copies everything, it may copy more than necessary, increasing memory pressure

Prototype Manager

In a system with many prototypes, a prototype manager can help organize everything.

# prototype_manager.py (exemple)
class PrototypeManager(dict):
    """Dictionnaire spécialisé pour stocker des prototypes"""

    def __setitem__(self, key, value):
        # S'assurer que seuls les objets supportant le clonage sont acceptés
        if not hasattr(value, 'clone'):
            raise ValueError(f'L\'objet {value} ne supporte pas le clonage')
        super().__setitem__(key, value)
# Programme principal avec PrototypeManager (exemple)
from prototype_manager import PrototypeManager

manager = PrototypeManager()

# Créer et stocker les prototypes
laptop_proto = Laptop('L0', 'Intel i5', '8 GB', '256 GB SSD', '14"')
manager['laptop'] = laptop_proto

tower_proto = Tower('T0', MainBoard('Generic', 'Budget'), 'Intel i5', '16 GB')
manager['tower'] = tower_proto

# Utiliser le manager pour récupérer et cloner
laptop_L1 = manager['laptop'].clone() | {'model_id': 'L1'}
tower_T1 = manager['tower'].clone()
tower_T1.model_id = 'T1'

Note: The code uses the new | operator for dictionaries introduced in Python 3.9.

Prototype module summary

  • Allows you to reduce the number of class definitions required
  • Competing with the Abstract Factory (but both can be combined)
  • Three types of implementations:
  1. Shallow cloning — copies references of nested objects
  2. Deep cloning — recursively copies nested objects
  3. Prototype manager — maintains a dictionary for easy access to prototypes
  • Warning: Deep clone can cause problems with recursive objects

7. Creational Patterns: Singleton

Introduction to the Singleton pattern

The Singleton Pattern is a creative pattern that:

  • Guarantees that a class has only one instance
  • Useful for controlling access to a limited resource (hardware device, connection pool, buffer pool)
  • Provide a global access point to this single instance
  • Supports lazy instantiation — useful if the object is expensive to instantiate

“There can only be one.” — Highlander

Classic singleton

Use case: A logging subsystem. There can only be one instance controlling the log file.

# singleton_logger.py (exemple)
class Singleton:
    _instance = None

    @staticmethod
    def instance():
        """Méthode statique pour obtenir l'unique instance"""
        if '_instance' not in Singleton.__dict__:
            Singleton._instance = Singleton()
        return Singleton._instance

class Logger(Singleton):
    def open_log(self, path):
        self._log_file = open(path, 'w')

    def write_log(self, message):
        import datetime
        self._log_file.write(f'{datetime.datetime.now()}: {message}\n')

    def close_log(self):
        self._log_file.close()

# Test
s1 = Singleton.instance()
s2 = Singleton.instance()
assert s1 is s2  # Même objet !
s1.ans = 42
assert s2.ans == 42  # Même état !
print("Assertions passées !")

Singleton Problems

The Singleton is sometimes referred to as an antipattern for the following reasons:

  1. Single Responsibility Principle violation — it does two things: manages its own instantiation AND maintains/processes state
  2. Non-standard access to the class — requires knowing how to use the instance() method instead of the normal constructor
  3. More difficult to test — strong coupling with objects that use it, difficult to replace with fakes/mocks for unit tests
  4. Carries a global state — similar to globals in terms of maintenance and testing issues
  5. Difficult to subclassify or reuse for other purposes

Singleton with base class

Solution: Create a base class for all singletons, then inherit from this class.

# base_singleton.py (exemple)
class Singleton:
    """Classe de base pour tous les singletons"""
    _instances = {}  # Dictionnaire : clé = classe, valeur = instance

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

class Logger(Singleton):
    def __init__(self, path='app.log'):
        if not hasattr(self, '_initialized'):
            self._log_file = open(path, 'w')
            self._initialized = True

    def write_log(self, message):
        import datetime
        self._log_file.write(f'{datetime.datetime.now()}: {message}\n')

    def close_log(self):
        self._log_file.close()

Note: The __new__ method is invoked at each class instantiation, but before __init__. It checks if the class is already in the dictionary.

Singleton with Metaclass

# metaclass_singleton.py (exemple)
class Singleton(type):
    """Metaclass pour singleton"""
    _instances = {}

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

class Logger(metaclass=Singleton):
    """La Logger utilise Singleton comme metaclass"""
    def __init__(self, path='app.log'):
        self._log_file = open(path, 'w')

    def write_log(self, message):
        import datetime
        self._log_file.write(f'{datetime.datetime.now()}: {message}\n')

Advantages:

  • The metaclass is completely separate and has one responsibility: managing instances
  • Separation of concerns is complete

MonoState

The MonoState Pattern is an alternative to the Singleton. He accepts the idea that there will be a global state, but does it differently.

# monostate.py (exemple)
class MonoState:
    """Partage l'état entre toutes les instances"""
    _shared_state = {}  # Dictionnaire d'état partagé

    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        instance.__dict__ = cls._shared_state  # Redirige l'état vers le dictionnaire partagé
        return instance

class Logger(MonoState):
    def __init__(self, path='app.log'):
        if not hasattr(self, '_log_file'):
            self._log_file = open(path, 'w')

    def write_log(self, message):
        import datetime
        self._log_file.write(f'{datetime.datetime.now()}: {message}\n')

# Test
logger1 = Logger('test.log')
logger2 = Logger('ignored.log')  # Le chemin est ignoré
# logger1 et logger2 partagent le même état !
assert logger1._log_file is logger2._log_file

Singleton module summary

The Singleton is useful for specific applications but has tradeoffs:

Advantages:

  • Single Instance Access Control
  • Lazy instantiation
  • Reduce size of global namespace
  • Subclassable with base class or metaclass techniques
  • More flexible than a static class

Four implementations seen:

  1. Classic singleton with instance() method
  2. Singleton base class (uses __new__)
  3. Metaclass Singleton
  4. MonoState

Important: Use the Singleton sparingly. It is considered by many to be an antipattern.


8. Structural Patterns: Adapt

Introduction to the Adapter pattern

The Adapter Pattern is a structural pattern that:

  • Converts the interface of one class to another expected by clients
  • Allows classes to work together even if their interfaces are incompatible
  • Often provides features that the adapted class does not have

Real-life analogies:

  • Mains adapters (AC to DC)
  • Plumbing fittings (change in pipe size or type)
  • Travel adapters (electrical sockets between continents)

The Adapter Pattern is also known as the Wrapper Pattern.

Motivating example Adapt

We have a program which displays the names and addresses of customers (Customer). Users want it to work with Vendors too, but the API is different:

  • Customer: address property (combined street and number)
  • Vendor: separate number and street properties

Problem: Duplicating code or adding conditional logic would violate the DRY principle.

Object Adapter

The Object Adapter uses composition (favors composition rather than inheritance).

UML structure:

AbsAdapter (ABC)
    ├── adaptee (propriété)
    ├── name (abstrait)
    └── address (abstrait)

VendorAdapter (ObjectAdapter)
    ├── __init__(adaptee: Vendor)
    ├── name → adaptee.name
    └── address → f"{adaptee.number} {adaptee.street}"
# abs_adapter.py (exemple)
import abc

class AbsAdapter(abc.ABC):
    def __init__(self, adaptee):
        self._adaptee = adaptee

    @property
    def adaptee(self):
        return self._adaptee

    @property
    @abc.abstractmethod
    def name(self):
        pass

    @property
    @abc.abstractmethod
    def address(self):
        pass
# vendor_adapter.py (exemple Object Adapter)
from abs_adapter import AbsAdapter

class VendorAdapter(AbsAdapter):
    @property
    def name(self):
        return self.adaptee.name  # Passthrough direct

    @property
    def address(self):
        # Conversion : séparé → combiné
        return f'{self.adaptee.number} {self.adaptee.street}'
# mock_vendors.py (composition à l'exécution)
from vendor import Vendor
from vendor_adapter import VendorAdapter

MOCKVENDORS = (
    VendorAdapter(Vendor('Acme Corp', '100', 'Main St')),
    VendorAdapter(Vendor('Widget Inc', '200', 'Oak Ave')),
)

Class Adapter

The Class Adapter uses multiple inheritance.

# class_vendor_adapter.py (exemple Class Adapter)
from customer import Customer
from vendor import Vendor

class VendorAdapter(Customer, Vendor):
    """Adapte Vendor pour ressembler à Customer via héritage multiple"""

    def __init__(self, name, number, street):
        # Python MRO : le constructeur de Vendor sera appelé en premier
        super().__init__(name, number, street)

    @property
    def address(self):
        # Override pour combiner number et street
        return f'{self.number} {self.street}'

Object vs Class Adapter comparison

CriterionObjectAdapterClassAdapter
MechanismCompositionMultiple inheritance
FlexibilityVery flexible, works with all subclassesRelated to a specific subclass
DelegationDelegates calls to the adaptedeOverloading adapted methods
SubclassesWorks with all subclasses of the adaptedRelated to a specific subclass
Added behaviorRequires changes if subclass adds behaviorAutomatically supports new subclass behaviors

Recommendation: All things being equal, prefer the Object Adapter as it may be the most flexible approach.


9. Structural Patterns: Bridge

Introduction to the Bridge pattern

The Bridge Pattern is a structural pattern (also known as Handle and Body) that:

  • Decouples an abstraction from its implementation so that both can vary independently
  • Avoid exponential growth of class hierarchy
  • Use composition rather than inheritance

The problem of exponential growth

Example: Online course subscription system with discounts.

Base class:

  • AnnualSubscription: $250/year
  • MonthlySubscription: $25/month

Added discounts:

  • 10% for students
  • 20% for businesses

Result with inheritance:

AnnualSubscription
    ├── AnnualStudentSubscription (nouveau)
    └── AnnualCorporateSubscription (nouveau)
MonthlySubscription
    ├── MonthlyStudentSubscription (nouveau)
    └── MonthlyCorporateSubscription (nouveau)

Problem: Already 6 classes! Adding a permanent subscription would require 9 classes. Adding a senior discount would double the total again.

Three issues identified:

  1. Exponential growth of classes
  2. Code duplication (DRY violation)
  3. Too much code to maintain

Implementation of the Bridge pattern

UML structure:

AbsSubscription (abstraction principale)
    ├── _discount (référence à l'implémenteur)
    ├── price_base (abstrait)
    └── price (concret — applique la remise)

AnnualSubscription       MonthlySubscription
    └── price_base          └── price_base

Discount (implémenteur)
    └── discount (abstrait)

StudentDiscount    CorporateDiscount    NoDiscount
    └── discount=0.1  └── discount=0.2   └── discount=0.0

Discount classes (the implementer):

# discounts.py (exemple)
import abc

class Discount(abc.ABC):
    @property
    @abc.abstractmethod
    def discount(self):
        pass

class StudentDiscount(Discount):
    @property
    def discount(self):
        return 0.10  # 10%

class CorporateDiscount(Discount):
    @property
    def discount(self):
        return 0.20  # 20%

class NoDiscount(Discount):
    """Null Pattern — pas de remise"""
    @property
    def discount(self):
        return 0.0

Subscription class with Bridge:

# subscription.py (exemple)
import abc

class AbsSubscription(abc.ABC):
    def __init__(self, subscriber, start_date, discount):
        self._subscriber = subscriber
        self._start_date = start_date
        self._discount = discount  # L'implémenteur (le pont)

    @property
    @abc.abstractmethod
    def price_base(self):
        """Prix de base avant remise"""
        pass

    @property
    def price(self):
        """Prix avec remise appliquée — non abstrait, commun à tous"""
        return self.price_base * (1 - self._discount.discount)

class AnnualSubscription(AbsSubscription):
    @property
    def price_base(self):
        return 250.00

class MonthlySubscription(AbsSubscription):
    @property
    def price_base(self):
        return 25.00

Main program:

# __main__.py (exemple)
from subscription import AnnualSubscription, MonthlySubscription
from discounts import StudentDiscount, CorporateDiscount, NoDiscount
import datetime

# Test avec remises
sub1 = AnnualSubscription('Alice', datetime.date.today(), StudentDiscount())
sub2 = AnnualSubscription('Corp Inc', datetime.date.today(), CorporateDiscount())
sub3 = MonthlySubscription('Bob', datetime.date.today(), NoDiscount())

for sub in [sub1, sub2, sub3]:
    print(f'Prix : {sub.price:.2f}$')

Linear growth:

  • 2 subscription types × 3 discounts = only 5 classes instead of 6+ subclasses

Bridge module summary

  • The Bridge controls growth by allowing the implementer to vary independently of the main structure
  • Uses composition — the implementer is composed with the main class
  • Extensible: you can add as many “bridge spans” as necessary
  • Class growth is linear rather than exponential
  • Respects the DRY and favoring composition over inheritance principles

10. Structural Patterns: Composite

Introduction to the Composite pattern

The Composite Pattern is a structural pattern that manages part-whole hierarchies (trees). It allows you to:

  • Process individual objects and collections of objects evenly
  • Navigating tree structures without special code
  • Easily add new component types

Examples of tree structures:

  • Employee hierarchies
  • Family trees
  • Nested groups
  • Scalable taxonomy

Motivating example: family trees

We want to find the oldest person in a family, including non-member singles and overlapping families.

Problem with naive approach:

# Approche naïve — deux boucles différentes
oldest = None
for person in family:
    if oldest is None or person.birthdate < oldest.birthdate:
        oldest = person

for person in singles:  # Boucle séparée !
    if oldest is None or person.birthdate < oldest.birthdate:
        oldest = person

Problem: Two distinct loops, and if we add a second generation, we would need an even different recursive function.

Structure of the Composite pattern

AbsComposite (ABC)
    └── get_oldest() [abstrait]

Person (Leaf)                  Tree (Composite)
    └── get_oldest()               ├── members: List[Person | Tree]
        → retourne self            └── get_oldest()
                                       → utilise reduce() sur tous les membres

Key points:

  • Leaf nodes are at the bottom of the tree — no members
  • Composite nodes (subtrees) have children which can be leaves or other composites
  • The client uses the abstract interface to access the entire structure

Implementing the Composite pattern

Abstract base class:

# abs_composite.py (exemple)
import abc

class AbsComposite(abc.ABC):
    @abc.abstractmethod
    def get_oldest(self):
        pass

The Person (Leaf) class:

# person.py (exemple)
import datetime
from abs_composite import AbsComposite

class Person(AbsComposite):
    def __init__(self, name, birthdate):
        self.name = name
        self.birthdate = birthdate

    def get_oldest(self):
        return self  # La personne est la plus vieille d'elle-même

class NullPerson(Person):
    """Null Pattern — personne fictive avec date maximale"""
    def __init__(self):
        super().__init__('', datetime.date.max)

The Tree (Composite) class:

# tree.py (exemple)
from collections.abc import Iterable
from functools import reduce
from abs_composite import AbsComposite
from person import NullPerson

class Tree(Iterable, AbsComposite):
    def __init__(self, members=None):
        self._members = members or []

    def __iter__(self):
        return iter(self._members)

    def get_oldest(self):
        def _get_older(person1, person2):
            return person1 if person1.birthdate <= person2.birthdate else person2.get_oldest()

        return reduce(_get_older, self, NullPerson())

Main program:

# __main__.py (exemple)
import datetime
from person import Person
from tree import Tree

# Créer des personnes
arthur = Person('Arthur', datetime.date(1952, 3, 11))
trillian = Person('Trillian', datetime.date(1965, 7, 1))
ford = Person('Ford', datetime.date(1973, 4, 15))
marvin = Person('Marvin', datetime.date(1980, 2, 20))  # Célibataire

# Créer des arbres
family = Tree([arthur, trillian, ford])
singles = Tree([marvin])

# Tout regrouper dans un grand arbre
all_people = Tree([family, singles])

# Trouver la personne la plus âgée — une seule ligne !
oldest = all_people.get_oldest()
print(f'La plus âgée : {oldest.name}')

Composite module summary

  • Provides a single interface to access a tree structure
  • Allows unified access to subtrees and leaves
  • Simplifies client code — no more need for runtime type tests
  • Easy to add new component types (Open/Closed principle)
  • Possible improvements:
  • Child nodes can maintain references to their parents (navigation up)
  • Components can be shared (memory saving)

11. Structural Patterns: Decorator

Introduction to the Decorator pattern

The Decorator Pattern is a structural pattern that:

  • Adds new responsibilities to an object dynamically at runtime
  • Reduces subclass proliferation by avoiding one subclass per combination
  • Respects the Open/Closed principle

The Decorator Pattern is also known as the Wrapper Pattern.

Use case: A car dealership sells cars with many options (engine, color, upholstery). Each combination of template + options would create an explosion of subclasses.

Problem calculation: 3 models × 2 engines × 3 colors × 2 upholstery = 36 subclasses minimum!

Naive approach by subclasses

# Approche naïve — 1 classe par combinaison
class EconomyCar4CylWhiteVinyl(EconomyCar):
    @property
    def description(self):
        return 'Economy Car, 4-Cylinder, White, Vinyl'

    @property
    def cost(self):
        return 15000  # + options

class EconomyCar6CylWhiteVinyl(EconomyCar):
    @property
    def description(self):
        return 'Economy Car, 6-Cylinder, White, Vinyl'

    @property
    def cost(self):
        return 16200  # + moteur V6
# ... et 34 autres classes !

Approach by properties

# Deuxième approche — propriétés dans la classe de base
class AbsCar(abc.ABC):
    def __init__(self, engine, paint, upholstery):
        self._engine = engine
        self._paint = paint
        self._upholstery = upholstery

    @property
    def cost(self):
        # Calcul du coût des options — SRP violé !
        total = 0
        if self._engine == 'V6':
            total += 1200
        # ...
        return total

SOLID Violations:

  • S: ABC should not calculate the aggregate cost of options
  • Y: Adding/changing options requires opening ABC and subclasses
  • I: The cost method should have its own abstraction
  • D: Concrete classes depend on the implementation of cost in the ABC
  • DRY: Code duplication everywhere

Implementing the Decorator pattern

UML structure:

AbsCar (abstract component)
    ├── description (abstrait)
    └── cost (abstrait)

EconomyCar     LuxuryCar     SportCar
(concrete components)

AbsDecorator (hérite de AbsCar, composé avec AbsCar)
    ├── _car (référence au composant décoré)
    └── car (propriété)

V6Decorator    LeatherDecorator    RedPaintDecorator
(concrete decorators)

Decorator abstract base class:

# decorators/abs_decorator.py (exemple)
import abc
from cars.abs_car import AbsCar

class AbsDecorator(AbsCar):
    def __init__(self, car):
        self._car = car  # Composition — référence à la voiture décorée

    @property
    def car(self):
        return self._car

Concrete Decorator V6:

# decorators/v6_decorator.py (exemple)
from decorators.abs_decorator import AbsDecorator

class V6Decorator(AbsDecorator):
    @property
    def description(self):
        return self.car.description + ', V6'  # Ajoute à la description existante

    @property
    def cost(self):
        return self.car.cost + 1200  # Ajoute le coût du moteur V6

Main program — successive decoration:

# __main__.py (exemple)
from cars.economy import EconomyCar
from decorators.v6_decorator import V6Decorator
from decorators.leather_decorator import LeatherDecorator
from decorators.red_paint_decorator import RedPaintDecorator

def main():
    # Voiture de base
    car = EconomyCar()
    print(f'{car.description}: ${car.cost}')

    # Ajouter un moteur V6
    car = V6Decorator(car)
    print(f'{car.description}: ${car.cost}')

    # Ajouter une sellerie en cuir
    car = LeatherDecorator(car)
    print(f'{car.description}: ${car.cost}')

    # Ajouter une peinture rouge
    car = RedPaintDecorator(car)
    print(f'{car.description}: ${car.cost}')
Sortie :
Economy Car: $15000
Economy Car, V6: $16200
Economy Car, V6, Leather: $17700
Economy Car, V6, Leather, Red Paint: $18200

Differences with Python decorators

AppearanceDecorator Pattern (GoF)Python decorators
SyntaxClasses inheriting an ABCdef or callable classes, @ syntax
What is wrappedClass instancesFunction/method/class definitions
Moment of ActionExecution (runtime)Compile time expansion
Added functionalityTo class instancesTo functions, methods and classes
FocusNarrow, specificGeneral purpose
ReferenceGang of FourPEP 318

Decorator module summary

  • Use the Decorator Pattern to add functionality to existing objects
  • Better approach than adding many subclasses with small variations
  • Better approach than adding many properties to a top level class
  • With many decorators, consider Factory and/or Builder patterns to return decorated objects
  • The Prototype Pattern is also a great way to solve this problem

12. Structural Patterns: Facade

Introduction to the Facade pattern

The Facade Pattern is a structural pattern (as its name suggests, it puts a new face on something) which:

  • Presents a unified interface to a set of interfaces
  • Simplifies the use of complex or multiple APIs
  • Reduces complexity for client programs

Example of accessing a database:

  1. Ask the DBA which base to use
  2. Get the right Python modules
  3. Instantiate a control object
  4. Construct the connection string
  5. Connect to the database
  6. Run query
  7. Process the results
  8. Disconnect and release resources

All this to perhaps read a single line from an employees table!

Motivating example: database access

# Approche naïve — beaucoup de code boilerplate
import pyodbc

CONSTR = 'DRIVER={SQL Server};SERVER=localhost;DATABASE=AdventureWorks;Trusted_Connection=yes'

def get_employees():
    conn = pyodbc.connect(CONSTR)
    query = """
        SELECT TOP 5 DISTINCT
            p.LastName, p.FirstName
        FROM HumanResources.Employee e
        JOIN Person.Person p ON e.BusinessEntityID = p.BusinessEntityID
        ORDER BY p.LastName, p.FirstName
    """
    cursor = conn.cursor()
    cursor.execute(query)
    for row in cursor:
        print(f'{row.LastName}, {row.FirstName}')
    conn.commit()
    conn.close()

get_employees()

Problems:

  • Too much boilerplate code
  • Each developer will write this code differently
  • If the DBA changes the database, all code must be updated

Implementation of the Façade pattern

Structure:

AbsFacade (ABC)
    └── get_employees() [abstrait]

SqlServerFacade
    └── get_employees() — implémentation SQL Server

# Dans le package __init__.py :
PROVIDER = 'sql_server'
CONSTR = '...'
QUERY = '...'

Abstract base class:

# get_employees/abs_facade.py (exemple)
import abc

class AbsFacade(abc.ABC):
    @abc.abstractmethod
    def get_employees(self):
        pass

SQL Server facade:

# get_employees/sql_server.py (exemple)
import pyodbc
from . import CONSTR, QUERY
from .abs_facade import AbsFacade

class SqlServerFacade(AbsFacade):
    def get_employees(self):
        conn = pyodbc.connect(CONSTR)
        cursor = conn.cursor()
        cursor.execute(QUERY)
        for row in cursor:
            print(f'{row.LastName}, {row.FirstName}')
        conn.commit()
        conn.close()

Package __init__.py:

# get_employees/__init__.py (exemple)
PROVIDER = 'sql_server'
CONSTR = 'DRIVER={SQL Server};SERVER=localhost;DATABASE=AdventureWorks;Trusted_Connection=yes'
QUERY = """
    SELECT TOP 5 DISTINCT
        p.LastName, p.FirstName
    FROM HumanResources.Employee e
    JOIN Person.Person p ON e.BusinessEntityID = p.BusinessEntityID
    ORDER BY p.LastName, p.FirstName
"""

Main program:

# __main__.py (exemple)
import importlib
from get_employees import PROVIDER

# Chargement dynamique de la façade selon le PROVIDER
module = importlib.import_module(f'get_employees.{PROVIDER}')

# Trouver et instancier la classe concrète
facade_class = [cls for name, cls in vars(module).items() 
                if isinstance(cls, type) and not name.startswith('Abs')][0]
facade = facade_class()
facade.get_employees()

Summary of the Facade module

Advantages of the Facade:

  • Protects clients from subsystem details
  • Reduces the number of objects that clients must interact with
  • Promotes weak coupling
  • Allows you to vary the subsystem or add new ones without changing the customer code
  • Nothing is lost — customers can still use subsystems directly if necessary

13. Structural Patterns: Flyweight

Introduction to the Flyweight pattern

The Flyweight Pattern is a structural pattern that:

  • Uses a single shared object to store data
  • Reduces the number of required object instances (sometimes to just one)
  • Fixes memory issues when an application has millions or billions of small objects

Motivating example: The Large Hadron Collider (LHC) at CERN records collisions at one billion events per second. With a traditional OO approach, each event would be an object — this would be impossible in Python.

Naive approach: the LHC

# Approche naïve — 1 objet par événement
import sys
import random

class Event:
    def __init__(self, x, y, t, e):
        self.x = x  # Position X sur le détecteur
        self.y = y  # Position Y sur le détecteur
        self.t = t  # Temps d'arrivée (nanosecondes)
        self.e = e  # Niveau d'énergie

    def get_velocity(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5 / (self.t * 1e-9)

# Simulation
start_time = time.time_ns()
events = []
for _ in range(100):
    x = random.uniform(0, 1.2)  # Rayon du détecteur Atlas
    y = random.uniform(0, 6.2)  # Longueur du détecteur Atlas
    t = random.uniform(start_time, time.time_ns())
    e = random.uniform(0.1, 1000.0)
    events.append(Event(x, y, t, e))

print(f'Taille d\'un événement : {sys.getsizeof(events[0])} bytes')
print(f'Taille totale de la liste : {sys.getsizeof(events) + sum(sys.getsizeof(e) for e in events)} bytes')

Result: Each event consumes 112 bytes. Imagine billions of events…

Implementation of the Flyweight pattern

UML structure:

AbsFlyweight (ABC)
    └── get_velocity() [abstrait]

SharedEvents (implémente AbsFlyweight)
    ├── __init__(x_size, y_size) → crée un tableau NumPy
    ├── set_event(x, y, t, e)
    ├── get_event(x, y)
    └── get_velocity(x, y)

FlyweightFactory
    └── get_flyweight(x_size, y_size) → SharedEvents
# flyweight.py (exemple)
import abc
import numpy as np

class AbsFlyweight(abc.ABC):
    @abc.abstractmethod
    def get_velocity(self, x, y):
        pass

class SharedEvents(AbsFlyweight):
    """Un seul objet partagé pour tous les événements"""

    def __init__(self, x_size, y_size):
        # Tableau NumPy 3D : [x][y][t, e]
        self._events = np.zeros((x_size, y_size, 2), dtype=np.float64)

    def set_event(self, x, y, t, e):
        """Stocker un événement à la position (x, y)"""
        self._events[x][y] = [t, e]

    def get_event(self, x, y):
        """Récupérer un événement à la position (x, y)"""
        return self._events[x][y]

    def get_velocity(self, x, y):
        """Calcul de vitesse pour cet événement"""
        event = self.get_event(x, y)
        t = event[0]
        return (x ** 2 + y ** 2) ** 0.5 / (t * 1e-9)
# factory.py (exemple)
from flyweight import SharedEvents

class FlyweightFactory:
    @staticmethod
    def get_flyweight(x_size, y_size):
        return SharedEvents(x_size, y_size)
# __main__.py (exemple)
import random
import time
from factory import FlyweightFactory

# Paramètres du détecteur Atlas
X_SIZE = 12  # 1.2m / 0.1m de résolution
Y_SIZE = 62  # 6.2m / 0.1m de résolution

factory = FlyweightFactory()
events = factory.get_flyweight(X_SIZE, Y_SIZE)

start_time = time.time_ns()
# Simuler 100 événements — UN SEUL OBJET !
for _ in range(100):
    x = random.randint(0, X_SIZE - 1)
    y = random.randint(0, Y_SIZE - 1)
    t = random.uniform(start_time, time.time_ns())
    e = random.uniform(0.1, 1000.0)
    events.set_event(x, y, t, e)

# Afficher quelques statistiques
print(f'Vitesse à (5, 10) : {events.get_velocity(5, 10):.2f} m/s')

Flyweight module summary

  • Useful when an application uses a lot of similar small objects that consume too many resources
  • Use efficient shared state rather than individual objects
  • Used with NumPy for large-scale numerical data
  • Often used with other patterns:
  • Composite — to build efficient tree structures
  • State and Strategy — to implement them efficiently

14. Structural Patterns: Proxy

Introduction to the Proxy pattern

The Proxy Pattern is a structural pattern that acts on behalf of something else (like a proxy vote). He :

  • Controls access to the real object (the subject)
  • Exposes identical interface to client
  • May be responsible for creating and destroying the actual object

Types of proxies

TypeDescriptionExample
Remote ProxyLocal representative of an object in a different address spaceWeb client accessing a database behind a firewall
Virtual ProxyCreates expensive items on demand (lazy loading)Return data from a long query only when necessary
Proxy ProtectionControls access according to access rightsRestrict access to employees’ personal information
Smart Reference ProxyAdditional actions during accessReference counting, locking for multithreading

Note: DBMSs use all 4 types of proxies!

Motivating example: employee access control

We want to control access to an employee object which contains sensitive information (date of birth, salary). An AccessControl object indicates which employees can see personal data.

Naive approach:

# Approche naïve avec logique de contrôle dans le programme principal
def print_employee_info(employee_ids, requester_id):
    for emp_id in employee_ids:
        emp = EMPLOYEES.get(emp_id)
        if emp:
            details = f'ID: {emp.employee_id}, Nom: {emp.name}'
            # Vérifier les droits d'accès
            if requester_id in ACCESS_CONTROL and ACCESS_CONTROL[requester_id].can_see_personal:
                details += f', Né(e) le: {emp.birthdate}, Salaire: {emp.salary}'
            print(details)

Problem: Access control logic is in the main program — violation of SRP and Open/Closed Principle.

Proxy pattern implementation

Structure:

AbsEmployees (ABC)
    └── get_employee_info(employee_ids, requester_id)

Employees (ConcreteSubject)
    └── get_employee_info() → générateur d'employés

EmployeesProxy (Proxy)
    ├── __init__(employees, requester_id)
    └── get_employee_info() → applique la protection
# abs_employees.py (exemple)
import abc

class AbsEmployees(abc.ABC):
    @abc.abstractmethod
    def get_employee_info(self, employee_ids, requester_id):
        pass
# employees.py (exemple)
from abs_employees import AbsEmployees
from test_data import EMPLOYEES

class Employees(AbsEmployees):
    def get_employee_info(self, employee_ids, requester_id):
        for emp_id in employee_ids:
            if emp_id in EMPLOYEES:
                yield EMPLOYEES[emp_id]
# employees_proxy.py (exemple)
from abs_employees import AbsEmployees
from test_data import ACCESS_CONTROL

class EmployeesProxy(AbsEmployees):
    def __init__(self, employees, requester_id):
        self._employees = employees  # Composition !
        self._requester_id = requester_id

    def get_employee_info(self, employee_ids, requester_id):
        for emp in self._employees.get_employee_info(employee_ids, requester_id):
            # Déterminer le niveau d'accès
            can_see_personal = (
                emp.employee_id == self._requester_id or
                (self._requester_id in ACCESS_CONTROL and
                 ACCESS_CONTROL[self._requester_id].can_see_personal)
            )

            if can_see_personal:
                yield emp  # Objet complet avec données personnelles
            else:
                # Créer un objet masqué (données personnelles cachées)
                yield EmployeePublicView(emp)

Main program:

# __main__.py (exemple)
from employees import Employees
from employees_proxy import EmployeesProxy

def print_info(requester_id, employee_ids):
    employees = Employees()
    proxy = EmployeesProxy(employees, requester_id)

    for emp in proxy.get_employee_info(employee_ids, requester_id):
        print(emp)

print_info('EMP001', ['EMP001', 'EMP002', 'EMP003'])

Proxy module summary

Significant consequences:

  • Introduces a level of indirection when accessing the actual topic
  • The Virtual Proxy can do lazy instantiation or caching — the lru_cache of functools is indeed a virtual proxy
  • The Remote Proxy can hide communication details (ex: pyodbc)
  • The Smart Proxy can add housekeeping (locking for multithreading)
  • Respects the Open/Closed principle
  • Prefer composition to inheritance

When to use: Always when you want to add controls to an object while remaining faithful to the Open/Closed principle. Proxies can be combined without knowing each other.


15. Behavioral Patterns: Strategy

Introduction to the Strategy pattern

The Strategy Pattern is a behavioral pattern that:

  • Provides a way to encapsulate a family of algorithms
  • Make algorithms interchangeable
  • Separates algorithms from the context in which they operate
  • Is also known as Policy Pattern

Feature: The algorithms have the same inputs and outputs, but their implementations can be very different (eg: Newtonian gravity vs. Einstein’s general relativity — same inputs/outputs, similar results on Earth).

Motivating example: calculation of delivery costs

Specifications: Calculate delivery costs for FedEx, UPS and La Poste. The system must be expandable (new carriers).

Problem with naive approach:

# Approche naïve — if/elif/else
class ShippingCost:
    def shipping_cost(self, order):
        if order.shipper == Shipper.FEDEX:
            return 3.00
        elif order.shipper == Shipper.UPS:
            return 4.00
        elif order.shipper == Shipper.POSTAL:
            return 1.50
        else:
            raise ValueError(f'Transporteur inconnu: {order.shipper}')

SOLID Violations:

  • S: An order should not manage how it will be delivered
  • Y: Edit class to add new carriers
  • D: Programming towards a concrete implementation

Warning signal: A long if/elif/else list may indicate the opportunity to apply the Strategy Pattern.

Implementation of the Strategy pattern

UML structure:

ShippingCost (Context)
    └── _strategy: AbsStrategy

AbsStrategy (ABC)
    └── calculate(order) [abstrait]

FedExStrategy    PostalStrategy    UPSStrategy
(ConcreteStrategies)

Policy abstract base class:

# strategy/strategy_abc.py
import abc

class AbsStrategy(abc.ABC):
    @abc.abstractmethod
    def calculate(self, order):
        """Calculer les frais de livraison"""
        pass

The context:

# strategy/shipping_cost.py
class ShippingCost(object):
    def __init__(self, strategy):
        self._strategy = strategy

    def shipping_cost(self, order):
        return self._strategy.calculate(order)

Concrete strategies:

# strategy/fedex_strategy.py (exemple)
from strategy.strategy_abc import AbsStrategy

class FedExStrategy(AbsStrategy):
    def calculate(self, order):
        return 3.00  # Ou calcul réel basé sur l'ordre

# strategy/ups_strategy.py (exemple)
class UPSStrategy(AbsStrategy):
    def calculate(self, order):
        return 4.00

# strategy/postal_strategy.py (exemple)
class PostalStrategy(AbsStrategy):
    def calculate(self, order):
        return 1.50

Main program:

# __main__.py (exemple)
from strategy.shipping_cost import ShippingCost
from strategy.fedex_strategy import FedExStrategy
from strategy.ups_strategy import UPSStrategy
from strategy.postal_strategy import PostalStrategy

order = Order()  # Objet commande

for strategy_class, expected_cost in [
    (FedExStrategy, 3.00),
    (UPSStrategy, 4.00),
    (PostalStrategy, 1.50)
]:
    strategy = strategy_class()
    cost_calculator = ShippingCost(strategy)
    cost = cost_calculator.shipping_cost(order)
    assert cost == expected_cost, f'Coût attendu {expected_cost}, obtenu {cost}'

print('Tests passés !')

Variations: functions and lambdas

In Python, functions are first-class objects, which allows strategies to be encapsulated in functions.

# Variation avec fonctions et lambdas
class ShippingCost:
    def __init__(self, strategy):
        self._strategy = strategy  # Callable, pas nécessairement une classe

    def shipping_cost(self, order):
        return self._strategy(order)  # Appel direct

# Stratégie comme fonction
def fedex_strategy(order):
    return 3.00

# Stratégie comme lambda
ups_strategy = lambda order: 4.00

# Lambda directement dans l'instanciation
postal_cost = ShippingCost(lambda order: 1.50)

# Test
order = Order()
assert ShippingCost(fedex_strategy).shipping_cost(order) == 3.00
assert ShippingCost(ups_strategy).shipping_cost(order) == 4.00
assert postal_cost.shipping_cost(order) == 1.50
print('Tests passés !')

Strategy module summary

Advantages of the Strategy Pattern:

  1. Fixes SOLID violations of naive approach
  2. Each algorithm is separate — easy to test in isolation
  3. Easy to test external code with deterministic mocks/fakes
  4. Eliminate if/elif/else structures — a potential refactoring signal
  5. Three techniques available:
  • Separate classes inheriting from an ABC
  • Functions
  • Lambda expressions for simple cases

16. Behavioral Patterns: Command

Introduction to the Command pattern

The Command Pattern is a behavioral pattern (also called Action Pattern or Transaction Pattern) which:

  • Wraps a query as an object
  • Parameterize objects with different queries (signatures may differ, unlike Strategy)
  • Supports queues and logs (for audits)
  • Facilitates Undo/Redo operations
  • Used in toolkits, CLI programs, GUI menus

Motivating example: CLI command processing

Specifications: Order processing system for order lines. Three operations: create order, update quantity, ship.

Problem with naive approach:

# Approche naïve
class CommandExecutor:
    def execute_command(self, command, *args):
        if command == 'CreateOrder':
            self._create_order(*args)
        elif command == 'UpdateQuantity':
            self._update_quantity(*args)
        elif command == 'ShipOrder':
            self._ship_order(*args)
        # ...

    def _update_quantity(self, product, old_qty, new_qty):
        print(f'Mise à jour de {product}: {old_qty} → {new_qty}')

SOLID Violations:

  • S: CommandExecutor parses AND processes commands
  • Y: Must be modified to add/change commands
  • D: Depends on the implementation of execute_command

Implementation of the Command pattern

UML structure:

Client (programme principal — Invoker)
    └── utilise AbsCommand

AbsCommand (ABC)
    └── execute() [abstrait]

AbsOrderCommand (ABC)
    ├── name [abstrait]
    └── description [abstrait]

CreateOrder    UpdateQuantity    ShipOrder    NoCommand
(ConcreteCommands)

Abstract base class:

# abs_command.py (exemple)
import abc

class AbsCommand(abc.ABC):
    @abc.abstractmethod
    def execute(self):
        pass

class AbsOrderCommand(AbsCommand):
    """Base pour les commandes de traitement d'ordre"""
    name = None        # Sera défini comme constante dans les sous-classes
    description = None # Sera défini comme constante dans les sous-classes

Concrete command UpdateQuantity:

# update_quantity.py (exemple)
from abs_command import AbsOrderCommand

class UpdateQuantity(AbsOrderCommand):
    name = 'UpdateQuantity'
    description = 'Mettre à jour la quantité d\'un article'

    def __init__(self, product, new_quantity):
        self._product = product
        self._new_quantity = new_quantity

    def execute(self):
        # Simulation de la mise à jour (en pratique, accès base de données)
        old_value = 10  # Valeur simulée
        print(f'Mise à jour de {self._product}: {old_value} → {self._new_quantity}')

NoCommand (Null Pattern):

# no_command.py (exemple)
from abs_command import AbsOrderCommand

class NoCommand(AbsOrderCommand):
    name = 'NoCommand'
    description = 'Commande invalide ou inconnue'

    def execute(self):
        print('Commande inconnue. Utilisation : create | update | ship')

Main program (Invoker):

# __main__.py (exemple)
import sys
from update_quantity import UpdateQuantity
from no_command import NoCommand

# Dictionnaire de commandes disponibles
COMMANDS = {
    'UpdateQuantity': UpdateQuantity,
    # Ajouter facilement de nouvelles commandes ici
}

def main():
    if len(sys.argv) < 2:
        print('Utilisation: python __main__.py <commande> [arguments]')
        return

    command_name = sys.argv[1]
    args = sys.argv[2:]

    command_class = COMMANDS.get(command_name, NoCommand)
    command = command_class(*args) if command_class != NoCommand else NoCommand()
    command.execute()

if __name__ == '__main__':
    main()

Undo / Redo

Use case: Application with a menu of actions that can be undone.

# abs_command_undo.py (exemple)
import abc

class AbsCommand(abc.ABC):
    @abc.abstractmethod
    def execute(self):
        pass

    @abc.abstractmethod
    def undo(self):
        pass
# door_commands.py (exemple)
from door import Door
from abs_command_undo import AbsCommand

class LockDoor(AbsCommand):
    def __init__(self, door: Door):
        self._door = door

    def execute(self):
        self._door.lock()

    def undo(self):
        self._door.unlock()  # Inverse l'action

class UnlockDoor(AbsCommand):
    def __init__(self, door: Door):
        self._door = door

    def execute(self):
        self._door.unlock()

    def undo(self):
        self._door.lock()

Multi-level undo management:

# menu_action.py (exemple)
class MenuAction:
    def __init__(self):
        self._history = []  # Pile d'actions exécutées

    def execute(self, command):
        command.execute()
        self._history.append(command)

    def undo(self):
        if self._history:
            command = self._history.pop()
            command.undo()
        else:
            print('Rien à annuler')

Command module summary

  • Excellent for encapsulating behaviors and separating logic from client control
  • Simple to use for CLI programs
  • Very useful for adding validation, undo/redo
  • Each menu item can be considered as an order
  • Numerous application possibilities

17. Behavioral Patterns: State

Introduction to the State pattern

The State Pattern is a behavioral pattern which:

  • Manages the behavior of an object which changes depending on its state
  • Encapsulate state-specific behaviors into separate classes
  • Eliminate long if/elif/else strings that check status
  • Is similar to Strategy Pattern (same structure, different intent — here we care about the state of the object)

Real life example: A kitchen can be in states: tidy, messy, in use, cleaning.

Motivating example: shopping cart

Cart Statuses:

  • EMPTY: empty
  • NOT_EMPTY: contains articles
  • AT_CHECKOUT: payment in progress
  • PAID_FOR: paid

Transitions:

[EMPTY] --add_item--> [NOT_EMPTY] --remove_last_item--> [EMPTY]
[NOT_EMPTY] --checkout--> [AT_CHECKOUT] --pay--> [PAID_FOR]
[AT_CHECKOUT] --remove_all--> [EMPTY]

Naive approach with constants and if/elif:

# Approche naïve — vérifications d'état partout
EMPTY = 0
NOT_EMPTY = 1
AT_CHECKOUT = 2
PAID_FOR = 3

class ShoppingCart:
    def __init__(self):
        self._state = EMPTY
        self._item_count = 0

    def add_item(self):
        if self._state == EMPTY:
            print('Premier article ajouté')
            self._item_count += 1
            self._state = NOT_EMPTY
        elif self._state == NOT_EMPTY:
            self._item_count += 1
            print(f'Article ajouté. Total: {self._item_count}')
        elif self._state == AT_CHECKOUT:
            print('Impossible d\'ajouter un article en cours de paiement')
        else:
            print('Déjà payé !')

    # ... même chose pour remove_item, checkout, pay

Implementation of the State pattern

UML structure:

AbsState (ABC) — un état du panier
    ├── __init__(context: ShoppingCart) [concret]
    ├── add_item() [abstrait]
    ├── remove_item() [abstrait]
    ├── checkout() [abstrait]
    ├── pay() [abstrait]
    └── empty_cart() [abstrait]

EmptyState    NotEmptyState    CheckoutState    PaidForState
(implémentent AbsState)

ShoppingCart (Context)
    ├── _state (état courant)
    ├── _item_count
    └── délègue add_item(), remove_item(), etc. à l'état courant

State abstract base class:

# abs_state.py (exemple)
import abc

class AbsState(abc.ABC):
    def __init__(self, cart):
        self._cart = cart  # Référence au contexte

    @abc.abstractmethod
    def add_item(self):
        pass

    @abc.abstractmethod
    def remove_item(self):
        pass

    @abc.abstractmethod
    def checkout(self):
        pass

    @abc.abstractmethod
    def pay(self):
        pass

    @abc.abstractmethod
    def empty_cart(self):
        pass

EMPTY status:

# empty_state.py (exemple)
from abs_state import AbsState

class EmptyState(AbsState):
    def add_item(self):
        self._cart._item_count += 1
        print('Premier article ajouté')
        self._cart._state = self._cart._not_empty_state  # Transition !

    def remove_item(self):
        print('Le panier est déjà vide')

    def checkout(self):
        print('Impossible de passer en caisse avec un panier vide')

    def pay(self):
        print('Rien à payer')

    def empty_cart(self):
        print('Le panier est déjà vide')

The ShoppingCart context:

# shopping_cart.py (exemple)
from empty_state import EmptyState
from not_empty_state import NotEmptyState
from checkout_state import CheckoutState
from paid_for_state import PaidForState

class ShoppingCart:
    def __init__(self):
        # Instancier tous les états
        self._empty_state = EmptyState(self)
        self._not_empty_state = NotEmptyState(self)
        self._checkout_state = CheckoutState(self)
        self._paid_for_state = PaidForState(self)

        self._item_count = 0
        self._state = self._empty_state  # État initial

    def add_item(self):
        self._state.add_item()  # Délègue à l'état courant !

    def remove_item(self):
        self._state.remove_item()

    def checkout(self):
        self._state.checkout()

    def pay(self):
        self._state.pay()

    def empty_cart(self):
        self._state.empty_cart()

State module summary

Significant consequences:

  1. Encapsulates state-specific behaviors — behavior varies dramatically between EMPTY and AT_CHECKOUT
  2. Distributes behavior over state classes — less compact solution but eliminates long conditionals
  3. Makes adding new states easier — most of the work is in the new state
  4. Explicit state transitions without complex code in context
  5. Possibility of sharing states (Flyweight pattern) if no instance variables
  6. Performance: you can create the states in advance (CPU saving) or at the transition (memory saving)

18. Behavioral Patterns: Observe

Introduction to the Observer pattern

The Observer Pattern is a behavioral pattern (also called Dependents Pattern or Publisher-Subscribe Pattern) which:

  • Defines a one-to-many relationship between a set of objects
  • Notify all dependents when the state of an object changes
  • Allows observers to attach and detach from the subject

Analogies:

  • UN Permanent Observers
  • Subscriptions to a newspaper or magazine
  • Twitter/YouTube subscriptions (push service)

Motivating example: KPI dashboard

Specifications: Dashboard for a technical support center displaying KPIs: open tickets, new tickets per hour, closed tickets.

Problem with naive approach:

# Approche naïve — difficile d'ajouter des observers
kpis = load_kpis()
print(f'Tickets ouverts: {kpis.open_tickets}')
print(f'Nouveaux tickets: {kpis.new_tickets}')
print(f'Tickets fermés: {kpis.closed_tickets}')

Problem: To send the results by email, then via a REST API, then add a new KPI… everything quickly becomes very complex.

Implementation of the Observer pattern

UML structure:

AbsSubject (ABC)
    ├── attach(observer) [concret]
    ├── detach(observer) [concret]
    └── notify(value=None) [concret]

AbsObserver (ABC)
    └── update(value) [abstrait]

KPIs (ConcreteSubject)      CurrentKPIs    ForecastKPIs
    ├── set_kpis(...)         (ConcreteObservers)
    └── notifie les observers quand les valeurs changent

Subject abstract base class:

# abs_subject.py (exemple)
import abc

class AbsSubject(abc.ABC):
    _observers = set()  # Ensemble d'observers

    def attach(self, observer):
        if not isinstance(observer, AbsObserver):
            raise TypeError('observer doit implémenter AbsObserver')
        self._observers.add(observer)

    def detach(self, observer):
        self._observers.discard(observer)

    def notify(self, value=None):
        for observer in self._observers:
            if value is None:
                observer.update()
            else:
                observer.update(value)  # Push notification

Abstract observer base class:

# abs_observer.py (exemple)
import abc

class AbsObserver(abc.ABC):
    @abc.abstractmethod
    def update(self, value=None):
        pass

Concrete subject KPIs:

# kpis.py (exemple)
from abs_subject import AbsSubject

class KPIs(AbsSubject):
    def __init__(self):
        super().__init__()
        self._open_tickets = 0
        self._new_tickets = 0
        self._closed_tickets = 0

    @property
    def open_tickets(self):
        return self._open_tickets

    @property
    def new_tickets(self):
        return self._new_tickets

    @property
    def closed_tickets(self):
        return self._closed_tickets

    def set_kpis(self, open_tickets, new_tickets, closed_tickets):
        self._open_tickets = open_tickets
        self._new_tickets = new_tickets
        self._closed_tickets = closed_tickets
        self.notify()  # Notifier tous les observers !

Observe concrete CurrentKPIs:

# current_kpis.py (exemple)
from abs_observer import AbsObserver

class CurrentKPIs(AbsObserver):
    def __init__(self, kpi_subject):
        self._kpi_subject = kpi_subject
        self._kpi_subject.attach(self)

    def update(self, value=None):
        kpis = self._kpi_subject
        print(f'=== KPIs Actuels ===')
        print(f'Tickets ouverts : {kpis.open_tickets}')
        print(f'Nouveaux tickets : {kpis.new_tickets}')
        print(f'Tickets fermés  : {kpis.closed_tickets}')

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._kpi_subject.detach(self)

Bug fix: dangling reference

The problem: Python is a reference-counting managed language. If the subject maintains a set of references to observers, the count will never drop to 0 — the memory will never be freed. This is a dangling reference.

Solution: Context Managers

# current_kpis.py avec context manager
class CurrentKPIs(AbsObserver):
    def __init__(self, kpi_subject):
        self._kpi_subject = kpi_subject

    def __enter__(self):
        self._kpi_subject.attach(self)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._kpi_subject.detach(self)  # Garantit le détachement même en cas d'exception !
        return False

    def update(self, value=None):
        # ...

Main program with with statements:

# __main__.py (exemple)
from kpis import KPIs
from current_kpis import CurrentKPIs
from forecast_kpis import ForecastKPIs

kpis = KPIs()

with CurrentKPIs(kpis) as current, ForecastKPIs(kpis) as forecast:
    kpis.set_kpis(10, 3, 7)  # Notifie les deux observers
    kpis.set_kpis(12, 5, 8)

# Après le bloc with, les observers sont automatiquement détachés
kpis.set_kpis(15, 6, 9)  # Plus aucune notification !

Observer module summary

  • Defines a one-to-many relationship between objects
  • Widely used in GUI applications (keyboard, mouse, touch events)
  • The MVC pattern (Model-View-Controller) uses Observer (the model is the subject, the view is the observer)
  • Supports push notifications — the value can be sent directly to the update method
  • Important: Always detach observers when they are no longer needed to avoid memory leaks

19. Behavioral Patterns: Visitor

Introduction to the Visitor pattern

The Visitor Pattern is a behavioral pattern that:

  • Adds new functionality to an entire object structure
  • Allows keeping functionality separate from the main structure
  • Reduces the cost and risk of updating many classes in an object structure
  • Disadvantage: can break encapsulation (visitor accesses internal details of objects)

Analogy: A visitor who comes to your home brings his own equipment (camera, bag). The destination is not responsible for this material — it only has to accept the visitor.

Structure of the Visitor pattern

Element (ABC)
    └── accept(visitor) [abstrait]

Person (Leaf Element)    Tree (Composite Element)
    └── accept(visitor)      └── accept(visitor) — itère sur les membres

AbsVisitor (ABC)
    ├── visit_person(person) [abstrait]
    └── visit_tree(tree) [abstrait]

PrettyPrintVisitor    GetOldestVisitor
(ConcreteVisitors)

Implementation of the Visitor pattern

Abstract base class with accept:

# abs_tree.py (exemple)
import abc

class AbsTree(abc.ABC):
    @abc.abstractmethod
    def accept(self, visitor):
        pass

    @abc.abstractmethod
    def get_oldest(self):
        pass

Person with accept:

# person.py (exemple)
import datetime
from abs_tree import AbsTree

class Person(AbsTree):
    def __init__(self, name, birthdate):
        self.name = name
        self.birthdate = birthdate

    def accept(self, visitor):
        visitor.visit_person(self)  # Donne accès à self — brise l'encapsulation

    def get_oldest(self):
        return self

Tree with accept:

# tree.py (exemple)
from abs_tree import AbsTree

class Tree(AbsTree):
    def __init__(self, name, members=None):
        self.name = name
        self._members = members or []

    def accept(self, visitor):
        visitor.visit_tree(self)
        for member in self._members:
            member.accept(visitor)  # Itère sur les membres

    def get_oldest(self):
        # ...

Visitor abstract base class:

# abs_visitor.py (exemple)
import abc

class AbsVisitor(abc.ABC):
    @abc.abstractmethod
    def visit_person(self, person):
        pass

    @abc.abstractmethod
    def visit_tree(self, tree):
        pass

PrettyPrint Visitor:

# pretty_print_visitor.py (exemple)
from abs_visitor import AbsVisitor

class PrettyPrintVisitor(AbsVisitor):
    def visit_person(self, person):
        print(f'  - {person.name} (né(e) le {person.birthdate})')

    def visit_tree(self, tree):
        print(f'Famille: {tree.name}')

GetOldest Visitor:

# get_oldest_visitor.py (exemple)
import datetime
from abs_visitor import AbsVisitor
from person import NullPerson

class GetOldestVisitor(AbsVisitor):
    def __init__(self):
        self._oldest = NullPerson()  # Date maximale

    def visit_person(self, person):
        if person.birthdate < self._oldest.birthdate:
            self._oldest = person

    def visit_tree(self, tree):
        pass  # Les arbres n'ont pas de date de naissance

    @property
    def oldest(self):
        return self._oldest

Main program:

# __main__.py (exemple)
from person import Person
from tree import Tree
from pretty_print_visitor import PrettyPrintVisitor
from get_oldest_visitor import GetOldestVisitor
import datetime

# Construire l'arbre généalogique
arthur = Person('Arthur Dent', datetime.date(1952, 3, 11))
trillian = Person('Trillian', datetime.date(1965, 7, 1))
ford = Person('Ford Prefect', datetime.date(1973, 4, 15))
douglas = Person('Douglas Adams', datetime.date(1952, 3, 11))

family1 = Tree('Famille Dent', [arthur, trillian])
singles = Tree('Célibataires', [ford, douglas])
all_people = Tree('Tous', [family1, singles])

# Utiliser le visiteur PrettyPrint
print_visitor = PrettyPrintVisitor()
all_people.accept(print_visitor)

# Utiliser le visiteur GetOldest
oldest_visitor = GetOldestVisitor()
all_people.accept(oldest_visitor)
oldest = oldest_visitor.oldest
age = (datetime.date.today() - oldest.birthdate).days // 365
print(f'\nLa personne la plus âgée est {oldest.name} ({age} ans)')

Summary and consequences of the Visitor pattern

Advantages:

  1. Easy to add new operations — nothing is changed in existing classes
  2. Separation of concerns — functional logic is separated from the model
  3. Works on different class hierarchies (if they implement accept)
  4. Can accumulate state during visit (see GetOldestVisitor)

Disadvantages:

  1. Breaks encapsulation — visitor has access to internal details
  2. Hard to change data model — each new concrete type requires a new method in all visitor classes
  3. Best when the data model is stable or rarely changes

Python alternative: class decorators can replace visitors and maintain the independence of functional improvements without changing the concrete elements.


20. Behavioral Patterns: Chain of Responsibility

Introduction to the Chain of Responsibility pattern

The Chain of Responsibility Pattern is a behavioral pattern which:

  • Decouples requests from handlers (handlers)
  • Allow multiple handlers to see each request
  • Each handler can process the request or pass it to the next one

Use case: Graphical applications where processing depends on the context (where the mouse is clicked, for example).

Linked chain implementation

Client → PetHandler (head) → CatHandler → DogHandler → FishHandler → PetHandler (null successor)

Abstract manager:

# handlers/abs_pet_handler.py (exemple)
import abc

class AbsPetHandler(abc.ABC):
    def __init__(self, successor=None):
        self._successor = successor

    @property
    def successor(self):
        return self._successor

    @successor.setter
    def successor(self, value):
        self._successor = value

    def handle(self, request):
        """Passer la requête au successeur si disponible"""
        if self._successor:
            self._successor.handle(request)

Concrete CatHandler Handler:

# handlers/cat_handler.py (exemple)
from abs_pet_handler import AbsPetHandler

class CatHandler(AbsPetHandler):
    def handle(self, request):
        if request.request_type == 'cat':
            print(f'CatHandler: gère {request.description}')
        else:
            super().handle(request)  # Passe au successeur

Chain construction:

# handlers/__init__.py (exemple)
from .cat_handler import CatHandler
from .dog_handler import DogHandler
from .fish_handler import FishHandler
from .abs_pet_handler import AbsPetHandler

def build_chain():
    head = AbsPetHandler()  # Tête de chaîne (pas de successeur)
    for handler_class in [CatHandler, DogHandler, FishHandler]:
        handler = handler_class(head)  # Chaque handler a le précédent comme successeur
        head = handler
    return head

handler_chain = build_chain()

Main program:

# __main__.py (exemple)
from dataclasses import dataclass
from handlers import handler_chain

@dataclass
class Request:
    request_type: str
    description: str

requests = [
    Request('cat', 'Nourrir le chat'),
    Request('dog', 'Promener le chien'),
    Request('fish', 'Changer l\'eau du poisson'),
    Request('bird', 'Type non supporté'),
]

for req in requests:
    handler_chain.handle(req)

Implementation with a list

An alternative is to keep the handlers in a list rather than a linked string.

# handlers_list/__init__.py (exemple)
from .cat_handler import CatHandler
from .dog_handler import DogHandler
from .fish_handler import FishHandler
from .abs_pet_handler import AbsPetHandler

class PetHandlerList(AbsPetHandler):
    def __init__(self):
        self._handlers = []

    def add_successors(self, *handlers):
        for handler in handlers:
            self._handlers.append(handler)

    def handle(self, request):
        for handler in self._handlers:
            if handler.handle(request):  # Retourne True si géré
                return
# Gestionnaire concret avec liste
class CatHandler:
    def handle(self, request):
        if request.request_type == 'cat':
            print(f'CatHandler: gère {request.description}')
            return True  # Géré !
        return None  # Pas géré (retour None = False)

Advantages of the list approach:

  • Handlers are completely independent (no coupling)

Chain of Responsibility module summary

  • Decouple requests from handlers
  • Allow multiple handlers to see each request
  • Two approaches:
  1. Linked chain — conforms to GoF description
  2. List of managers — completely independent managers
  • A possible third approach: use the Composite pattern to put the managers in a tree structure

21. Behavioral Patterns: Mediator

Introduction to the Mediator pattern

The Mediator Pattern is a behavioral pattern that:

  • Reduce interactions between colleague objects
  • Centralizes interaction logic in a mediator
  • Increases the reusability of colleagues by decoupling them

Problem: In a complex application, many small objects must interact. It can feel like a plate of spaghetti—hard to maintain, change, or understand.

Example: In VS Code, a button, help text, context menu, and mouse position are separate objects, but they must know each other to display the correct behavior.

Implementation of the Mediator pattern

UML structure:

AbsMediator (ABC)
    └── méthodes d'interaction [abstraites]

AbsColleague (ABC)
    └── médiateur (référence)

Chat, Dog, Fish (ConcreteColleagues)
    └── utilisent le médiateur au lieu de références directes

PetMediator (ConcreteMediator)
    ├── références aux pets
    └── logique d'interaction centralisée

Colleague abstract base class:

# abs_pet.py (exemple)
import abc

class AbsPet(abc.ABC):
    def __init__(self, name):
        self.name = name
        self.mediator = None  # Sera défini par le médiateur

Chat using mediator:

# cat.py (exemple)
import random
from abs_pet import AbsPet

class Cat(AbsPet):
    def __init__(self, name):
        super().__init__(name)
        self._asleep = False

    @property
    def is_asleep(self):
        # Simuler un résultat aléatoire
        self._asleep = random.random() > 0.5
        return self._asleep

    def wants_out(self):
        # Utilise le médiateur au lieu d'une référence directe au poisson !
        if self.mediator.is_fish_alive():
            print(f'{self.name} veut entrer (le poisson est en vie)')
        else:
            print(f'{self.name} ne veut pas rentrer (le poisson est mort)')

The mediator:

# pet_mediator.py (exemple)
class PetMediator:
    def __init__(self, cat, dog, fish):
        self._cat = cat
        self._dog = dog
        self._fish = fish

        # Connecter les pets au médiateur
        self._cat.mediator = self
        self._dog.mediator = self
        self._fish.mediator = self

    def is_cat_asleep(self):
        return self._cat.is_asleep

    def is_fish_alive(self):
        return self._fish.is_alive

    def wake_up_cat(self):
        self._cat.wake_up()

    def time_of_day(self, time_value):
        """Gère les actions selon le moment de la journée"""
        if time_value < 0:  # Matin
            print('\n=== Matin ===')
            self._fish.feed()
            self._dog.walk()
            self._cat.wants_out()
        elif time_value == 0:  # Midi
            print('\n=== Midi ===')
            self._cat.wants_in()
        else:  # Soir
            print('\n=== Soir ===')
            self._dog.walk()
            self._fish.check_alive()

Main program:

# __main__.py (exemple)
from cat import Cat
from dog import Dog
from fish import Fish
from pet_mediator import PetMediator

cat = Cat('Whiskers')
dog = Dog('Rex')
fish = Fish('Nemo')

mediator = PetMediator(cat, dog, fish)

for time_value in [-1, 0, 1]:  # Matin, midi, soir
    mediator.time_of_day(time_value)

Consequences of the Mediator pattern

Advantages:

  • Reduces the need for subclasses — distributed behavior is localized in the mediator
  • Increases reusability — colleagues are decoupled
  • Simplifies maintenance — centralized logic in the mediator
  • Colleague objects can change without worrying about others

Disadvantages:

  • If many colleagues, the mediator may become too complex
  • Risk of excessive centralization — can create a monolith that is difficult to maintain
  • Swaps interaction complexity for mediator complexity

Typical use: Complex GUI applications like VS Code where interactions between widgets, mouse and keyboard are centrally managed.


22. Behavioral Patterns: Memento

Introduction to the Memento pattern

The Memento Pattern (also called Token Pattern) is a behavioral pattern which:

  • Save the state of an object (checkpoints)
  • Allows you to restore the state to a previous point
  • Preserves encapsulation — neither Memento nor Caretaker need to know details
  • Simplifies the Originator — no longer responsible for maintaining saved states

Real life example: Video games (save/load), PowerPoint (autosave), Word (Ctrl+Z).

Implementation of the Memento pattern

UML structure:

Originator (le jeu)
    ├── create_memento() → Memento
    └── set_memento(Memento)

Memento
    ├── save_state(state)
    └── get_state()

Caretaker
    └── gère les Mementos (sans connaître le contenu)

The Memento class:

# memento.py (exemple)
import pickle

class Memento:
    """Stocke l'état de l'Originator — agnostique au contenu"""
    _state = None

    def save_state(self, state):
        self._state = pickle.dumps(state)  # Sérialisation

    def get_state(self):
        return pickle.loads(self._state)  # Désérialisation

The Game class (Originator):

# game.py (exemple)
from dataclasses import dataclass
from memento import Memento

@dataclass
class GameState:
    name: str
    level: int

class IHeart42:
    def __init__(self, name):
        self._game_state = GameState(name=name, level=1)

    def create_memento(self):
        """Créer un checkpoint"""
        memento = Memento()
        memento.save_state(self._game_state)
        return memento

    def set_memento(self, memento):
        """Restaurer un checkpoint"""
        self._game_state = memento.get_state()

    def display(self):
        print(f'Joueur: {self._game_state.name}, Niveau: {self._game_state.level}')

Main program:

# __main__.py (exemple)
from game import IHeart42

game = IHeart42('Arthur')

# État initial
print('État initial:')
game.display()

# Sauvegarder le checkpoint
checkpoint = game.create_memento()

# Changer l'état
game._game_state.name = 'Ford'
game._game_state.level = 5
print('\nÉtat modifié:')
game.display()

# Restaurer le checkpoint
game.set_memento(checkpoint)
print('\nÉtat restauré:')
game.display()

Advantage of Memento: If we now want to save several states or make the saves persistent (database), only the code of the Memento class changes — the game is not affected.

Memento module summary

Advantages:

  • Preserves encapsulation — neither Memento nor Caretaker knows the contents
  • Simplifies the Originator — no backup logic
  • Easy to implement state restoration

Potential disadvantages:

  • Cost: may be slow to create and restore
  • Memory: the Caretaker can be memory intensive
  • In Python, full encapsulation is difficult (introspection available)

23. Behavioral Patterns: Null

Introduction to the Null pattern

The Null Pattern is a behavioral pattern (often called a mini-pattern) which:

  • Provide default object to clients
  • Avoid null value tests (None) everywhere
  • Allows the client to use a returned object with complete confidence that it is valid

Common issue:

# Pattern typique à éviter
obj = factory.create_object('SomeClass')
if obj is not None:  # Test de None partout !
    obj.do_something()
else:
    print('Erreur: objet non trouvé')

Implementation of the Null pattern

# null_class.py (exemple)
from myabc import MyABC

class NullClass(MyABC):
    """Implémentation nulle — ne fait rien mais satisfait l'interface"""
    
    def do_something(self):
        print('NullClass: ne fait rien (comme prévu)')
# object_factory.py (exemple)
from myclass import MyClass
from null_class import NullClass

class ObjectFactory:
    @staticmethod
    def create_object(class_name):
        if class_name == 'MyClass':
            return MyClass()
        else:
            return NullClass()  # Au lieu de retourner None !
# __main__.py (exemple)
from object_factory import ObjectFactory

# Plus besoin de tester None !
obj = ObjectFactory.create_object('UnknownClass')
obj.do_something()  # Fonctionne toujours grâce au NullClass

obj2 = ObjectFactory.create_object('MyClass')
obj2.do_something()  # Fonctionne avec la vraie classe

Module Summary Null

  • Provides a default object that implements all required methods and properties
  • The implementation doesn’t need to do much — just imitate the actual object
  • Remove None tests in client code
  • Useful not only for classes, but also for functions, iterators and generators
  • The Strategy Pattern can use the Null Pattern
  • As soon as you see if obj is None: after a method call, consider implementing the Null Pattern

24. Behavioral Patterns: Template

Introduction to the Template pattern

The Template Pattern is a behavioral pattern that:

  • Defines the skeleton of an algorithm in an abstract base class
  • Delegate some steps to subclasses
  • Does not change the overall structure of the algorithm
  • Respects the DRY (Don’t Repeat Yourself) principle

Example: A bus trip and an airplane flight have a similar structure (start, leave terminal, travel, arrive) but with differences in the implementation of each step.

Implementation of the Template pattern

UML structure:

AbsTransport (ABC) — la classe Template
    ├── take_trip() [TEMPLATE METHOD — ordre fixe des étapes]
    ├── start_engine() [abstrait — doit être implémenté]
    ├── leave_terminal() [concret — peut être surchargé]
    ├── travel_to_destination() [abstrait — doit être implémenté]
    ├── entertainment() [hook — méthode vide, optionnellement surchargeable]
    └── arrive_at_destination() [concret — peut être surchargé]

Bus (implémente l'abstrait, utilise les méthodes concrètes)
Airplane (implémente l'abstrait, surcharge les méthodes concrètes)

Abstract base class:

# abs_transport.py (exemple)
import abc

class AbsTransport(abc.ABC):
    def __init__(self, destination):
        self._destination = destination

    def take_trip(self):
        """Méthode Template — définit l'ordre des étapes"""
        self.start_engine()
        self.leave_terminal()
        self.travel_to_destination()
        self.entertainment()  # Hook — optionnel
        self.arrive_at_destination()

    @abc.abstractmethod
    def start_engine(self):
        """Abstrait — doit être implémenté"""
        pass

    def leave_terminal(self):
        """Concret — peut être surchargé"""
        print('Départ du terminal')

    @abc.abstractmethod
    def travel_to_destination(self):
        """Abstrait — doit être implémenté"""
        pass

    def entertainment(self):
        """Hook — vide, peut être surchargé si nécessaire"""
        pass

    def arrive_at_destination(self):
        """Concret — peut être surchargé"""
        print(f'Arrivée à {self._destination}')

Airplane Class:

# airplane.py (exemple)
from abs_transport import AbsTransport

class Airplane(AbsTransport):
    def start_engine(self):
        print('Démarrage des turbines')  # Implémentation requise

    def leave_terminal(self):
        print('Embarquement des passagers, remorquage hors du gate')  # Surcharge

    def travel_to_destination(self):
        print(f'Vol vers {self._destination}')  # Implémentation requise

    def entertainment(self):
        print('Projection d\'un film en vol')  # Utilisation du hook !

    def arrive_at_destination(self):
        print(f'Atterrissage et débarquement à {self._destination}')  # Surcharge

Bus Class:

# bus.py (exemple)
from abs_transport import AbsTransport

class Bus(AbsTransport):
    def start_engine(self):
        print('Démarrage du moteur diesel')  # Implémentation requise

    def travel_to_destination(self):
        print(f'Route vers {self._destination}')  # Implémentation requise

    # Utilise les méthodes concrètes par défaut de la classe de base
    # Pas de divertissement (hook non implémenté)

Main program:

# __main__.py (exemple)
from airplane import Airplane
from bus import Bus

def travel(destination, transport_class):
    transport = transport_class(destination)
    transport.take_trip()

print('=== Vol vers Amsterdam ===')
travel('Amsterdam', Airplane)

print('\n=== Bus vers New York ===')
travel('New York', Bus)

Consequences of the Template pattern

Advantages:

  • Code reuse — invariant parts remain in the parent class
  • The Hollywood Approach: “Don’t call us, we’ll call you” — subclass methods are called at runtime
  • Very flexible:
  • Abstract methods (required)
  • Invariant concrete methods in parent class
  • Concrete methods overloaded in subclasses
  • Hooks (empty methods, optionally implemented)
  • Can use Factory Pattern to simplify creation
  • Can use Strategy Pattern to override parts of the algorithm

25. Behavioral Patterns: Iterator

Introduction to the Iterator pattern

The Iterator Pattern is a behavioral pattern (also called Cursor Pattern) which:

  • Iterates over elements of a collection without exposing the underlying representation
  • Preserve encapsulation of the collection
  • Provides a uniform interface for all collection types

Python usage: for loops, list comprehensions, and generators all use this pattern.

Implementation with __iter__ and __next__

# employee_collection.py (exemple)
from collections.abc import Iterator

class Employees(Iterator):
    def __init__(self):
        self._employees = {}  # Dictionnaire : clé = numéro, valeur = employé
        self._empid = 0

    def add_employee(self, employee):
        self._empid += 1
        self._employees[self._empid] = employee

    @property
    def headcount(self):
        return len(self._employees)

    def __iter__(self):
        self._empid = 0  # Réinitialiser pour la nouvelle itération
        return self

    def __next__(self):
        if self._empid < self.headcount:
            self._empid += 1
            return self._employees[self._empid]
        raise StopIteration  # Python convention
# department_collection.py (exemple avec Sequence)
from collections.abc import Sequence

class Departments(Sequence):
    def __init__(self):
        self._departments = []

    def add_department(self, dept):
        self._departments.append(dept)

    def __getitem__(self, item):
        return self._departments[item]

    def __len__(self):
        return len(self._departments)

Main program:

# __main__.py (exemple)
from employee_collection import Employees
from department_collection import Departments
from test_data import TESTEMPLOYEES, TESTDEPARTMENTS

def print_summary(items):
    for item in items:  # Boucle polymorphique !
        print(f'  {item.name}')

print('Employés:')
print_summary(TESTEMPLOYEES)

print('\nDépartements:')
print_summary(TESTDEPARTMENTS)

Implementation with generators

# employee_collection_gen.py (exemple avec générateurs)
class Employees:
    def __init__(self):
        self._employees = {}
        self._next_id = 1

    def add_employee(self, employee):
        self._employees[self._next_id] = employee
        self._next_id += 1

    def __iter__(self):
        return (emp for emp in self._employees.values())  # Expression générateur !
# department_collection_gen.py (exemple)
class Departments:
    def __init__(self):
        self._departments = []

    def add_department(self, dept):
        self._departments.append(dept)

    def __iter__(self):
        return (dept for dept in self._departments)  # Encore plus simple !

Advantage: Generating expressions are lazily evaluated — the next element is only calculated when requested. Ideal for large collections or infinite iterables.

Iterator module summary

Significant consequences:

  1. Standard interface for processing the members of a collection, simple or complex
  2. Any program can use for, list comprehension or generator
  3. Multiple active iterations simultaneously (generators allow this easily)
  4. Preserves encapsulation — collection can change without affecting clients

When to use:

  • To provide a way to iterate over a collection (from list to nested hierarchy)
  • To support multiple active iterators
  • To provide a uniform interface allowing polymorphic iteration

26. Behavioral Patterns: Interpreting

Introduction to the Interpreter pattern

The Interpreter Pattern is a behavioral pattern that:

  • Interpret sentences in DSL (Domain-Specific Language)
  • Not human language, but a computer language targeting a particular type of problem

Domain Specific Languages ​​(DSL)

DSLs are everywhere in development:

DSLDescription
SQLQueries on relational databases
CSSFormatting web pages
HTMLStructure of web pages
JSONData exchange in key-value pairs
RegExRegular expressions
XMLStructured data
YAMLReadable data serialization
Make/CronTask automation
Python Format Mini LanguageFormatting strings

Backus Normal Form (BNF)

Most computer languages ​​are defined with a formal grammar in BNF (Backus Normal Form). Python uses BNF in its documentation.

Example: definition of the Python Format Specification Mini Language:

format_spec     ::=  [[fill]align][sign][z][#][0][width][grouping_option][.precision][type]
fill            ::=  <any character>
align           ::=  "<" | ">" | "=" | "^"
sign            ::=  "+" | "-" | " "
width           ::=  digit+
grouping_option ::=  "_" | ","
precision       ::=  digit+
type            ::=  "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" | "n" | "o" | "s" | "x" | "X" | "%"

Reading the BNF:

  • ::= means “is defined as”
  • [...] means optional
  • | means alternation (or)
  • + means one or more times

Implementation of the Interpreter pattern

Grammar for MyDSL (making scrambled eggs):

expression ::= command | sequence | repetition
sequence   ::= expression ';' expression
command    ::= 'break egg' | 'mix in bowl' | 'melt butter in pan' | 'cook eggs' | set
repetition ::= 'while' variable expression
set        ::= 'set' variable 'to' ('true'|'false')
variable   ::= [A-Z][A-Z0-9]*

Example expression:

break egg; break egg; mix in bowl; melt butter in pan;
set NOTCOOKED to true;
while NOTCOOKED cook eggs;
set NOTCOOKED to false

UML structure:

Client → Context (AST)
              ├── Expression (NonTerminal)
              │       ├── Sequence (NonTerminal)
              │       └── Repetition (NonTerminal)
              └── BreakEgg (Terminal)
                  MixEggs (Terminal)
                  MeltButter (Terminal)
                  CookEggs (Terminal)

Abstract base class:

# abs_expression.py (exemple)
import abc

class AbsExpression(abc.ABC):
    @abc.abstractmethod
    def interpret(self, context):
        pass

Context:

# context.py (exemple)
class Context:
    def __init__(self):
        self.variables = {}  # Pour les variables de l'expression 'set'

Terminal expressions (leaves):

# ast.py (exemple)
from abs_expression import AbsExpression

class BreakEgg(AbsExpression):
    def interpret(self, context):
        print('Casser un œuf dans le bol')

class MixEggs(AbsExpression):
    def interpret(self, context):
        print('Mélanger les œufs dans le bol')

class MeltButter(AbsExpression):
    def interpret(self, context):
        print('Faire fondre le beurre dans la poêle')

class CookEggs(AbsExpression):
    def interpret(self, context):
        print('Cuire les œufs dans la poêle')

class SetVariable(AbsExpression):
    def __init__(self, variable, value):
        self._variable = variable
        self._value = value

    def interpret(self, context):
        context.variables[self._variable] = self._value

Non-terminal expressions:

# ast.py (suite — exemples)
class Expression(AbsExpression):
    """Expression de niveau supérieur — non-terminal"""
    def __init__(self, expression):
        self._expression = expression

    def interpret(self, context):
        self._expression.interpret(context)

class Sequence(AbsExpression):
    """Séquence de deux expressions"""
    def __init__(self, expr1, expr2):
        self._expr1 = expr1
        self._expr2 = expr2

    def interpret(self, context):
        self._expr1.interpret(context)
        self._expr2.interpret(context)

class Repetition(AbsExpression):
    """Boucle while"""
    def __init__(self, variable, expression):
        self._variable = variable
        self._expression = expression

    def interpret(self, context):
        while context.variables.get(self._variable, False):
            self._expression.interpret(context)

Customer program:

# __main__.py (exemple)
from ast import *
from context import Context

context = Context()

# Construire l'AST pour : break egg; break egg; mix in bowl; ...
# set NOTCOOKED to true; while NOTCOOKED cook eggs; set NOTCOOKED to false
program = Sequence(
    Sequence(
        Sequence(BreakEgg(), BreakEgg()),
        Sequence(MixEggs(), MeltButter())
    ),
    Sequence(
        SetVariable('NOTCOOKED', True),
        Sequence(
            Repetition('NOTCOOKED',
                Sequence(CookEggs(), SetVariable('NOTCOOKED', False))
            ),
            SetVariable('NOTCOOKED', False)
        )
    )
)

print('=== Recette des œufs brouillés ===')
program.interpret(context)

Interpreter module summary

Advantages:

  • Easy to extend and change grammar — expressions are classes
  • Simple to implement — AST nodes are often similar
  • Easy to change how expressions are interpreted
  • Additional patterns:
  • Composite — for ASTs
  • Flyweight — to share symbols in an AST
  • Visitor — to implement the behavior in the interpreter
  • Iterator — to iterate through the AST structure

Disadvantages:

  • Complex grammars can be difficult to maintain
  • For very complex grammars, consider a parser generator rather than the Interpreter pattern

27. Course Summary

Congratulations!

You’ve covered 24 design patterns — all the classics from Abstract Factory to Visitor, all in Python.

Kudos to Gang of Four

Without the Gang of Four’s reference work, this course would not exist. They classify design patterns into three types:

TypeRole
CreativeHelp when building items
StructuralHelp when combining patterns
BehavioralHelp organize things at runtime

Reminder of SOLID principles

PrincipleDescription
S — Single ResponsibilityAn object should only be responsible for one thing
O — Open/ClosedClasses should be open to extension but closed to modification. Helps avoid version dependencies and 3am emergency calls
L — Liskov SubstitutionSubclasses should be able to override their parent class without breaking anything
I — Segregation InterfaceSpecific interfaces are better than a catch-all interface
D — Dependency InversionProgram towards abstractions, not implementations. Implementations may vary, abstractions should not

Don’t Repeat Yourself (DRY)

Even if it doesn’t fit into the SOLID acronym, the DRY principle is essential. The natural progression:

  1. Try writing similar code? → Don’t code it, copy it.
  2. Copying is not ideal → Don’t copy it, link to it. In Python: include other modules in your build.
  3. To avoid versioning issues → Don’t link it, load it at runtime. Load a module from a central source managed by a team.

DRY is not limited to OOP — apply it regardless of language, paradigm or style.

Python Abstract Base Classes

ABCs correspond to interface definitions in other languages:

  • Abstract methods — must be implemented in concrete classes
  • Concrete methods — can help keep code DRY
  • Helps comply with SOLID’s Dependency Inversion and Interface Segregation
  • Help to focus on what belongs to a concrete class (Single Responsibility)

Other design pattern categories

This course covers the classic Gang of Four patterns, but there are others:

CategoryDescription
Asynchronous PatternsReduce wait times by doing as many things asynchronously as possible
Parallel Processing PatternsFor CPU-intensive programs (numerical analysis, AI inference)
Functional patternsAnother way to build programs with their own mathematics

Summary of the 24 patterns

Creational Patterns (5):

PatternRole
FactoryInterface for creating objects, letting subclasses decide what to build
Abstract FactoryCreates families of related objects without specifying their concrete classes
BuilderSeparates the construction of a complex object from its representation
PrototypeCreates new objects by cloning an existing object
SingletonGuarantees that a class has only one instance

Structural Patterns (7):

PatternRole
AdaptConverts the interface of one class to another expected by clients
BridgeDecouples an abstraction from its implementation
CompositeManages part-whole hierarchies with a uniform interface
DecoratorAdds new responsibilities to an object dynamically
FacadeSimplified interface for a complex subsystem
FlyweightUses sharing to efficiently manage large numbers of objects
ProxyProvides a substitute or representative for another object

Behavioral Patterns (12):

PatternRole
StrategyEncapsulates a family of interchangeable algorithms
CommandWraps a query as an object
StateAllows an object to change behavior depending on its state
ObserveOne-to-many notification on state change
VisitorAdds new operations to an object structure without modifying it
Chain of ResponsibilityDecouples request senders and handlers
MediatorObject that encapsulates interactions between objects
MementoCaptures and restores the internal state of an object
NullDefault object avoiding null tests
TemplateAlgorithm skeleton with steps delegated to subclasses
IteratorAccesses items in a collection sequentially
InterpretInterpret sentences in domain-specific language

28. References

  • “Design Patterns: Elements of Reusable Object-Oriented Software” — Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Gang of Four), 1995
  • Python documentationabc module, copy module, functools module, collections.abc
  • NumPy — High-performance numerical computing for Python


Search Terms

design · patterns · python · foundations · data · analysis · engineering · analytics · pattern · behavioral · motivating · factory · abstract · singleton · structural · command · composite · creational · visitor · adapter · approach · bridge · builder · chain

Interested in this course?

Contact us to book it or get a custom training plan for your team.