Python version: 3.9.2+ (applicable to Python 3.x) IDE used: Visual Studio Code with Python extension
Table of Contents
- What is a design pattern?
- Real-world examples
- Classification of design patterns
- The SOLID principles
- Necessary tools
- Create interfaces in Python — Abstract Base Classes
- Introduction to Factory pattern
- Motivating example: brute force approach
- Simple Factory Pattern
- Full Factory Pattern (Gang of Four)
- Factory module summary
- Introduction to the Abstract Factory pattern
- Motivating example
- Abstract Factory pattern structure
- Implementation
- Abstract
- Introduction to Pattern Builder
- Motivating example: the parameter list problem
- Pattern Builder implementation
- Builder module summary
- Introduction to the Prototype pattern
- Shallow Cloning
- Deep Cloning
- Prototype Manager
- Prototype module summary
- Introduction to Singleton pattern
- Classic Singleton
- Singleton Problems
- Singleton with base class
- Singleton with Metaclass
- MonoState
- Singleton module summary
- Introduction to the Adapter pattern
- Motivating example
- Object Adapter
- Class Adapter
- Object vs Class Adapter Comparison
- Introduction to the Bridge pattern
- The problem of exponential growth
- Bridge pattern implementation
- Bridge module summary
- Introduction to the Composite pattern
- Motivating example: family trees
- Composite pattern structure
- Composite pattern implementation
- Composite module summary
- Introduction to Decorator pattern
- Naive approach by subclasses
- Approach by properties
- Decorator pattern implementation
- Differences with Python decorators
- Decorator module summary
- Introduction to the Façade pattern
- Motivating example: database access
- Facade pattern implementation
- Facade module summary
- Introduction to the Flyweight pattern
- Naive approach: the LHC
- Flyweight pattern implementation
- Flyweight module summary
- Introduction to the Proxy pattern
- Proxies types
- Motivating example: employee access control
- Implementation of the Proxy pattern
- Proxy module summary
- Introduction to the Strategy pattern
- Motivating example: calculation of delivery costs
- Strategy pattern implementation
- Variations: functions and lambdas
- Strategy module summary
- Introduction to Pattern Command
- Inspiring example: CLI command processing
- Implementation of the Command pattern
- Undo / Redo
- Command module summary
- Introduction to pattern State
- Motivating example: shopping basket
- Implementation of the State pattern
- State module summary
- Introduction to the Observer pattern
- Motivating example: KPI dashboard
- Observer pattern implementation
- Bug fix: dangling reference
- Observer module summary
- Introduction to the Visitor pattern
- Visitor pattern structure
- Implementation of the Visitor pattern
- Summary and consequences of the Visitor pattern
- Introduction to the Chain of Responsibility pattern
- Linked-chain implementation
- Implementation with a list
- Chain of Responsibility module summary
- Introduction to the Mediator pattern
- Mediator pattern implementation
- Consequences of the Mediator pattern
- Introduction to the pattern Template
- Template pattern implementation
- Consequences of the Pattern Template
- Introduction to the Iterator pattern
- Implementation with
__iter__and__next__ - Implementation with generators
- Iterator module summary
- Introduction to the Interpreter pattern
- Domain-specific languages (DSL)
- Backus Normal Form (BNF)
- Implementation of the Interpreter pattern
- Interpreter module 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:
| Category | Description | Examples |
|---|---|---|
| Creative | Concern the creation of objects | Factory, Builder, Singleton |
| Structural | Define relationships between objects | Adapt, Facade, Composite |
| Behavioral | Manage inter-object communication | Command, 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:
| Letter | Principle | Description |
|---|---|---|
| S | Single Responsibility | A class should only have one responsibility |
| O | Open/Closed | A class must be open to extension, but closed to modification |
| L | Liskov Substitution | Subclasses must be able to override their parent class without breaking the program |
| I | Segregation interface | Several specific interfaces are better than one general interface |
| D | Dependency Inversion | Program 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
abcmodule and inherit fromabc.ABC - Use
@abc.abstractmethodto declare abstract methods - Use
@abc.abstractpropertyfor 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/elsestructure 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
NullCarhere — 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
inspectmodule for introspection - Importing the
autospackage executes theimportstatements in the__init__module, adding the classes to theAutoFactorynamespace - Dictionary maps model name to corresponding class
- If no match is found,
NullCaris 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_factoryinstead
Factory module summary
The Factory Pattern brings several advantages:
- Encapsulates object instantiation — no need to instantiate classes directly
- Supports the Dependency Inversion Principle — clients no longer depend on implementations
- 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
ifstructure.
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
@staticmethodbecause 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
MainBoardobject, 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:
- Shallow cloning — copies references of nested objects
- Deep cloning — recursively copies nested objects
- 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:
- Single Responsibility Principle violation — it does two things: manages its own instantiation AND maintains/processes state
- Non-standard access to the class — requires knowing how to use the
instance()method instead of the normal constructor - More difficult to test — strong coupling with objects that use it, difficult to replace with fakes/mocks for unit tests
- Carries a global state — similar to globals in terms of maintenance and testing issues
- 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:
- Classic singleton with
instance()method - Singleton base class (uses
__new__) - Metaclass Singleton
- 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:addressproperty (combined street and number)Vendor: separatenumberandstreetproperties
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
| Criterion | ObjectAdapter | ClassAdapter |
|---|---|---|
| Mechanism | Composition | Multiple inheritance |
| Flexibility | Very flexible, works with all subclasses | Related to a specific subclass |
| Delegation | Delegates calls to the adaptede | Overloading adapted methods |
| Subclasses | Works with all subclasses of the adapted | Related to a specific subclass |
| Added behavior | Requires changes if subclass adds behavior | Automatically 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/yearMonthlySubscription: $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:
- Exponential growth of classes
- Code duplication (DRY violation)
- 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
costmethod should have its own abstraction- D: Concrete classes depend on the implementation of
costin 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
| Appearance | Decorator Pattern (GoF) | Python decorators |
|---|---|---|
| Syntax | Classes inheriting an ABC | def or callable classes, @ syntax |
| What is wrapped | Class instances | Function/method/class definitions |
| Moment of Action | Execution (runtime) | Compile time expansion |
| Added functionality | To class instances | To functions, methods and classes |
| Focus | Narrow, specific | General purpose |
| Reference | Gang of Four | PEP 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:
- Ask the DBA which base to use
- Get the right Python modules
- Instantiate a control object
- Construct the connection string
- Connect to the database
- Run query
- Process the results
- 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
| Type | Description | Example |
|---|---|---|
| Remote Proxy | Local representative of an object in a different address space | Web client accessing a database behind a firewall |
| Virtual Proxy | Creates expensive items on demand (lazy loading) | Return data from a long query only when necessary |
| Proxy Protection | Controls access according to access rights | Restrict access to employees’ personal information |
| Smart Reference Proxy | Additional actions during access | Reference 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_cacheoffunctoolsis 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/elselist 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:
- Fixes SOLID violations of naive approach
- Each algorithm is separate — easy to test in isolation
- Easy to test external code with deterministic mocks/fakes
- Eliminate
if/elif/elsestructures — a potential refactoring signal - 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:
CommandExecutorparses 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/elsestrings 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:
- Encapsulates state-specific behaviors — behavior varies dramatically between EMPTY and AT_CHECKOUT
- Distributes behavior over state classes — less compact solution but eliminates long conditionals
- Makes adding new states easier — most of the work is in the new state
- Explicit state transitions without complex code in context
- Possibility of sharing states (Flyweight pattern) if no instance variables
- 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
updatemethod - 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:
- Easy to add new operations — nothing is changed in existing classes
- Separation of concerns — functional logic is separated from the model
- Works on different class hierarchies (if they implement
accept) - Can accumulate state during visit (see
GetOldestVisitor)
Disadvantages:
- Breaks encapsulation — visitor has access to internal details
- Hard to change data model — each new concrete type requires a new method in all visitor classes
- 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:
- Linked chain — conforms to GoF description
- 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
Nonetests 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:
- Standard interface for processing the members of a collection, simple or complex
- Any program can use
for, list comprehension or generator - Multiple active iterations simultaneously (generators allow this easily)
- 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:
| DSL | Description |
|---|---|
| SQL | Queries on relational databases |
| CSS | Formatting web pages |
| HTML | Structure of web pages |
| JSON | Data exchange in key-value pairs |
| RegEx | Regular expressions |
| XML | Structured data |
| YAML | Readable data serialization |
| Make/Cron | Task automation |
| Python Format Mini Language | Formatting 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:
| Type | Role |
|---|---|
| Creative | Help when building items |
| Structural | Help when combining patterns |
| Behavioral | Help organize things at runtime |
Reminder of SOLID principles
| Principle | Description |
|---|---|
| S — Single Responsibility | An object should only be responsible for one thing |
| O — Open/Closed | Classes should be open to extension but closed to modification. Helps avoid version dependencies and 3am emergency calls |
| L — Liskov Substitution | Subclasses should be able to override their parent class without breaking anything |
| I — Segregation Interface | Specific interfaces are better than a catch-all interface |
| D — Dependency Inversion | Program 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:
- Try writing similar code? → Don’t code it, copy it.
- Copying is not ideal → Don’t copy it, link to it. In Python: include other modules in your build.
- 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:
| Category | Description |
|---|---|
| Asynchronous Patterns | Reduce wait times by doing as many things asynchronously as possible |
| Parallel Processing Patterns | For CPU-intensive programs (numerical analysis, AI inference) |
| Functional patterns | Another way to build programs with their own mathematics |
Summary of the 24 patterns
Creational Patterns (5):
| Pattern | Role |
|---|---|
| Factory | Interface for creating objects, letting subclasses decide what to build |
| Abstract Factory | Creates families of related objects without specifying their concrete classes |
| Builder | Separates the construction of a complex object from its representation |
| Prototype | Creates new objects by cloning an existing object |
| Singleton | Guarantees that a class has only one instance |
Structural Patterns (7):
| Pattern | Role |
|---|---|
| Adapt | Converts the interface of one class to another expected by clients |
| Bridge | Decouples an abstraction from its implementation |
| Composite | Manages part-whole hierarchies with a uniform interface |
| Decorator | Adds new responsibilities to an object dynamically |
| Facade | Simplified interface for a complex subsystem |
| Flyweight | Uses sharing to efficiently manage large numbers of objects |
| Proxy | Provides a substitute or representative for another object |
Behavioral Patterns (12):
| Pattern | Role |
|---|---|
| Strategy | Encapsulates a family of interchangeable algorithms |
| Command | Wraps a query as an object |
| State | Allows an object to change behavior depending on its state |
| Observe | One-to-many notification on state change |
| Visitor | Adds new operations to an object structure without modifying it |
| Chain of Responsibility | Decouples request senders and handlers |
| Mediator | Object that encapsulates interactions between objects |
| Memento | Captures and restores the internal state of an object |
| Null | Default object avoiding null tests |
| Template | Algorithm skeleton with steps delegated to subclasses |
| Iterator | Accesses items in a collection sequentially |
| Interpret | Interpret 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 documentation —
abcmodule,copymodule,functoolsmodule,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