Patron de méthode modèle Python

Le patron Méthode Modèle est un patron de conception comportemental qui définit le squelette d'un algorithme dans une opération, en reportant certaines étapes aux sous-classes. Il permet aux sous-classes de redéfinir certaines étapes d'un algorithme sans changer la structure de l'algorithme.

En termes simples, c'est comme une recette de cuisine : la recette spécifie l'ordre des étapes (laver les légumes, les couper, les faire sauter, le dressage), mais la façon exacte de laver, couper et faire sauter peut être mise en œuvre par différents chefs selon leur propre style.


Pourquoi avons-nous besoin du patron Méthode Modèle ?

Réutilisation du code

Le patron Méthode Modèle déplace le comportement invariant dans la superclasse, évitant la duplication de code dans les sous-classes. Plusieurs sous-classes peuvent partager la structure d'algorithme définie par la même méthode modèle.

Extensibilité

Les sous-classes peuvent étendre ou modifier une partie du comportement de l'algorithme en redéfinissant des méthodes hook ou des méthodes d'étape concrètes, sans changer la structure globale de l'algorithme.

Inversion de contrôle

La classe parente contrôle le flux d'exécution de l'algorithme, et les sous-classes n'ont qu'à se concentrer sur les étapes spécifiques qu'elles doivent implémenter, ce qui réalise le « principe d'Hollywood » — « Ne nous appelez pas, nous vous appellerons. »


Structure du patron Méthode Modèle

Comprenons la structure du patron de méthode à travers un diagramme de classes :

Description des composants principaux

Composant Responsabilité Description
AbstractClass Définit le squelette de l'algorithme Contient la méthode modèle et les méthodes d'étape abstraites
ConcreteClass Implémente les étapes concrètes Implémente les méthodes abstraites définies par la classe parente
template_method Méthode modèle Définit les parties invariantes de l'algorithme
step1, step2 Méthode abstraite Étapes spécifiques qui doivent être implémentées par les sous-classes
crochet Méthode crochet Étape facultative ; les sous-classes peuvent choisir de la redéfinir ou non

Syntaxe de base et implémentation

Définition de la classe de base abstraite

Exemple

from abc import ABC, abstractmethod

class AbstractClass(ABC):
    """Classe de base abstraite du patron de la méthode de gabarit"""
   
    def template_method(self):
        """Méthode de gabarit - définit le squelette de l'algorithme"""
        self.step1()
        self.step2()
        self.hook()
        self.step3()
   
    @abstractmethod
    def step1(self):
        """Méthode abstraite 1 - doit être implémentée par les sous-classes"""
        pass
   
    @abstractmethod
    def step2(self):
        """Méthode abstraite 2 - doit être implémentée par les sous-classes"""
        pass
   
    def step3(self):
        """Méthode concrète - a une implémentation par défaut"""
        print("Exécuter l'étape 3 - implémentation par défaut")
   
    def hook(self):
        """Méthode hook - étape facultative ; les sous-classes peuvent choisir de la redéfinir"""
        print("Exécuter la méthode hook - par défaut ne fait rien")

Implémentation des sous-classes concrètes

Exemple

class ConcreteClassA(AbstractClass):
    """Classe d'implémentation concrète A"""
   
    def step1(self):
        print("ConcreteClassA - exécution de l'étape 1")
   
    def step2(self):
        print("ConcreteClassA - exécution de l'étape 2")
   
    def hook(self):
        print("ConcreteClassA - redéfinition de la méthode de hook pour ajouter des fonctionnalités supplémentaires")

class ConcreteClassB(AbstractClass):
    """Classe d'implémentation concrète B"""
   
    def step1(self):
        print("ConcreteClassB - exécution de l'étape 1")
   
    def step2(self):
        print("ConcreteClassB - exécution de l'étape 2")
   
    # Ne pas redéfinir la méthode de hook ; utiliser l'implémentation par défaut

Exemples concrets d'application

Comprenons plus en profondeur le patron de méthode de modèle à travers plusieurs exemples pratiques.

Exemple 1 : Modèle de traitement de données

Exemple

from abc import ABC, abstractmethod
import json
import csv

class DataProcessor(ABC):
    """Modèle de traitement de données"""
   
    def process_data(self, input_file, output_file):
        """Méthode modèle de traitement de données"""
        print(f"Début du traitement des données : {input_file} -> {output_file}")
       
        # Lire les données
        data = self.read_data(input_file)
        print(f"Lecture de {len(data)} enregistrements")
       
        # Transformer les données
        transformed_data = self.transform_data(data)
        print("Transformation des données terminée")
       
        # Enregistrer les données
        self.save_data(transformed_data, output_file)
        print("Enregistrement des données terminé")
       
        # Nettoyage
        self.cleanup()
   
    @abstractmethod
    def read_data(self, file_path):
        """Lire les données - méthode abstraite"""
        pass
   
    @abstractmethod
    def transform_data(self, data):
        """Transformer les données - méthode abstraite"""
        pass
   
    def save_data(self, data, file_path):
        """Enregistrer les données - méthode concrète"""
        with open(file_path, 'w', encoding='utf-8') as f:
            if isinstance(data, list):
                for item in data:
                    f.write(str(item) + '\n')
            else:
                f.write(str(data))
   
    def cleanup(self):
        """Nettoyage - méthode de hook"""
        print("Nettoyage terminé")

