Patron Adaptateur en Python

Le patron Adaptateur est un patron de conception structurel qui permet à des interfaces incompatibles de collaborer. Tout comme un adaptateur de prise dans la vie réelle permet à des fiches électriques de différents pays de se connecter aux prises locales, en programmation, le patron Adaptateur agit comme un pont entre deux interfaces incompatibles.

Concepts fondamentaux

Le patron Adaptateur résout principalement le problème d'incompatibilité d'interface. Lorsque nous devons utiliser une classe existante mais que son interface ne répond pas à nos besoins, le patron Adaptateur s'avère utile. Il convertit l'interface en créant une couche intermédiaire (adaptateur), permettant à des classes à l'origine incompatibles de fonctionner ensemble.

Structure du patron

Le patron Adaptateur se compose de trois rôles principaux :

  • Interface cible (Target): L'interface attendue par le client
  • Adaptateur (Adapter): implémente l'interface cible et encapsule l'adapté
  • Adapté (Adaptee): La classe existante qui doit être adaptée

Pourquoi avons-nous besoin du patron Adaptateur ?

Scénario de problème concret

Imaginez que vous avez un système de journalisation existant qui utilise une méthode spécifique pour enregistrer les journaux :

Exemple

class LegacyLogger:
    def write_log(self, message):
        print(f"[LEGACY] {message}")

Mais maintenant, vous voulez utiliser une nouvelle interface de journalisation :

Exemple

class NewLogger:
    def log(self, level, message):
        print(f"[{level.upper()}] {message}")

Sans adaptateur, vous devriez modifier tout le code qui appelle l'ancien système de journalisation, ce qui prend du temps et est source d'erreurs.

Avantages de l'adaptateur

  1. Réutilisation du code: Réutiliser les classes existantes sans modifier le code source
  2. Découplage: Séparer la logique de conversion d'interface de la logique métier
  3. Flexibilité: Basculer facilement entre différentes implémentations
  4. Respecte le principe ouvert/fermé: Ouvert à l'extension, fermé à la modification

Approches d'implémentation du patron Adaptateur

