Patron Fabrique en Python
Le patron Factory est un patron de conception de création qui fournit la meilleure façon de créer des objets.
Imaginez que vous allez au restaurant pour commander à manger : vous n'avez pas besoin de savoir comment la cuisine prépare les plats, vous dites simplement au serveur ce que vous voulez, et la cuisine le préparera pour vous.
Le patron Factory est une telle « cuisine » : il est responsable de la création des objets, et vous n'avez qu'à lui indiquer quel type d'objet vous voulez.
Pourquoi avons-nous besoin du patron Factory ?
En programmation, nous avons souvent besoin de créer des objets. Si nous utilisons directement lenewmot-clé ou un constructeur de classe dans le code, cela conduira à :
- Fort couplage du code: Le code de création d'objets est étroitement couplé aux classes concrètes
- Difficile à maintenir: Lorsque vous devez modifier ou ajouter de nouveaux types d'objets, vous devez modifier le code à plusieurs endroits
- Viole le principe Open/Closed: Le principe d'ouverture à l'extension et de fermeture à la modification est enfreint
Le patron Factory résout ces problèmes en encapsulant le processus de création d'objets.
Trois types de patron Factory
Le patron Factory se divise principalement en trois types ; comprenons-les à travers des exemples concrets.
1. Patron Simple Factory
Le patron Simple Factory est le patron de fabrique le plus basique ; il utilise une classe de fabrique pour créer différents types d'objets.
Structure de base
Exemple
# Interface de produit
class Animal(ABC):
@abstractmethod
def speak(self):
pass
# Produit concret
class Dog(Animal):
def speak(self):
return "Ouaf!"
class Cat(Animal):
def speak(self):
return "Miaou!"
class Duck(Animal):
def speak(self):
return "Coin-coin!"
# Fabrique simple
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
elif animal_type == "duck":
return Duck()
else:
raise ValueError(f"Type d'animal inconnu : {animal_type}")
# Exemple d'utilisation
def test_simple_factory():
factory = AnimalFactory()
dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
duck = factory.create_animal("duck")
print(dog.speak()) # Sortie : Woof!
print(cat.speak()) # Sortie : Meow!
print(duck.speak()) # Sortie : Quack!
if __name__ == "__main__":
test_simple_factory()
Avantages et inconvénients
Avantages :
- Découple le client des classes concrètes de produits
- Séparation des responsabilités, facile à maintenir
Inconvénients :
- L'ajout de nouveaux produits nécessite de modifier la classe de fabrique, ce qui viole le principe Open/Closed
- La classe de fabrique a trop de responsabilités, ce qui ne respecte pas le principe de responsabilité unique
2. Patron Factory Method
Le patron de méthode de fabrique résout les problèmes du patron de fabrique simple en laissant les sous-classes décider quels objets créer.
Structure de base
Exemple
# Interface de produit
class Button(ABC):
@abstractmethod
def render(self):
pass
@abstractmethod
def onClick(self):
pass
# Produit concret
class WindowsButton(Button):
def render(self):
return "Afficher un bouton de style Windows"
def onClick(self):
return "Bouton Windows cliqué"
class MacButton(Button):
def render(self):
return "Afficher un bouton de style Mac"
def onClick(self):
return "Bouton Mac cliqué"
# Classe créatrice abstraite
class Dialog(ABC):
@abstractmethod
def createButton(self) -> Button:
pass
def render(self):
# Appeler la méthode de fabrique pour créer un produit
button = self.createButton()
result = button.render()
return result
# Créateur concret
class WindowsDialog(Dialog):
def createButton(self) -> Button:
return WindowsButton()
class MacDialog(Dialog):
def createButton(self) -> Button:
return MacButton()
# Exemple d'utilisation
def test_factory_method():
# Choisir la fabrique concrète en fonction de la configuration
config = "windows" # Peut être lu depuis un fichier de configuration
if config == "windows":
dialog = WindowsDialog()
else:
dialog = MacDialog()
result = dialog.render()
print(result)
if __name__ == "__main__":
test_factory_method()
Flux de travail du patron Méthode de fabrique

