chemical-and-materials-engineering
Modèles de conception en génie Python: renforcer la réutilisabilité du code
Table of Contents
Les modèles de conception représentent des solutions éprouvées dans le temps pour relever des défis récurrents dans le développement logiciel. En ingénierie Python, ces solutions réutilisables aident les développeurs à créer un code plus durable, flexible et évolutif. Comprendre et mettre en œuvre des modèles de conception peut transformer efficacement la façon dont vous approchez l'architecture logiciel, vous permettant d'écrire un code qui est non seulement fonctionnel, mais aussi élégant, réutilisable et plus facile à maintenir au fil du temps.
Les modèles de conception servent de vocabulaire qui permet aux ingénieurs de communiquer les décisions structurelles de façon concise. Lorsqu'un développeur senior suggère d'utiliser un modèle spécifique, ils transmettent une approche architecturale entière en quelques mots. Ce langage partagé accélère la collaboration d'équipe et assure à chacun de comprendre la structure sous-jacente de la base de code.
Comprendre les modèles de conception dans le contexte Python
Python est un langage de programmation de haut niveau avec une typographie dynamique et une liaison dynamique, ce qui en fait un langage dynamique puissant et de haut niveau. Cette flexibilité donne des avantages uniques à Python lors de la mise en œuvre de modèles de conception, mais cela signifie également que certains modèles doivent être adaptés aux idiomes et capacités de Python.
Tout en Python est un objet, y compris les fonctions, qui sont des objets de première classe. Cette caractéristique fondamentale influence la façon dont les modèles de conception sont mis en œuvre en Python par rapport à des langages plus rigides et statiquement typés. La nature dynamique de Python permet des implémentations plus concises de nombreux modèles, tout en introduisant le besoin de pratiques de codage disciplinées.
Parce que Python est si puissant et flexible, nous avons besoin de certaines règles ou modèles pour la programmation. Sans ces principes directeurs, les bases de code peuvent rapidement devenir incompréhensibles et difficiles à maintenir. Les modèles de conception fournissent la structure nécessaire pour exploiter le vaste potentiel de Python tout en maintenant la qualité et la lisibilité du code.
Les trois catégories de modèles de conception
Les modèles de conception sont traditionnellement organisés en trois grandes catégories, chacune traitant de différents aspects de la conception de logiciels. Comprendre ces catégories aide les développeurs à choisir le modèle approprié pour leurs défis spécifiques.
Modèles de création
Les modèles de création se concentrent sur le processus de création d'objets, fournissant des mécanismes qui augmentent la flexibilité et la réutilisation du code existant. Ces modèles résument le processus d'inocnération, rendant les systèmes indépendants de la façon dont les objets sont créés, composés et représentés.
Au lieu de créer des objets directement en utilisant un constructeur, les modèles de création offrent plus de contrôle et de flexibilité sur le processus de création. Cette approche est particulièrement utile lorsque la logique de création est complexe, lorsque vous devez contrôler quelle classe est inactive, ou lorsque vous voulez gérer l'allocation des ressources plus efficacement.
Les modèles de création courants en Python comprennent Singleton, Factory Method, Abstract Factory, Builder et Prototype. Chacun sert un but distinct dans la gestion de la complexité de la création d'objets.
Modèles structurels
Les modèles de conception structurelle se concentrent sur la composition de classes ou d'objets pour former des structures plus grandes et plus complexes, aidant à organiser et gérer les relations entre les objets pour obtenir une plus grande flexibilité, réutilisabilité et maintien.Ces modèles sont concernés par la façon dont les classes et les objets sont composés pour former des structures plus grandes tout en maintenant ces structures flexibles et efficaces.
Les structures permettent de s'assurer que toute modification d'une partie d'un système n'est pas nécessaire. Elles facilitent la conception de systèmes où les composants peuvent être facilement remplacés ou agrandis sans affecter d'autres parties de l'application. Les structures communes comprennent l'adaptateur, le pont, le composite, le décorateur, la façade, le poids-fly et le proxy.
Modèles comportementaux
Les modèles comportementaux sont concernés par les algorithmes et l'attribution des responsabilités entre les objets. Ils décrivent non seulement les modèles d'objets ou de classes, mais aussi les modèles de communication entre eux. Ces modèles caractérisent le flux de contrôle complexe qui est difficile à suivre au moment de l'exécution.
L'observateur adresse la communication, permettant à plusieurs parties d'un système de réagir automatiquement aux événements ou aux changements d'état. D'autres modèles comportementaux comprennent Stratégie, Commande, Itérateur, Médiateur, Memento, État, Méthode de modèle, Visiteur, et Chaîne de responsabilité.
Le modèle Singleton: une seule instance pour les gouverner
Le modèle Singleton garantit qu'une classe n'a qu'une seule instance dans un programme et fournit un point d'accès global, couramment utilisé pour gérer des ressources partagées comme les bases de données, les systèmes de journalisation ou les gestionnaires de fichiers.
Quand utiliser Singleton
Le modèle Singleton est particulièrement utile lorsque vous avez besoin d'une instance de classe pour contrôler des ressources telles que les connexions de base de données, les gestionnaires de configuration ou les systèmes de log. Le modèle garantit que tout le code utilisant l'instance de classe fonctionne avec le même objet, assurant la cohérence dans l'application.
Les cas d'utilisation légitime pour les singletons en Python comprennent :
- Interfaces matérielles qui représentent des ressources physiques uniques, comme une caméra, une imprimante ou une interface GPIO, où un monotone modèle avec précision
- Cache les calques où vous voulez un seul cache partagé dans votre application
- Les piscines de fil ou les piscines de connexion où vous voulez limiter et partager des ressources coûteuses, avec le pool lui-même étant un simpleton bien que les ressources qu'il gère ne sont pas
- Systèmes de gestion de la configuration qui doivent maintenir des paramètres cohérents tout au long de l'application
- Systèmes d'exploitation forestière où la gestion centralisée des journaux est essentielle
Mise en œuvre de Singleton en Python
La classe Singleton peut être mise en œuvre de différentes façons en Python, y compris la classe de base, le décorateur et les approches métaclass, la métaclass étant la meilleure à cette fin. Chaque méthode d'implémentation a ses propres avantages et compromis.
La mise en œuvre classique utilisant la méthode new ressemble à ceci:
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(DatabaseConnection, cls).__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
self.connection = None
self.host = "localhost"
self.port = 5432
def connect(self):
if not self.connection:
self.connection = f"Connected to {self.host}:{self.port}"
return self.connection
Une implémentation sans fil utilise un objet de verrouillage qui synchronise les fils lors du premier accès au Singleton. Ceci est crucial dans les applications multifilaires où les conditions de course pourraient conduire à plusieurs instances en cours de création:
from threading import Lock
class ThreadSafeSingleton:
_instance = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
L'alternative pytonique : monotones de niveau module
Le système de module de Python est lui-même un mécanisme de monotone : lorsque vous importez un module, Python l'exécute une fois et cache le résultat dans sys.modules, chaque importation ultérieure renvoyant l'objet de module mis en cache, pas un nouveau. Cela fait des modules une manière naturelle et pythonique d'implémenter le comportement de monotone.
Python importe un module une seule fois, ce qui fait de tout ce qui est défini dans un module un simpleton. Cette approche est souvent plus simple et plus durable que la mise en œuvre d'une classe standard Singleton:
# config.py
class _Config:
def __init__(self):
self.database_url = "postgresql://localhost/mydb"
self.api_key = "secret_key"
self.debug_mode = True
def update_setting(self, key, value):
setattr(self, key, value)
# Create the singleton instance
config = _Config()
# In other files, simply import:
# from config import config
Le modèle de simpleton n'a généralement pas de sens dans Python sous sa forme la plus pure; il est plutôt plus logique de faire une seule instance d'une classe et d'attribuer cette instance à une variable globale dans un module. Cette approche est plus transparente, plus facile à tester et s'aligne mieux sur la philosophie de Python.
Inconvénients et considérations concernant le monotone
De nombreux développeurs considèrent le modèle Singleton comme un antipattern, c'est pourquoi son utilisation est en déclin dans le code Python. Plusieurs préoccupations légitimes existent:
- En raison de son état global et de son couplage serré avec d'autres parties de la base de code, le modèle Singleton peut rendre difficile l'essai unitaire, en se moquant ou en remplaçant l'instance Singleton par une instance encombrante
- Dans les environnements multithreadés, le modèle Singleton peut introduire des problèmes de concordance si elle n'est pas mise en œuvre avec soin, avec plusieurs threads pouvant créer plusieurs instances sans synchronisation appropriée
- Les Singletons créent des dépendances cachées qui rendent le code plus difficile à comprendre et à maintenir
- Ils violent le principe de responsabilité unique en gérant à la fois leur logique d'entreprise et leur invocation
- Les Singletons rendent difficile l'extension ou la modification du comportement par héritage
Envisagez plutôt d'utiliser l'injection de dépendance, car elle pourrait être plus propre, ou utiliser une instance de niveau module. Ces alternatives offrent souvent les mêmes avantages sans les inconvénients.
Motif d'usine: Création d'objets flexibles
Le modèle Factory fournit une interface pour créer des objets sans spécifier leurs classes exactes, favorisant le couplage lâche et rendant le code plus flexible aux changements. Ce modèle est inestimable lorsque vous avez besoin de créer des objets mais que vous voulez découpler la logique de création du code qui utilise ces objets.
Usine se concentre sur la façon dont les objets sont créés, cachant la logique d'instantiation et réduisant le couplage étroit entre les composants. En centralisant la création d'objets, le modèle Factory facilite l'introduction de nouveaux types sans modifier le code existant.
Mise en œuvre simple de l'usine
Le modèle Simple Factory encapsule la création d'objets dans une fonction ou une classe dédiée. Voici un exemple pratique pour créer différents types de connexions de base de données:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def query(self, sql):
pass
class PostgreSQLDatabase(Database):
def connect(self):
return "Connected to PostgreSQL"
def query(self, sql):
return f"PostgreSQL executing: {sql}"
class MySQLDatabase(Database):
def connect(self):
return "Connected to MySQL"
def query(self, sql):
return f"MySQL executing: {sql}"
class MongoDBDatabase(Database):
def connect(self):
return "Connected to MongoDB"
def query(self, sql):
return f"MongoDB executing: {sql}"
class DatabaseFactory:
@staticmethod
def create_database(db_type):
databases = {
'postgresql': PostgreSQLDatabase,
'mysql': MySQLDatabase,
'mongodb': MongoDBDatabase
}
db_class = databases.get(db_type.lower())
if not db_class:
raise ValueError(f"Unknown database type: {db_type}")
return db_class()
# Usage
db = DatabaseFactory.create_database('postgresql')
print(db.connect())
print(db.query("SELECT * FROM users"))
Modèle de méthode d'usine
La méthode Factory fournit une interface pour créer des objets dans une superclasse, mais permet aux sous-classes de modifier le type d'objets qui seront créés. Cette variation donne encore plus de flexibilité en déléguant l'invocation aux sous-classes:
from abc import ABC, abstractmethod
class DocumentProcessor(ABC):
@abstractmethod
def create_parser(self):
"""Factory method to be implemented by subclasses"""
pass
def process_document(self, content):
parser = self.create_parser()
return parser.parse(content)
class Parser(ABC):
@abstractmethod
def parse(self, content):
pass
class JSONParser(Parser):
def parse(self, content):
return f"Parsing JSON: {content}"
class XMLParser(Parser):
def parse(self, content):
return f"Parsing XML: {content}"
class CSVParser(Parser):
def parse(self, content):
return f"Parsing CSV: {content}"
class JSONDocumentProcessor(DocumentProcessor):
def create_parser(self):
return JSONParser()
class XMLDocumentProcessor(DocumentProcessor):
def create_parser(self):
return XMLParser()
# Usage
json_processor = JSONDocumentProcessor()
result = json_processor.process_document('{"name": "John"}')
print(result)
Avantages du modèle d'usine
Le modèle Factory offre plusieurs avantages convaincants :
- Loose Coupling: Le code client n'a pas besoin de connaître les classes de béton en cours d'invocation
- Responsabilité unique[: La logique de création d'objets est centralisée en un seul endroit
- Principe ouvert/fermé: De nouveaux types peuvent être ajoutés sans modifier le code existant
- Flexibilité: Facile à basculer entre différentes implémentations au moment de l'exécution
- Testabilité: Les objets de choc peuvent être facilement injectés pour les essais
Motif d'observateur : Architecture animée par l'événement
Le modèle Observer définit un mécanisme d'abonnement pour notifier plusieurs objets sur tout événement qui se produit à l'objet qu'ils observent. Ce modèle est fondamental pour la programmation axée sur les événements et est largement utilisé dans les cadres de GUI, les systèmes en temps réel et les applications réactives.
Comprendre le modèle d'observateur
Le modèle Observateur établit une dépendance entre les objets. Lorsque le sujet (observable) change d'état, toutes ses personnes à charge (observateurs) sont automatiquement notifiées et mises à jour. Cela découple le sujet de ses observateurs, leur permettant de varier de façon indépendante.
Voici une mise en œuvre complète du modèle Observateur:
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
@abstractmethod
def update(self, subject):
"""Receive update from subject"""
pass
class Subject(ABC):
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
"""Attach an observer to the subject"""
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: Observer):
"""Detach an observer from the subject"""
try:
self._observers.remove(observer)
except ValueError:
pass
def notify(self):
"""Notify all observers about an event"""
for observer in self._observers:
observer.update(self)
class StockPrice(Subject):
def __init__(self, symbol: str, price: float):
super().__init__()
self._symbol = symbol
self._price = price
@property
def symbol(self):
return self._symbol
@property
def price(self):
return self._price
@price.setter
def price(self, new_price: float):
if new_price != self._price:
self._price = new_price
self.notify()
class StockDisplay(Observer):
def __init__(self, name: str):
self._name = name
def update(self, subject: StockPrice):
print(f"{self._name}: {subject.symbol} is now ${subject.price:.2f}")
class StockAlert(Observer):
def __init__(self, threshold: float):
self._threshold = threshold
def update(self, subject: StockPrice):
if subject.price > self._threshold:
print(f"ALERT: {subject.symbol} exceeded ${self._threshold}! Current: ${subject.price:.2f}")
# Usage
apple_stock = StockPrice("AAPL", 150.00)
display1 = StockDisplay("Display 1")
display2 = StockDisplay("Display 2")
alert = StockAlert(160.00)
apple_stock.attach(display1)
apple_stock.attach(display2)
apple_stock.attach(alert)
apple_stock.price = 155.50
apple_stock.price = 162.00
Applications du monde réel
Le modèle d'observateur est largement utilisé dans le développement de logiciels modernes:
- GUI Event Handling: Bouton cliquant, mouvements de souris et événements clavier
- Model-View-Controller (MVC): Les vues observent les modèles pour les changements de données
- Feeds de données en temps réel: Prix des stocks, mises à jour météorologiques, notifications sur les médias sociaux
- Systèmes de logage[: plusieurs gestionnaires de journaux observant les événements d'application
- Systèmes d'abonnement aux publications[: files d'attente pour les messages et les bus événementiels
Profil de décorateur : Extension dynamique de la fonctionnalité
Le motif de décorateur permet d'ajouter un comportement à un objet — englacement, cache, authentification, réessayer — sans modifier la classe de l'objet et sans sous-classement. Ce motif offre une alternative flexible à la sous-classe pour étendre la fonctionnalité.
Décorateurs Python vs motif de décorateur
La syntaxe @decorator de Python et le modèle de design de Decorator sont conceptuellement les mêmes : les deux comportements enveloppent un appel existant sans le modifier, la syntaxe de Python faisant du modèle une fonctionnalité linguistique-native.
Voici un exemple utilisant la syntaxe de décorateur de Python :
import time
import functools
def timing_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
def cache_decorator(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
print(f"Returning cached result for {args}")
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
def retry_decorator(max_attempts=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
return None
return wrapper
return decorator
@timing_decorator
@cache_decorator
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
@retry_decorator(max_attempts=3)
def unreliable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("API temporarily unavailable")
return "Success!"
# Usage
print(fibonacci(10))
print(fibonacci(10)) # This will use cached result
Modèle de décorateur basé sur la classe
Le modèle traditionnel de décorateur peut également être mis en œuvre en utilisant des classes, ce qui est utile pour des scénarios plus complexes:
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self):
pass
@abstractmethod
def description(self):
pass
class SimpleCoffee(Coffee):
def cost(self):
return 2.00
def description(self):
return "Simple coffee"
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost()
def description(self):
return self._coffee.description()
class MilkDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 0.50
def description(self):
return self._coffee.description() + ", milk"
class SugarDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 0.25
def description(self):
return self._coffee.description() + ", sugar"
class WhippedCreamDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 0.75
def description(self):
return self._coffee.description() + ", whipped cream"
# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost():.2f}")
coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost():.2f}")
fancy_coffee = WhippedCreamDecorator(SugarDecorator(MilkDecorator(SimpleCoffee())))
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost():.2f}")
Modèle de stratégie : Algorithmes interchangeables
Le modèle de stratégie vous permet de définir une famille d'algorithmes, d'encapsuler chacun d'eux et de les rendre interchangeables au moment de l'exécution, en délimitant le comportement à des classes ou des fonctions de stratégie distinctes au lieu d'écrire de grands blocs conditionnels.
Le modèle Stratégie définit une famille d'algorithmes, les place dans une classe séparée et rend leurs objets interchangeables. Cela permet de sélectionner des algorithmes à l'exécution en fonction du contexte ou de la configuration.
Plan de mise en œuvre de la stratégie
Voici un exemple complet qui illustre le modèle de la Stratégie pour le traitement des paiements :
from abc import ABC, abstractmethod
from typing import Protocol
class PaymentStrategy(Protocol):
def pay(self, amount: float) -> str:
"""Process payment and return confirmation"""
...
class CreditCardPayment:
def __init__(self, card_number: str, cvv: str, expiry: str):
self.card_number = card_number
self.cvv = cvv
self.expiry = expiry
def pay(self, amount: float) -> str:
# Simulate payment processing
masked_card = f"****-****-****-{self.card_number[-4:]}"
return f"Paid ${amount:.2f} using Credit Card {masked_card}"
class PayPalPayment:
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} using PayPal account {self.email}"
class CryptocurrencyPayment:
def __init__(self, wallet_address: str, currency: str = "BTC"):
self.wallet_address = wallet_address
self.currency = currency
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} using {self.currency} to wallet {self.wallet_address[:10]}..."
class BankTransferPayment:
def __init__(self, account_number: str, routing_number: str):
self.account_number = account_number
self.routing_number = routing_number
def pay(self, amount: float) -> str:
masked_account = f"****{self.account_number[-4:]}"
return f"Paid ${amount:.2f} via bank transfer from account {masked_account}"
class ShoppingCart:
def __init__(self):
self.items = []
self.payment_strategy = None
def add_item(self, item: str, price: float):
self.items.append({'item': item, 'price': price})
def set_payment_strategy(self, strategy: PaymentStrategy):
self.payment_strategy = strategy
def calculate_total(self) -> float:
return sum(item['price'] for item in self.items)
def checkout(self) -> str:
if not self.payment_strategy:
raise ValueError("Payment strategy not set")
total = self.calculate_total()
return self.payment_strategy.pay(total)
# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
cart.add_item("Keyboard", 79.99)
# Pay with credit card
cart.set_payment_strategy(CreditCardPayment("1234567890123456", "123", "12/25"))
print(cart.checkout())
# Change strategy to PayPal
cart.set_payment_strategy(PayPalPayment("[email protected]"))
print(cart.checkout())
# Change strategy to Cryptocurrency
cart.set_payment_strategy(CryptocurrencyPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"))
print(cart.checkout())
Stratégie pythonique avec fonctions
Les fonctions de première classe de Python permettent une mise en œuvre plus concise du modèle de la Stratégie :
def discount_none(total):
return total
def discount_percentage(percentage):
def apply_discount(total):
return total * (1 - percentage / 100)
return apply_discount
def discount_fixed_amount(amount):
def apply_discount(total):
return max(0, total - amount)
return apply_discount
def discount_bulk(threshold, discount_percentage):
def apply_discount(total):
if total >= threshold:
return total * (1 - discount_percentage / 100)
return total
return apply_discount
class Order:
def __init__(self, total: float):
self.total = total
self.discount_strategy = discount_none
def set_discount_strategy(self, strategy):
self.discount_strategy = strategy
def calculate_final_price(self):
return self.discount_strategy(self.total)
# Usage
order = Order(100.00)
print(f"No discount: ${order.calculate_final_price():.2f}")
order.set_discount_strategy(discount_percentage(10))
print(f"10% discount: ${order.calculate_final_price():.2f}")
order.set_discount_strategy(discount_fixed_amount(15))
print(f"$15 off: ${order.calculate_final_price():.2f}")
order.set_discount_strategy(discount_bulk(50, 20))
print(f"Bulk discount: ${order.calculate_final_price():.2f}")
Modèle de constructeur: Construction d'objets complexes
Le modèle Builder vous permet de construire des objets complexes étape par étape, vous permettant de produire différents types et représentations d'un objet en utilisant le même code de construction. Ce modèle est particulièrement utile lorsqu'un objet nécessite de nombreuses options de configuration ou lorsque le processus de construction comporte plusieurs étapes.
Quand utiliser le modèle de constructeur
Le modèle Builder brille dans des scénarios où:
- La construction d'objets nécessite de nombreux paramètres optionnels
- Le processus de construction doit suivre une séquence spécifique
- Vous devez créer différentes représentations du même objet
- Les paramètres du constructeur créeraient un anti-pattern "constructeur de télescopage"
- La création d'objets implique une logique complexe qui devrait être séparée de l'objet lui-même
Mise en œuvre du modèle de constructeur en Python
Voici une implémentation pratique pour construire des objets de requête de base de données:
from typing import List, Optional
from dataclasses import dataclass, field
@dataclass
class Query:
table: str
columns: List[str]
where_clauses: List[str]
joins: List[str]
order_by: Optional[str]
limit: Optional[int]
offset: Optional[int]
def to_sql(self) -> str:
# Select clause
cols = ", ".join(self.columns) if self.columns else "*"
sql = f"SELECT {cols} FROM {self.table}"
# Joins
if self.joins:
sql += " " + " ".join(self.joins)
# Where clause
if self.where_clauses:
sql += " WHERE " + " AND ".join(self.where_clauses)
# Order by
if self.order_by:
sql += f" ORDER BY {self.order_by}"
# Limit and offset
if self.limit:
sql += f" LIMIT {self.limit}"
if self.offset:
sql += f" OFFSET {self.offset}"
return sql
class QueryBuilder:
def __init__(self):
self._table: Optional[str] = None
self._columns: List[str] = []
self._where_clauses: List[str] = []
self._joins: List[str] = []
self._order_by: Optional[str] = None
self._limit: Optional[int] = None
self._offset: Optional[int] = None
def table(self, table_name: str) -> 'QueryBuilder':
self._table = table_name
return self
def select(self, *columns: str) -> 'QueryBuilder':
self._columns.extend(columns)
return self
def where(self, condition: str) -> 'QueryBuilder':
self._where_clauses.append(condition)
return self
def join(self, join_clause: str) -> 'QueryBuilder':
self._joins.append(join_clause)
return self
def order_by(self, column: str, direction: str = "ASC") -> 'QueryBuilder':
self._order_by = f"{column} {direction}"
return self
def limit(self, limit: int) -> 'QueryBuilder':
self._limit = limit
return self
def offset(self, offset: int) -> 'QueryBuilder':
self._offset = offset
return self
def build(self) -> Query:
if not self._table:
raise ValueError("Table name is required")
return Query(
table=self._table,
columns=self._columns,
where_clauses=self._where_clauses,
joins=self._joins,
order_by=self._order_by,
limit=self._limit,
offset=self._offset
)
def reset(self) -> 'QueryBuilder':
self.__init__()
return self
# Usage
builder = QueryBuilder()
# Build a simple query
query1 = (builder
.table("users")
.select("id", "name", "email")
.where("age > 18")
.where("status = 'active'")
.order_by("name", "ASC")
.limit(10)
.build())
print(query1.to_sql())
# Build a complex query with joins
builder.reset()
query2 = (builder
.table("orders")
.select("orders.id", "users.name", "products.title", "orders.total")
.join("INNER JOIN users ON orders.user_id = users.id")
.join("INNER JOIN products ON orders.product_id = products.id")
.where("orders.status = 'completed'")
.where("orders.total > 100")
.order_by("orders.created_at", "DESC")
.limit(20)
.offset(10)
.build())
print(query2.to_sql())
Avantages de l'interface fluide
Le modèle Builder implémente souvent une interface fluide (chaînement de méthodes), qui offre plusieurs avantages:
- Readability: Le code se lit comme un langage naturel
- Flexibilité: Facile à ajouter ou à supprimer des étapes de configuration
- Immutabilité: L'objet final peut être immuable pendant que le constructeur est mutable
- Validation: La logique de construction peut valider l'objet avant la création
- Reutilisabilité: Les constructeurs peuvent être réutilisés pour créer plusieurs objets similaires
Modèle d'adaptateur : faire fonctionner les interfaces incompatibles
Le modèle Adapter permet aux objets avec des interfaces incompatibles de collaborer. Ce modèle structural agit comme un pont entre deux interfaces incompatibles, permettant aux classes de travailler ensemble qui ne pourraient autrement pas en raison d'interfaces incompatibles.
La méthode adaptateur est un modèle de conception structurelle qui vous permet de faire fonctionner deux interfaces incompatibles en créant un pont entre elles. Ceci est particulièrement utile lors de l'intégration de bibliothèques tierces, de code d'origine ou d'API externes dans votre application.
Mise en œuvre de l'adaptateur réel mondial
Considérez un scénario où vous devez intégrer plusieurs passerelles de paiement avec différentes interfaces:
from abc import ABC, abstractmethod
# Target interface that our application expects
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float, currency: str) -> dict:
pass
# Adaptee 1: Stripe API (incompatible interface)
class StripeAPI:
def create_charge(self, amount_cents: int, currency_code: str, source: str):
return {
'id': 'ch_stripe_123',
'amount': amount_cents,
'currency': currency_code,
'status': 'succeeded'
}
# Adaptee 2: PayPal API (different incompatible interface)
class PayPalAPI:
def make_payment(self, total: float, currency_type: str, account: str):
return {
'transaction_id': 'pp_456',
'total_amount': total,
'currency': currency_type,
'state': 'approved'
}
# Adaptee 3: Square API (yet another incompatible interface)
class SquareAPI:
def charge_card(self, money_amount: dict, card_token: str):
return {
'payment_id': 'sq_789',
'amount_money': money_amount,
'status': 'COMPLETED'
}
# Adapter for Stripe
class StripeAdapter(PaymentProcessor):
def __init__(self, stripe_api: StripeAPI):
self.stripe = stripe_api
def process_payment(self, amount: float, currency: str) -> dict:
# Convert dollars to cents for Stripe
amount_cents = int(amount * 100)
# Call Stripe API with adapted parameters
result = self.stripe.create_charge(
amount_cents=amount_cents,
currency_code=currency.upper(),
source='tok_visa'
)
# Adapt the response to our standard format
return {
'success': result['status'] == 'succeeded',
'transaction_id': result['id'],
'amount': amount,
'currency': currency,
'provider': 'Stripe'
}
# Adapter for PayPal
class PayPalAdapter(PaymentProcessor):
def __init__(self, paypal_api: PayPalAPI):
self.paypal = paypal_api
def process_payment(self, amount: float, currency: str) -> dict:
result = self.paypal.make_payment(
total=amount,
currency_type=currency.upper(),
account='[email protected]'
)
return {
'success': result['state'] == 'approved',
'transaction_id': result['transaction_id'],
'amount': amount,
'currency': currency,
'provider': 'PayPal'
}
# Adapter for Square
class SquareAdapter(PaymentProcessor):
def __init__(self, square_api: SquareAPI):
self.square = square_api
def process_payment(self, amount: float, currency: str) -> dict:
money_amount = {
'amount': int(amount * 100),
'currency': currency.upper()
}
result = self.square.charge_card(
money_amount=money_amount,
card_token='cnon:card-token'
)
return {
'success': result['status'] == 'COMPLETED',
'transaction_id': result['payment_id'],
'amount': amount,
'currency': currency,
'provider': 'Square'
}
# Client code that works with the unified interface
class PaymentService:
def __init__(self, processor: PaymentProcessor):
self.processor = processor
def charge_customer(self, amount: float, currency: str = 'USD'):
result = self.processor.process_payment(amount, currency)
if result['success']:
print(f"✓ Payment successful via {result['provider']}")
print(f" Transaction ID: {result['transaction_id']}")
print(f" Amount: {result['amount']} {result['currency']}")
else:
print(f"✗ Payment failed")
return result
# Usage - all payment gateways work through the same interface
stripe_processor = StripeAdapter(StripeAPI())
paypal_processor = PayPalAdapter(PayPalAPI())
square_processor = SquareAdapter(SquareAPI())
# Process payments using different providers
service = PaymentService(stripe_processor)
service.charge_customer(99.99)
service = PaymentService(paypal_processor)
service.charge_customer(149.99)
service = PaymentService(square_processor)
service.charge_customer(199.99)
Modèle Modèle de méthode: Définition des écueils d'algorithme
La méthode modèle définit le squelette d'un algorithme dans la superclasse mais permet aux sous-classes de passer outre des étapes spécifiques de l'algorithme sans changer sa structure. Ce modèle comportemental est excellent pour l'application d'un processus cohérent tout en permettant la personnalisation à des points spécifiques.
Méthode type Mise en œuvre
from abc import ABC, abstractmethod
import time
class DataProcessor(ABC):
"""Template class defining the data processing algorithm"""
def process(self, data):
"""Template method defining the algorithm structure"""
print("Starting data processing pipeline...")
# Step 1: Validate
if not self.validate(data):
raise ValueError("Data validation failed")
# Step 2: Extract
extracted = self.extract(data)
# Step 3: Transform
transformed = self.transform(extracted)
# Step 4: Load
result = self.load(transformed)
# Step 5: Cleanup (optional hook)
self.cleanup()
print("Data processing completed successfully")
return result
@abstractmethod
def validate(self, data) -> bool:
"""Validate input data - must be implemented by subclasses"""
pass
@abstractmethod
def extract(self, data):
"""Extract relevant data - must be implemented by subclasses"""
pass
@abstractmethod
def transform(self, data):
"""Transform data - must be implemented by subclasses"""
pass
@abstractmethod
def load(self, data):
"""Load processed data - must be implemented by subclasses"""
pass
def cleanup(self):
"""Optional hook method - can be overridden by subclasses"""
print("Performing default cleanup...")
class CSVDataProcessor(DataProcessor):
def validate(self, data) -> bool:
print("Validating CSV data...")
return isinstance(data, str) and len(data) > 0
def extract(self, data):
print("Extracting CSV data...")
lines = data.strip().split('n')
headers = lines[0].split(',')
rows = [line.split(',') for line in lines[1:]]
return {'headers': headers, 'rows': rows}
def transform(self, data):
print("Transforming CSV data...")
headers = data['headers']
rows = data['rows']
return [dict(zip(headers, row)) for row in rows]
def load(self, data):
print(f"Loading {len(data)} CSV records...")
return data
def cleanup(self):
print("Cleaning up CSV processing resources...")
class JSONDataProcessor(DataProcessor):
def validate(self, data) -> bool:
print("Validating JSON data...")
import json
try:
json.loads(data)
return True
except:
return False
def extract(self, data):
print("Extracting JSON data...")
import json
return json.loads(data)
def transform(self, data):
print("Transforming JSON data...")
if isinstance(data, list):
return [self._flatten_dict(item) for item in data]
return [self._flatten_dict(data)]
def _flatten_dict(self, d, parent_key='', sep='_'):
items = []
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(self._flatten_dict(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)
def load(self, data):
print(f"Loading {len(data)} JSON records...")
return data
class XMLDataProcessor(DataProcessor):
def validate(self, data) -> bool:
print("Validating XML data...")
return data.strip().startswith('')
def extract(self, data):
print("Extracting XML data...")
# Simplified XML parsing
return {'xml_content': data}
def transform(self, data):
print("Transforming XML data...")
return {'processed_xml': data['xml_content']}
def load(self, data):
print("Loading XML data...")
return data
def cleanup(self):
print("Cleaning up XML processing resources...")
print("Closing XML parser...")
# Usage
csv_data = """name,age,city
John,30,New York
Jane,25,Los Angeles
Bob,35,Chicago"""
json_data = '''[
{"name": "John", "age": 30, "address": {"city": "New York"}},
{"name": "Jane", "age": 25, "address": {"city": "Los Angeles"}}
]'''
xml_data = "John30"
# Process CSV
csv_processor = CSVDataProcessor()
csv_result = csv_processor.process(csv_data)
print(f"CSV Result: {csv_result}n")
# Process JSON
json_processor = JSONDataProcessor()
json_result = json_processor.process(json_data)
print(f"JSON Result: {json_result}n")
# Process XML
xml_processor = XMLDataProcessor()
xml_result = xml_processor.process(xml_data)
print(f"XML Result: {xml_result}")
Quand utiliser les modèles de conception
Savoir quand utiliser les modèles de conception est crucial pour une conception efficace des logiciels, particulièrement lorsque vous rencontrez des problèmes de conception récurrents qui ont des solutions bien établies, car les modèles de conception fournissent des approches éprouvées et éprouvées aux défis communs de conception des logiciels.
Cas d'utilisation appropriée
Utilisez les modèles de conception pour promouvoir la réutilisation, la flexibilité et la maintenance du code, car ils aident à structurer le code de manière à ce qu'il soit plus facile de modifier et d'étendre les exigences en fonction de l'évolution des besoins.
- Vous avez un problème qui correspond à l'intention d'un modèle connu
- Le modèle offre des avantages évidents sur une solution plus simple
- Votre équipe comprend le modèle et peut le maintenir
- La complexité supplémentaire est justifiée par une flexibilité ou une maintenabilité accrues
- Vous voulez améliorer la communication entre les membres de l'équipe
Quand NE PAS utiliser les modèles
Le meilleur code est le code le plus simple qui résout correctement le problème, parfois c'est un modèle bien placé, mais souvent c'est juste une fonction et un dict. Évitez les modèles quand:
- Une solution plus simple fonctionnerait aussi bien
- Vous appliquez des modèles pour l'utilisation de modèles
- Le modèle ajoute une complexité inutile au code simple
- Votre équipe ne connaît pas le modèle et la documentation manque.
- Le problème ne correspond pas à l'intention du modèle
Chaque modèle a ses propres compromis, et vous devez prêter plus attention à la raison pour laquelle vous choisissez un modèle donné que de la façon de le mettre en œuvre. La décision d'utiliser un modèle devrait être motivée par le problème en cause, pas par le désir de démontrer la connaissance des modèles.
Anti-Patters à éviter dans Python
Les modules Python sont déjà des monotones – chaque module n'est importé qu'une seule fois, de sorte que les classes de monotones explicites ajoutent une complexité inutile, avec de meilleures alternatives étant des variables de niveau de module ou une injection de dépendance.
Des modèles qui ne correspondent pas bien à Python
- God Object: Centralise trop de logique dans une seule classe, rend le code plus difficile à tester et à maintenir, avec la meilleure alternative étant de diviser la fonctionnalité en classes plus petites et cohésives
- Hiérarchies de l'héritage profond: Les arbres de l'héritage profond rendent le code fragile, alors préférez la composition et la délégation
- Abstractions inutiles: Le typage du canard de Python élimine souvent le besoin de hiérarchies d'interface complexes
Considérations modernes sur le modèle de python
Dans Python moderne (3.8+), préférez Protocole pour le sous-typage structurel car il ne nécessite pas d'héritage explicite, avec des classes satisfaisant un Protocole juste en ayant les bonnes méthodes, tout en utilisant ABC quand vous voulez faire respecter l'héritage et fournir des implémentations par défaut.
Utilisation des protocoles pour le dactylotype du canard
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing a circle"
class Square:
def draw(self) -> str:
return "Drawing a square"
def render(shape: Drawable) -> None:
print(shape.draw())
# Both Circle and Square satisfy the Drawable protocol
# without explicit inheritance
render(Circle())
render(Square())
Dessins dans les applications Python de production
En 2026, Python se trouve à l'intersection de l'IA et de l'apprentissage automatique, du développement évolutif de produits web et de l'ingénierie des données d'entreprise, avec sa domination reflétant un avantage structurel que les équipes d'ingénierie ont découvert il y a des années : Python vous permet de bouger plus rapidement, d'intégrer plus facilement et de construire des systèmes qui sont entretenus par les équipes, pas seulement par l'auteur original.
Modèles spécifiques au cadre
Django reste le cadre le plus complet pour la construction de produits par équipes avec accès multi-utilisateurs, modèles de données complexes, interfaces administratives et systèmes d'authentification. Django utilise de manière intensive des modèles comme:
- Modèle-Vue-Template (MVT): La variation de Django de MVC
- Enregistrement actif: Modèles ORM de Django
- Méthode type: vues fondées sur des classes
- Chaîne de micro-appareils: Traitement des demandes/réponses
FastAPI met à profit les fonctionnalités et les modèles modernes de Python :
- Injection de la dépen dance: Système d'AI intégré
- : Décors de route
- Plan stratégique: Authentification plugtable
- Modèle de réaction[: Création de modèle de réponse
Modèles architecturaux
En 2026, le consensus entre les équipes d'ingénierie Python expérimentées est plus clair : commencer par un monolithe bien structuré, se décomposer en services lorsque des limites spécifiques émergent d'une utilisation réelle, car un monolithe n'est pas un mode de défaillance.
Instagram a couru sur un monolithe Django bien passé 100 millions d'utilisateurs avant de décomposer des fonctions spécifiques à haute charge en services, la clé étant de construire le monolithe avec décomposition de service en tête dès le début à travers des limites de module claires, un couplage minimal entre modules et des modèles de communication animés par des événements.
Essais de modèles
Les modèles de conception devraient rendre le code plus testable, pas moins. Voici des stratégies pour tester les implémentations de modèles:
Plan de stratégie d'essai
import unittest
from unittest.mock import Mock
class TestPaymentStrategies(unittest.TestCase):
def test_credit_card_payment(self):
strategy = CreditCardPayment("1234567890123456", "123", "12/25")
result = strategy.pay(100.00)
self.assertIn("Credit Card", result)
self.assertIn("100.00", result)
def test_paypal_payment(self):
strategy = PayPalPayment("[email protected]")
result = strategy.pay(50.00)
self.assertIn("PayPal", result)
self.assertIn("[email protected]", result)
def test_shopping_cart_with_different_strategies(self):
cart = ShoppingCart()
cart.add_item("Item 1", 50.00)
# Test with credit card
cart.set_payment_strategy(CreditCardPayment("1234", "123", "12/25"))
result1 = cart.checkout()
# Test with PayPal
cart.set_payment_strategy(PayPalPayment("[email protected]"))
result2 = cart.checkout()
self.assertIsNotNone(result1)
self.assertIsNotNone(result2)
Essai du modèle d'observateur
class TestObserverPattern(unittest.TestCase):
def test_observer_notification(self):
stock = StockPrice("AAPL", 150.00)
observer = Mock(spec=Observer)
stock.attach(observer)
stock.price = 155.00
observer.update.assert_called_once_with(stock)
def test_multiple_observers(self):
stock = StockPrice("GOOGL", 2800.00)
observer1 = Mock(spec=Observer)
observer2 = Mock(spec=Observer)
stock.attach(observer1)
stock.attach(observer2)
stock.price = 2850.00
observer1.update.assert_called_once()
observer2.update.assert_called_once()
def test_detach_observer(self):
stock = StockPrice("MSFT", 300.00)
observer = Mock(spec=Observer)
stock.attach(observer)
stock.detach(observer)
stock.price = 310.00
observer.update.assert_not_called()
Considérations relatives aux performances
Bien que les modèles de conception améliorent l'organisation du code et sa maintenance, ils peuvent introduire des frais généraux de performance si elles ne sont pas mises en œuvre avec soin.
Initialisation paresseuse
L'instance n'est créée que lorsque la méthode get instance() est appelée pour la première fois, en veillant à ce que les ressources ne soient allouées qu'au besoin. Ceci est particulièrement important pour les objets à forte intensité de ressources :
class DatabaseConnection:
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not DatabaseConnection._initialized:
self._connect()
DatabaseConnection._initialized = True
def _connect(self):
# Expensive connection operation
print("Establishing database connection...")
self.connection = "Connected"
Résultats du décorateur de cache
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_computation(n):
"""Cached using built-in LRU cache"""
print(f"Computing for {n}...")
return sum(range(n))
# First call computes
result1 = expensive_computation(1000)
# Second call returns cached result
result2 = expensive_computation(1000)
Meilleures pratiques pour les modèles de conception en Python
Les modèles de conception devraient simplifier, et non compliquer, car les modèles de conception de Python ne sont pas à propos de copier des diagrammes de manuels, ils sont à propos de résoudre les problèmes réels avec élégance.
1. Composition des favoris sur l'héritage
La nature dynamique de Python rend la composition particulièrement puissante. Au lieu de hiérarchies d'héritage profondes, composez des objets à partir de composants plus petits et ciblés :
# Instead of inheritance
class EmailNotifier:
def send(self, message):
print(f"Sending email: {message}")
class SMSNotifier:
def send(self, message):
print(f"Sending SMS: {message}")
# Use composition
class NotificationService:
def __init__(self):
self.notifiers = []
def add_notifier(self, notifier):
self.notifiers.append(notifier)
def notify(self, message):
for notifier in self.notifiers:
notifier.send(message)
# Usage
service = NotificationService()
service.add_notifier(EmailNotifier())
service.add_notifier(SMSNotifier())
service.notify("Important update!")
2. Utiliser des conseils de type pour la clarté
Les conseils de type rendent les implémentations de motifs plus explicites et permettent une meilleure prise en charge de l'IDE :
from typing import Protocol, List
from abc import abstractmethod
class PaymentMethod(Protocol):
def process(self, amount: float) -> bool:
...
class PaymentProcessor:
def __init__(self, methods: List[PaymentMethod]):
self.methods = methods
def charge(self, amount: float) -> bool:
for method in self.methods:
if method.process(amount):
return True
return False
3. Utilisation du modèle de document
Toujours documenter quel modèle vous utilisez et pourquoi:
class DataExporter:
"""
Uses the Strategy pattern to support multiple export formats.
This allows adding new export formats without modifying existing code,
following the Open/Closed Principle.
Example:
exporter = DataExporter(JSONExportStrategy())
exporter.export(data)
"""
def __init__(self, strategy: ExportStrategy):
self._strategy = strategy
def export(self, data):
return self._strategy.export(data)
4. Gardez-le simple
Ne sur-enginer. Commencez simple et refactorez les modèles lorsque la complexité le justifie:
# Simple is often better
def calculate_discount(price, customer_type):
discounts = {
'regular': 0,
'premium': 0.10,
'vip': 0.20
}
return price * (1 - discounts.get(customer_type, 0))
# Only introduce Strategy pattern when you need:
# - Runtime strategy switching
# - Complex discount logic
# - Multiple discount calculation methods
Ressources pour l'apprentissage des modèles de conception
Pour approfondir votre compréhension des modèles de conception en Python, explorez ces précieuses ressources :
- Refactoring.Guru: Catalogue complet des motifs de conception avec des exemples de Python à https://refactoring.guru/design-patterns/python
- Guide des motifs de python: guide évolutif de Brandon Rhodes à https://python-patterns.guide/
- Python de GitHub: collection de motifs axée sur la communauté à https://github.com/faif/python-patterns
- Real Python: Des tutoriels pratiques sur les modèles de conception de Python à https://realpython.com
- GeeksforGeeks Python Design Patterns: Didacticiels complets à https://www.geeksforgeeks.org/python-design-patterns/
Conclusion
Les modèles de conception sont des outils puissants dans la boîte à outils d'un ingénieur Python, mais ils doivent être appliqués judicieusement. Les modèles de stratégie, d'usine et d'observateur apparaissent souvent ensemble dans des systèmes Python bien archivés, avec Stratégie axée sur la sélection et l'échange d'algorithmes, Factory se concentrant sur la façon dont les objets sont créés, et Observer traitant de la communication, en comprenant ces distinctions vous aidant à choisir le bon modèle en fonction de votre défi à savoir si votre comportement, création ou communication.
La clé pour utiliser avec succès les modèles de conception en Python est de comprendre les modèles eux-mêmes et les caractéristiques uniques de Python. La typage dynamique, les fonctions de première classe et la bibliothèque standard puissante de Python permettent souvent des implémentations plus simples que dans les langages à caractères statiques.
N'oubliez pas que les motifs sont des moyens à une fin, pas des fins en eux-mêmes. L'objectif est d'écrire du code qui est facile à comprendre, à tester et à modifier. Lorsqu'un motif aide à atteindre ce but, utilisez-le. Lorsqu'une solution plus simple suffit, embrassez la simplicité.
En maîtrisant les modèles de conception et en comprenant quand les appliquer, vous serez mieux équipés pour construire des applications Python robustes et évolutives qui résistent au test du temps et des exigences en évolution.