Adaptateur de classe (utilisant l'héritage)

L'adaptateur de classe implémente l'adaptation d'interface par héritage multiple :

Exemple

class LegacyLogger:
    """Classe de journal héritée en cours d'adaptation"""
    def write_log(self, message):
        print(f"[LEGACY] {message}")

class NewLoggerInterface:
    """Interface cible"""
    def log(self, level, message):
        pass

class LoggerAdapter(NewLoggerInterface, LegacyLogger):
    """Adaptateur de classe - implémenté par héritage"""
    def log(self, level, message):
        # Convertir la nouvelle interface en l'ancienne interface
        formatted_message = f"{level}: {message}"
        self.write_log(formatted_message)

# Exemple d'utilisation
logger = LoggerAdapter()
logger.log("INFO", "Système démarré avec succès")
logger.log("ERROR", "Échec de la connexion à la base de données")

Sortie :

[LEGACY] INFO: 系统启动成功
[LEGACY] ERROR: 数据库连接失败

Adaptateur d'objet (utilisant la composition)

L'adaptateur d'objet est implémenté par composition, ce qui est l'approche la plus courante :

Exemple

class LegacyPayment:
    """Système de paiement hérité en cours d'adaptation"""
    def process_payment(self, amount_in_dollars):
        print(f"Traitement du paiement : ${amount_in_dollars}")
        return True

class NewPaymentInterface:
    """Nouvelle interface de paiement"""
    def pay(self, amount, currency="CNY"):
        pass

class PaymentAdapter(NewPaymentInterface):
    """Adaptateur d'objet - implémenté par composition"""
   
    def __init__(self, legacy_payment):
        self.legacy_payment = legacy_payment
   
    def pay(self, amount, currency="CNY"):
        # Logique de conversion des devises
        if currency == "CNY":
            amount_in_dollars = amount / 7.0  # Conversion simplifiée du taux de change
        else:
            amount_in_dollars = amount
       
        # Appeler l'interface de l'ancien système
        return self.legacy_payment.process_payment(amount_in_dollars)

# Exemple d'utilisation
legacy_payment = LegacyPayment()
payment = PaymentAdapter(legacy_payment)

# Utiliser la nouvelle interface pour appeler l'ancien système
payment.pay(700, "CNY")  # Payer 700 RMB
payment.pay(100, "USD")  # Payer 100 USD

Sortie :

处理支付: $100.0
处理支付: $100

Cas d'application réels

Cas 1 : Adaptateur de format de données

Exemple

import json

class XMLData:
    """Source de données XML"""
    def get_xml_data(self):
        return """
        <users>
            <user>
<name>Zhang San</name>
                <age>25</age>
            </user>
            <user>
<name>Li Si</name>
                <age>30</age>
            </user>
        </users>
        """


class JSONProcessor:
    """Processeur JSON (attend le format JSON)"""
    def process_json(self, json_data):
        data = json.loads(json_data)
        for user in data['users']:
            print(f"Nom : {user['name']}, Âge : {user['age']}")

class XMLToJSONAdapter:
    """Adaptateur XML vers JSON"""
   
    def __init__(self, xml_data):
        self.xml_data = xml_data
   
    def convert_to_json(self):
        # Conversion simplifiée de XML en JSON
        xml_content = self.xml_data.get_xml_data()
        # Dans des applications réelles, vous devriez utiliser des bibliothèques telles que xml.etree.ElementTree pour l'analyse
        json_data = {
            "users": [
                {"name": "Zhang San", "age": 25},
                {"name": "Li Si", "age": 30}
            ]
        }
        return json.dumps(json_data, ensure_ascii=False)

# Utiliser l'adaptateur
xml_source = XMLData()
adapter = XMLToJSONAdapter(xml_source)
json_processor = JSONProcessor()

json_data = adapter.convert_to_json()
json_processor.process_json(json_data)

Sortie :

姓名: 张三, 年龄: 25
姓名: 李四, 年龄: 30

Cas 2 : Adaptateur de service tiers

Exemple

class WeChatPay:
    """WeChat Pay SDK"""
    def wechat_pay(self, amount, user_id):
        print(f"WeChat Pay : l'utilisateur {user_id} a payé {amount} yuan")
        return {"status": "success", "transaction_id": "wx_123456"}

class AliPay:
    """SDK Alipay"""
    def alipay(self, money, user_account):
        print(f"Alipay : le compte {user_account} a payé {money} yuan")
        return {"code": 200, "trade_no": "ali_789012"}

class UnifiedPaymentAdapter:
    """Adaptateur de paiement unifié"""
   
    def __init__(self, payment_service):
        self.payment_service = payment_service
   
    def pay(self, amount, user_info):
        # Interface de paiement unifiée
        if isinstance(self.payment_service, WeChatPay):
            return self.payment_service.wechat_pay(amount, user_info)
        elif isinstance(self.payment_service, AliPay):
            return self.payment_service.alipay(amount, user_info)
        else:
            raise ValueError("Méthode de paiement non prise en charge")

# Le code client peut effectuer des appels unifiés
def process_order(payment_adapter, amount, user_info):
    """Traiter le paiement de la commande"""
    result = payment_adapter.pay(amount, user_info)
    print(f"Résultat du paiement : {result}")
    return result

# Exemple d'utilisation
wechat_pay = WeChatPay()
ali_pay = AliPay()

wechat_adapter = UnifiedPaymentAdapter(wechat_pay)
ali_adapter = UnifiedPaymentAdapter(ali_pay)

# Approche d'appel unifiée
process_order(wechat_adapter, 100, "user_001")
process_order(ali_adapter, 200, "user_002")

Sortie :

微信支付: 用户user_001支付100元
支付结果: {'status': 'success', 'transaction_id': 'wx_123456'}
支付宝: 账户user_002支付200元
支付结果: {'code': 200, 'trade_no': 'ali_789012'}

Bonnes pratiques pour le patron de conception Adaptateur

1. Choisir la bonne approche d'implémentation

Approche d'implémentation Scénario applicable Avantages Inconvénients
Adaptateur de classe Peu de classes adaptées et une interface cible simple Implémentation simple, moins de code Nécessite l'héritage multiple, peut compliquer la hiérarchie de classes
Adaptateur d'objet La plupart des scénarios, en particulier lorsqu'il est nécessaire d'adapter plusieurs classes Plus flexible, suit le principe de composition plutôt que d'héritage Nécessite la création d'objets adaptateurs

2. Conserver la responsabilité unique de l'adaptateur

Exemple

# Bonne pratique : responsabilité unique
class DataFormatAdapter:
    """Uniquement responsable de la conversion de format de données"""
    def convert_format(self, data):
        pass

# Mauvaise pratique : trop de responsabilités
class BadAdapter:
    """Gère la conversion de format, la validation, la mise en cache, etc."""
    def convert_format(self, data):
        pass
   
    def validate_data(self, data):
        pass
   
    def cache_data(self, data):
        pass

3. Utiliser l'abstraction d'interface

Exemple

from abc import ABC, abstractmethod

class PaymentInterface(ABC):
    """Abstraction de l'interface de paiement"""
    @abstractmethod
    def pay(self, amount, user_info):
        pass

class LoggerInterface(ABC):
    """Abstraction de l'interface de journalisation"""
    @abstractmethod
    def log(self, level, message):
        pass

Problèmes courants et solutions

Problème 1 : Trop d'adaptateurs augmentent la complexité

Solution: Utiliser le patron Factory pour créer des adaptateurs

Exemple

class AdapterFactory:
    """Fabrique d'adaptateurs"""
   
    @staticmethod
    def create_payment_adapter(service_type):
        if service_type == "wechat":
            return UnifiedPaymentAdapter(WeChatPay())
        elif service_type == "alipay":
            return UnifiedPaymentAdapter(AliPay())
        else:
            raise ValueError(f"Type de paiement non pris en charge : {service_type}")

# Utiliser la fabrique pour créer des adaptateurs
wechat_adapter = AdapterFactory.create_payment_adapter("wechat")
ali_adapter = AdapterFactory.create_payment_adapter("alipay")

Problème 2 : Considérations de performance

Solution: Implémenter le chargement paresseux et la mise en cache

Exemple

class CachingAdapter:
    """Adaptateur avec mise en cache"""
   
    def __init__(self, adaptee):
        self.adaptee = adaptee
        self._cache = {}
   
    def process_data(self, key):
        if key not in self._cache:
            # Simuler une opération chronophage
            result = self.adaptee.expensive_operation(key)
            self._cache[key] = result
        return self._cache[key]

Exercices pratiques

Exercice 1 : Créer un adaptateur de format de fichier

Implémenter un adaptateur de format de fichier qui convertit des données au format CSV vers le format JSON :

Exemple

class CSVReader:
    """Lecteur de fichier CSV"""
    def read_csv(self, filepath):
        # Simuler la lecture d'un fichier CSV
        return [
            ["nom", "âge", "ville"],
            ["Alice", "25", "Pékin"],
            ["Bob", "30", "Shanghai"]
        ]

class JSONExporter:
    """Exportateur JSON"""
    def export_json(self, data):
        import json
        print("Exporter les données JSON :")
        print(json.dumps(data, indent=2, ensure_ascii=False))

# Veuillez implémenter la classe CSVToJSONAdapter ici
# Votre code...

# Code de test
csv_reader = CSVReader()
json_exporter = JSONExporter()

# Créez l'adaptateur et testez
adapter = CSVToJSONAdapter(csv_reader)
json_data = adapter.convert_to_json()
json_exporter.export_json(json_data)

Exercice 2 : Design Thinking

Considérez le scénario suivant : Comment utiliseriez-vous le patron Adaptateur ?

  1. Votre application doit prendre en charge plusieurs bases de données (MySQL, PostgreSQL, SQLite), mais chaque base de données a une méthode de connexion différente.
  2. Vous devez intégrer plusieurs API tierces, mais leurs méthodes d’authentification et leurs formats de retour sont tous différents.
  3. Votre système doit prendre en charge plusieurs files de messages (RabbitMQ, Kafka, Redis).

Veuillez concevoir une solution basée sur le patron Adaptateur pour l’un de ces scénarios.


Résumé

Le patron Adaptateur est un outil efficace pour résoudre les problèmes d’incompatibilité d’interface. À travers cet article, vous devriez maîtriser :

  • Les concepts de base et les scénarios d’application du patron Adaptateur.
  • Les approches d’implémentation des adaptateurs de classe et des adaptateurs d’objet.
  • Les techniques d’application dans des projets réels.
  • Les bonnes pratiques et précautions d’emploi du patron Adaptateur.

Rappelez-vous, l’idée centrale du patron Adaptateur est la « conversion » plutôt que la « modification ». Il nous aide à intégrer de nouvelles fonctionnalités sans modifier le code existant, et c’est un patron de conception important pour la maintenance de grands systèmes.

Dans le développement réel, utiliser le patron Adaptateur de manière appropriée peut rendre votre code plus flexible, plus maintenable et plus facile à étendre.

Autres extensions