class JSONProcessor(DataProcessor):
    """Processeur de données JSON"""
   
    def read_data(self, file_path):
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
   
    def transform_data(self, data):
        # Simple transformation : convertir toutes les valeurs de chaîne en majuscules
        if isinstance(data, dict):
            return {k: v.upper() if isinstance(v, str) else v
                   for k, v in data.items()}
        elif isinstance(data, list):
            return [item.upper() if isinstance(item, str) else item
                   for item in data]
        return data

class CSVProcessor(DataProcessor):
    """Processeur de données CSV"""
   
    def read_data(self, file_path):
        data = []
        with open(file_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                data.append(row)
        return data
   
    def transform_data(self, data):
        # Ajouter un horodatage de traitement à chaque enregistrement
        import datetime
        timestamp = datetime.datetime.now().isoformat()
        for record in data:
            record['processed_at'] = timestamp
        return data
   
    def cleanup(self):
        """Redéfinir la méthode cleanup pour ajouter une logique de nettoyage supplémentaire"""
        print("Nettoyage du processeur CSV terminé")
        print("Libérer les ressources de l'analyseur CSV")

# Exemple d'utilisation
if __name__ == "__main__":
    # Créer des données de test
    import os
   
    # Test du processeur JSON
    json_data = {'name': 'john', 'age': 30, 'city': 'new york'}
    with open('test.json', 'w') as f:
        json.dump(json_data, f)
   
    json_processor = JSONProcessor()
    json_processor.process_data('test.json', 'output_json.txt')
   
    print("\n" + "="*50 + "\n")
   
    # Tester le processeur CSV
    csv_data = "nom,âge,ville\njohn,30,new york\njane,25,los angeles"
    with open('test.csv', 'w') as f:
        f.write(csv_data)
   
    csv_processor = CSVProcessor()
    csv_processor.process_data('test.csv', 'output_csv.txt')
   
    # Nettoyer les fichiers de test
    for file in ['test.json', 'test.csv', 'output_json.txt', 'output_csv.txt']:
        if os.path.exists(file):
            os.remove(file)

Exemple 2 : Modèle de préparation de boissons

Exemple

from abc import ABC, abstractmethod

class BeverageMaker(ABC):
    """Modèle de préparation de boissons"""
   
    def make_beverage(self):
        """Méthode modèle pour la préparation de boissons"""
        self.boil_water()
        self.brew()
        self.pour_in_cup()
        if self.customer_wants_condiments():
            self.add_condiments()
        self.serve()
   
    def boil_water(self):
        """Faire bouillir l'eau - méthode concrète"""
        print("Eau bouillante")
   
    @abstractmethod
    def brew(self):
        """Infuser - méthode abstraite"""
        pass
   
    def pour_in_cup(self):
        """Verser dans la tasse - méthode concrète"""
        print("Verser dans la tasse")
   
    @abstractmethod
    def add_condiments(self):
        """Ajouter des condiments - méthode abstraite"""
        pass
   
    def customer_wants_condiments(self):
        """Méthode crochet - indique si le client souhaite des condiments"""
        return True
   
    def serve(self):
        """Servir la boisson - méthode concrète"""
        print("Votre boisson est prête, dégustez !")

class CoffeeMaker(BeverageMaker):
    """Préparation du café"""
   
    def brew(self):
        print("Infuser le café moulu avec de l'eau bouillante")
   
    def add_condiments(self):
        print("Ajouter du sucre et du lait")
   
    def customer_wants_condiments(self):
        answer = input("Voulez-vous du sucre et du lait dans votre café ? (o/n) : ")
        return answer.lower() == 'y'

class TeaMaker(BeverageMaker):
    """Préparation du thé"""
   
    def brew(self):
        print("Infuser les feuilles de thé dans de l'eau bouillante")
   
    def add_condiments(self):
        print("Ajouter du citron")

# Exemple d'utilisation
if __name__ == "__main__":
    print("Préparation du café :")
    coffee = CoffeeMaker()
    coffee.make_beverage()
   
    print("\n" + "="*30 + "\n")
   
    print("Préparation du thé :")
    tea = TeaMaker()
    tea.make_beverage()

Variantes du patron de méthode

1. Patron de méthode avec paramètres

Exemple

class ConfigurableProcessor(ABC):
    """Processeur de données configurable"""
   
    def process_with_config(self, input_file, output_file, config):
        """Méthode modèle avec paramètres de configuration"""
        self.validate_config(config)
        data = self.read_data(input_file)
        processed_data = self.process_with_config_impl(data, config)
        self.save_data(processed_data, output_file)
        self.post_process(config)
   
    def validate_config(self, config):
        """Valider la configuration - méthode concrète"""
        required_keys = ['format', 'encoding']
        for key in required_keys:
            if key not in config:
                raise ValueError(f"Clé requise manquante dans la configuration : {key}")
   
    @abstractmethod
    def process_with_config_impl(self, data, config):
        """Implémentation pour le traitement des données à l'aide de la configuration"""
        pass
   
    def post_process(self, config):
        """Post-traitement - méthode hook"""
        if config.get('cleanup', False):
            print("Exécution des opérations de nettoyage")

2. Méthode modèle multi-étapes

Exemple

class MultiStepProcessor(ABC):
    """Processeur multi-étapes"""
   
    def complex_processing(self):
        """Flux de traitement complexe"""
        self.initialize()
        self.pre_process()
        self.main_process()
        self.post_process()
        self.finalize()
   
    def initialize(self):
        print("Initialisation du processeur")
   
    def pre_process(self):
        """Prétraitement - méthode de raccordement"""
        pass
   
    @abstractmethod
    def main_process(self):
        """Logique de traitement principale"""
        pass
   
    def post_process(self):
        """Post-traitement - méthode hook"""
        pass
   
    def finalize(self):
        print("Traitement terminé")

Bonnes pratiques et considérations

1. Utiliser les méthodes abstraites de manière appropriée

  • Ne déclarez comme méthodes abstraites que les méthodes qui doivent véritablement être implémentées par les sous-classes
  • Fournissez des méthodes de raccordement avec des implémentations par défaut pour les étapes facultatives

2. Contrôle d'accès des méthodes de gabarit

  • Les méthodes de modèle doivent généralement être déclarées commefinal(En Python, cela peut se faire via des conventions de nommage)
  • Les méthodes d'étape doivent être protégées pour éviter les appels externes directs

3. Gestion des erreurs

Exemple

class RobustTemplate(ABC):
    """Classe de modèle robuste"""
   
    def template_method(self):
        try:
            self.setup()
            self.execute_steps()
        except Exception as e:
            self.handle_error(e)
        finally:
            self.cleanup()
   
    def execute_steps(self):
        """Séquence d'étapes d'exécution"""
        self.step1()
        self.step2()
        self.step3()
   
    def handle_error(self, error):
        """Gestion des erreurs - méthode hook"""
        print(f"Une erreur s'est produite lors du traitement : {error}")
   
    @abstractmethod
    def setup(self):
        pass
   
    @abstractmethod
    def step1(self):
        pass
   
    @abstractmethod
    def step2(self):
        pass
   
    @abstractmethod
    def step3(self):
        pass
   
    def cleanup(self):
        print("Nettoyer les ressources")

Exercices pratiques

À vous de vous entraîner ! Réalisez les exercices suivants pour consolider votre compréhension du patron de méthode modèle :

Exercice 1 : Implémenter un modèle d'export de fichier

Créez un modèle d'export de fichier qui prend en charge l'exportation vers différents formats (TXT, HTML, JSON).

Exigences :

  • Définir une classe de base abstraiteFileExporter
  • Implémenter des sous-classes concrètesTXTExporter、HTMLExporter、JSONExporter
  • La méthode modèle doit inclure : la préparation des données, le formatage des données, la sauvegarde des fichiers, le post-traitement et d'autres étapes.

Exercice 2 : Améliorer le modèle de préparation de boissons

Étendre le modèle de préparation de boissons précédent et ajouter les fonctionnalités suivantes :

  • Prise en charge de la sélection de la taille de la boisson (petite, moyenne, grande)
  • Ajouter la fonctionnalité de calcul du prix
  • Prise en charge des condiments personnalisés

Résumé

Le patron Template Method est un patron de conception puissant et pratique qui nous aide à construire un meilleur code des manières suivantes :

Principaux avantages

  1. Améliorer la réutilisabilité du code: Placer le code commun dans la classe parente
  2. Améliorer l'extensibilité: Étendre des étapes spécifiques via des sous-classes
  3. Respecter le principe Open/Closed: Ouvert à l'extension, fermé à la modification
  4. Inversion de contrôle: La classe parente contrôle le flux, et les sous-classes implémentent les détails

Scénarios applicables

  • Plusieurs classes ont la même méthode, mais les implémentations concrètes diffèrent
  • Besoin de contrôler les points d'extension des sous-classes
  • Besoin de définir un squelette d'algorithme, mais permettre à certaines étapes de varier

Points clés

  • La méthode Template définit les parties invariantes de l'algorithme
  • Les méthodes abstraites définissent les parties variables de l'algorithme
  • Les méthodes de raccordement fournissent des points d'extension facultatifs
Autres extensions