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
def write_log(self, message):
print(f"[LEGACY] {message}")
Mais maintenant, vous voulez utiliser une nouvelle interface de journalisation :
Exemple
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
- Réutilisation du code: Réutiliser les classes existantes sans modifier le code source
- Découplage: Séparer la logique de conversion d'interface de la logique métier
- Flexibilité: Basculer facilement entre différentes implémentations
- 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
"""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
"""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
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
"""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
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
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
"""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
"""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
"""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 ?
- 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.
- Vous devez intégrer plusieurs API tierces, mais leurs méthodes d’authentification et leurs formats de retour sont tous différents.
- 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