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

from abc import ABC, abstractmethod

# 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

from abc import ABC, abstractmethod

# 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

from abc import ABC, abstractmethod

# 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

from abc import ABC, abstractmethod
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

import logging
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

# Non recommandé: utiliser une fabrique complexe pour une situation simple
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

# Non recommandé : une fabrique qui fait trop de choses
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

from abc import ABC, abstractmethod

# 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émentecalculate_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éezPostgreSQLConnectionClasse
  • 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 :

  1. Réduire le couplage: Le client n'a pas besoin de connaître les détails de création de produits spécifiques.
  2. 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.
  3. Renforcer la flexibilité: De nouveaux types de produits peuvent être facilement ajoutés.
  4. 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