3. Patron Abstract Factory
Le patron Fabrique abstraite fournit une interface pour créer des familles d'objets liés ou dépendants sans spécifier leurs classes concrètes.
Structure de base
Exemple
# Produit abstrait A
class Button(ABC):
@abstractmethod
def paint(self):
pass
# Produit abstrait B
class Checkbox(ABC):
@abstractmethod
def paint(self):
pass
# Produit concret A1
class WindowsButton(Button):
def paint(self):
return "Afficher le bouton Windows"
# Produit concret A2
class MacButton(Button):
def paint(self):
return "Afficher le bouton Mac"
# Produit concret B1
class WindowsCheckbox(Checkbox):
def paint(self):
return "Afficher la case à cocher Windows"
# Produit concret B2
class MacCheckbox(Checkbox):
def paint(self):
return "Afficher la case à cocher Mac"
# Fabrique abstraite
class GUIFactory(ABC):
@abstractmethod
def createButton(self) -> Button:
pass
@abstractmethod
def createCheckbox(self) -> Checkbox:
pass
# Fabrique concrète 1
class WindowsFactory(GUIFactory):
def createButton(self) -> Button:
return WindowsButton()
def createCheckbox(self) -> Checkbox:
return WindowsCheckbox()
# Fabrique concrète 2
class MacFactory(GUIFactory):
def createButton(self) -> Button:
return MacButton()
def createCheckbox(self) -> Checkbox:
return MacCheckbox()
# Code client
class Application:
def __init__(self, factory: GUIFactory):
self.factory = factory
self.button = None
self.checkbox = None
def createUI(self):
self.button = self.factory.createButton()
self.checkbox = self.factory.createCheckbox()
def paint(self):
result = []
if self.button:
result.append(self.button.paint())
if self.checkbox:
result.append(self.checkbox.paint())
return "\n".join(result)
# Exemple d'utilisation
def test_abstract_factory():
# Sélectionner la fabrique en fonction du type de système
system_type = "windows" # Peut être détecté automatiquement ou lu depuis la configuration
if system_type == "windows":
factory = WindowsFactory()
else:
factory = MacFactory()
app = Application(factory)
app.createUI()
print(app.paint())
if __name__ == "__main__":
test_abstract_factory()
Comparaison des trois patrons de fabrique
| Fonctionnalité | Patron de fabrique simple | Patron de méthode de fabrique | Patron de conception Fabrique abstraite |
|---|---|---|---|
| Complexité | Faible | Moyenne | Élevée |
| Extensibilité | Médiocre | OK | Très bon |
| Scénarios applicables | Peu de types d'objets | Famille de produits unique | Plusieurs familles de produits apparentées |
| Principe ouvert/fermé | Viole | Respecte | Respecte |
| Dépendance | Dépend de classes concrètes | Dépend de classes abstraites | Dépend d'interfaces abstraites |
Scénarios d'application pratiques
Scénario 1 : Fabrique de connexion à la base de données
Exemple
import sqlite3
import mysql.connector
# Interface de connexion à la base de données
class DatabaseConnection(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def execute(self, query):
pass
# Connexion concrète à la base de données
class SQLiteConnection(DatabaseConnection):
def __init__(self, db_path):
self.db_path = db_path
self.connection = None
def connect(self):
self.connection = sqlite3.connect(self.db_path)
return self.connection
def execute(self, query):
if self.connection:
cursor = self.connection.cursor()
cursor.execute(query)
return cursor.fetchall()
class MySQLConnection(DatabaseConnection):
def __init__(self, host, user, password, base de données):
self.host = host
self.user = user
self.password = password
self.database = database
self.connection = None
def connect(self):
self.connection = mysql.connector.connect(
host=self.host,
user=self.user,
password=self.password,
database=self.database
)
return self.connection
def execute(self, query):
if self.connection:
cursor = self.connection.cursor()
cursor.execute(query)
return cursor.fetchall()
# Fabrique de base de données
class DatabaseFactory:
@staticmethod
def create_connection(db_type, **kwargs):
if db_type == "sqlite":
return SQLiteConnection(**kwargs)
elif db_type == "mysql":
return MySQLConnection(**kwargs)
else:
raise ValueError(f"Type de base de données non pris en charge : {db_type}")
# Exemple d'utilisation
def test_database_factory():
# Créer une connexion SQLite
sqlite_conn = DatabaseFactory.create_connection(
"sqlite",
db_path="example.db"
)
sqlite_conn.connect()
# Créer une connexion MySQL
mysql_conn = DatabaseFactory.create_connection(
"mysql",
host="localhost",
user="root",
password="password",
database="test"
)
mysql_conn.connect()
print("Connexion à la base de données créée avec succès !")
if __name__ == "__main__":
test_database_factory()
Scénario 2 : Fabrique Logger
Exemple
from abc import ABC, abstractmethod
import sys
# Interface Logger
class Logger(ABC):
@abstractmethod
def info(self, message):
pass
@abstractmethod
def error(self, message):
pass
@abstractmethod
def debug(self, message):
pass
# Journaliseur de console
class ConsoleLogger(Logger):
def info(self, message):
print(f"INFO: {message}")
def error(self, message):
print(f"ERROR: {message}", file=sys.stderr)
def debug(self, message):
print(f"DEBUG: {message}")
# Journaliseur de fichiers
class FileLogger(Logger):
def __init__(self, filename):
self.filename = filename
def info(self, message):
with open(self.filename, 'a') as f:
f.write(f"INFO: {message}\n")
def error(self, message):
with open(self.filename, 'a') as f:
f.write(f"ERROR: {message}\n")
def debug(self, message):
with open(self.filename, 'a') as f:
f.write(f"DEBUG: {message}\n")
# Fabrique de logger
class LoggerFactory:
@staticmethod
def get_logger(logger_type, **kwargs):
if logger_type == "console":
return ConsoleLogger()
elif logger_type == "file":
return FileLogger(**kwargs)
else:
raise ValueError(f"Type de logger non pris en charge : {logger_type}")
# Exemple d'utilisation
def test_logger_factory():
# Créer un logger console
console_logger = LoggerFactory.get_logger("console")
console_logger.info("Ceci est un message d'information")
console_logger.error("Ceci est un message d'erreur")
# Créer un logger fichier
file_logger = LoggerFactory.get_logger("file", filename="app.log")
file_logger.info("Information enregistrée dans le fichier")
file_logger.debug("Informations de débogage")
if __name__ == "__main__":
test_logger_factory()
Bonnes pratiques et considérations
1. Quand utiliser le patron Factory ?
Situations adaptées à l'utilisation du patron Factory :
- Le processus de création d'objets est relativement complexe
- Besoin de créer différents objets en fonction de différentes conditions
- Vouloir séparer la création d'objets de l'utilisation
- Le système doit prendre en charge plusieurs types de produits
Situations où il n'est pas adapté :
- Le processus de création d'objets est simple, il suffit d'utiliser directement le constructeur
- Il y a peu de types de produits et l'extension est peu probable
2. Erreurs courantes et comment les éviter
Erreur 1: Sur-ingénierie
Exemple
class SimpleObject:
def __init__(self, name):
self.name = name
# Fabrique sur-ingénierée
class SimpleObjectFactory:
@staticmethod
def create_simple_object(name):
return SimpleObject(name)
# Recommandé : créer directement
obj = SimpleObject("test")
Erreur 2 : La classe Fabrique a trop de responsabilités
Exemple
class GodFactory:
def create_user(self): ...
def create_order(self): ...
def create_product(self): ...
def send_email(self): ... # Ce n'est pas de la création d'objets !
# Recommandé : séparer par responsabilité
class UserFactory: ...
class OrderFactory: ...
class ProductFactory: ...
3. Intégration avec l'injection de dépendances
Le patron Factory est souvent utilisé avec l'injection de dépendances (DI) :
Exemple
# Interface de service
class NotificationService(ABC):
@abstractmethod
def send(self, message):
pass
# Service concret
class EmailService(NotificationService):
def send(self, message):
return f"Envoyer un e-mail : {message}"
class SMSService(NotificationService):
def send(self, message):
return f"Envoyer un SMS : {message}"
# Fabrique
class NotificationFactory:
@staticmethod
def create_service(service_type):
if service_type == "email":
return EmailService()
elif service_type == "sms":
return SMSService()
else:
raise ValueError(f"Type de service inconnu : {service_type}")
# Classe utilisant l'injection de dépendances
class OrderProcessor:
def __init__(self, notification_service: NotificationService):
self.notification_service = notification_service
def process_order(self, order):
# Gérer la logique de commande
result = self.notification_service.send("Traitement de la commande terminé")
return result
# Utilisation
def main():
# Créer un service via la fabrique
notification_service = NotificationFactory.create_service("email")
# Injecter la dépendance
processor = OrderProcessor(notification_service)
result = processor.process_order({"id": 1})
print(result)
if __name__ == "__main__":
main()
Exercices pratiques
Exercice 1 : Implémenter une fabrique de formes
Créez une fabrique de formes qui permet de créer Circle, Rectangle et Triangle. Chaque forme doit avoir des méthodes pour calculer l'aire et le périmètre.
Exigences :
- Utilisez le patron Factory Method
- Chaque classe de forme implémente
calculate_area()etcalculate_perimeter()methods - Fournir des exemples d'utilisation
Exercice 2 : Étendre la fabrique de bases de données
En vous basant sur l'exemple précédent de fabrique de bases de données, ajoutez la prise en charge de la base de données PostgreSQL.
Exigences :
- Créez
PostgreSQLConnectionClasse - Modifiez la fabrique pour prendre en charge le nouveau type de base de données
- Assurez-vous de ne pas casser le code existant
Exercice 3 : Fabrique pilotée par configuration
Créez un système capable de sélectionner dynamiquement une fabrique en fonction de fichiers de configuration.
Exigences :
- Lire la configuration à partir de fichiers JSON ou YAML
- Créer les objets correspondants en fonction de la configuration
- Prendre en charge le rechargement à chaud de la configuration
Résumé
Le patron Fabrique est un patron de création très important dans la conception Python. En encapsulant le processus de création d'objets, il offre les avantages suivants :
- Réduire le couplage: Le client n'a pas besoin de connaître les détails de création de produits spécifiques.
- Améliorer la maintenabilité: La logique de création est gérée de manière centralisée, ce qui facilite les modifications et les extensions.
- Renforcer la flexibilité: De nouveaux types de produits peuvent être facilement ajoutés.
- Favoriser la réutilisation du code: La logique de création peut être réutilisée à plusieurs endroits.
N'oubliez pas de choisir le type approprié de patron de fabrique :
- Fabrique simple: Convient aux scénarios avec peu de types de produits et peu de changements.
- Méthode de fabrique: Convient aux scénarios où les familles de produits doivent être étendues.
- Fabrique abstraite: Convient aux scénarios complexes nécessitant la création de familles de produits liées.
En utilisant correctement le patron de fabrique, vous pouvez écrire du code Python plus flexible, plus maintenable et plus extensible !
Autres extensions