Адаптация интерфейсов через множественное наследование и композицию. Facade для упрощения сложных подсистем.
Adapter превращает несовместимое в совместимое. Facade превращает сложное в простое.
Эти два паттерна часто путают, поскольку оба работают с интерфейсами. Однако их цели принципиально различаются:
Оба паттерна следуют принципу «композиция важнее наследования» — они оборачивают объекты, а не наследуются от них.
Adapter — структурный паттерн, позволяющий объектам с несовместимыми интерфейсами работать вместе.
Представьте, что вы разрабатываете приложение, которое использует современный API платежей. Вдруг заказчик говорит: «А можно подключить старую платёжную систему, которую мы использовали 5 лет назад?» Проблема в том, что старая система ожидает данные в другом формате, использует другие имена методов и возвращает другие типы значений.
Вы можете переписать всю старую систему? Нет. Можете ли вы изменить свой код, чтобы он работал со старым интерфейсом? Теоретически да, но это нарушит принцип открытости/закрытости (Open/Closed Principle) — ваш код станет зависеть от конкретной реализации.
Adapter выступает в роли «переводчика» между двумя интерфейсами. Он принимает вызовы в одном формате и преобразует их в другой. Клиентский код даже не подозревает, что работает не с родным объектом, а с адаптером.
# ❌ ПЛОХО: Клиент зависит от конкретного класса
class PaymentGateway:
def process_payment(self, amount: float, card_number: str) -> bool:
# ... обработка ...
return True
# Старая система использует другой интерфейс
class LegacyPaymentProcessor:
def pay(self, money: int, card: str) -> int:
# Возвращает 1 при успехе, 0 при ошибке
# ... обработка ...
return 1
# Клиентский код ожидает PaymentGateway
def checkout(processor: PaymentGateway, amount: float, card: str):
success = processor.process_payment(amount, card)
return success
# LegacyProcessor несовместим!# ✅ ХОРОШО: Adapter делает LegacyProcessor совместимым
class PaymentGateway:
def process_payment(self, amount: float, card_number: str) -> bool:
pass
class LegacyPaymentProcessor:
def pay(self, money: int, card: str) -> int:
return 1 # Успех
class PaymentAdapter(PaymentGateway):
def __init__(self, legacy_processor: LegacyPaymentProcessor):
self._legacy = legacy_processor
def process_payment(self, amount: float, card_number: str) -> bool:
# Адаптация интерфейса
result = self._legacy.pay(int(amount * 100), card_number)
return result == 1
# Использование
legacy = LegacyPaymentProcessor()
adapter = PaymentAdapter(legacy)
# Клиентский код работает с адаптером как с PaymentGateway
success = checkout(adapter, 100.0, "1234-5678")Как это работает:
PaymentAdapter наследуется от PaymentGateway, поэтому клиентский код может работать с ним как с обычным PaymentGateway.LegacyPaymentProcessor и делегирует ему работу.amount (float) → int(amount * 100) для legacy-системы, которая работает с центами.1/0 → True/False для современного кода.Когда использовать наследование: Этот подход подходит, когда у вас есть чёткий базовый класс или интерфейс, и вы хотите, чтобы адаптер был полностью совместим с ним. Однако в Python чаще предпочитают композицию, так как она гибче и не требует наследования.
# ✅ Более гибкий: Adapter без наследования
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float, card: str) -> bool:
...
class StripeProcessor:
def process_payment(self, amount: float, card: str) -> bool:
print(f"Stripe: ${amount}")
return True
class PayPalProcessor:
def charge(self, total: float, email: str) -> bool:
print(f"PayPal: ${total} to {email}")
return True
class PayPalAdapter:
def __init__(self, paypal: PayPalProcessor, email: str):
self._paypal = paypal
self._email = email
def process_payment(self, amount: float, card: str) -> bool:
# card игнорируется, используется email
return self._paypal.charge(amount, self._email)
# Использование
paypal = PayPalProcessor()
adapter = PayPalAdapter(paypal, "user@example.com")
# Работает с любым кодом, ожидающим PaymentProcessor
checkout(adapter, 50.0, "ignored")Почему композиция лучше:
Protocol определяет интерфейс, которому должны следовать классы, но не требует наследования.PayPalAdapter не наследуется от PaymentProcessor, а просто реализует метод с тем же именем.PayPalProcessor и делегирует ему вызовы, преобразуя интерфейс на лету.Преимущества композиции:
В современном Python этот подход считается предпочтительным.
# Адаптация XML-парсера к JSON-интерфейсу
import xml.etree.ElementTree as ET
import json
class JSONParser:
def parse(self, content: str) -> dict:
return json.loads(content)
class XMLParser:
def parse_xml(self, content: str) -> ET.Element:
return ET.fromstring(content)
class XMLToJSONAdapter:
def __init__(self, xml_parser: XMLParser):
self._parser = xml_parser
def parse(self, content: str) -> dict:
# Парсим XML
root = self._parser.parse_xml(content)
# Конвертируем в dict
return self._element_to_dict(root)
def _element_to_dict(self, element: ET.Element) -> dict:
result = {"tag": element.tag}
if element.attrib:
result["attrib"] = element.attrib
if element.text and element.text.strip():
result["text"] = element.text.strip()
if list(element):
result["children"] = [
self._element_to_dict(child) for child in element
]
return result
# Использование
xml_parser = XMLParser()
adapter = XMLToJSONAdapter(xml_parser)
xml_content = "<user name='Alice'><age>30</age></user>"
result = adapter.parse(xml_content)
# {'tag': 'user', 'attrib': {'name': 'Alice'}, 'children': [...]}Типичный сценарий: Часто в проекте требуется работать со сторонними библиотеками, которые не следуют вашему интерфейсу. В примере выше:
parse(), возвращающий dict.parse_xml(), возвращающий ET.Element.Решение: адаптер оборачивает XML-парсер и предоставляет нужный интерфейс:
parse_xml() для получения дерева элементов.Таким образом, клиентский код работает с адаптером как с обычным JSON-парсером, не зная о внутренней сложности.
Facade — структурный паттерн, предоставляющий простой интерфейс к сложной системе классов, библиотеке или фреймворку.
Представьте, что вы работаете с видеоконвертером. Внутри система состоит из десятков компонентов: декодеров, аудио-микшеров, буферов, кодеков. Для конвертации файла нужно вызвать методы в правильном порядке, обработать ошибки, управлять памятью.
Без Facade клиентский код выглядит так:
# Клиент должен знать всё о внутренней устройстве
mixer = AudioMixer()
reader = BitrateReader()
factory = CodecFactory()
decoder = VideoDecoder()
encoder = VideoEncoder()
# 50 строк кода для настройки...
reader.read(file)
decoder.decode(...)
mixer.fix()
# и так далееПроблема: клиентский код зависит от конкретных классов подсистемы. Любое изменение внутри системы сломает клиентский код.
Facade создаёт единый «фасад» над всей подсистемой:
convert(file, codec).AudioMixer, BitrateReader и других.# ❌ ПЛОХО: Клиент знает о всех классах подсистемы
class AudioMixer:
def fix(self): print("Audio fixed")
def reset(self): print("Audio reset")
class BitrateReader:
def read(self, file: str) -> str:
print(f"Reading {file}")
return "data"
def convert(self, data: str) -> str:
print("Converting")
return "converted"
class CodecFactory:
def extract(self, data: str) -> str:
print("Extracting")
return "codec"
class VideoConverter:
def convert_video(self, file: str, codec: str) -> str:
# Клиент должен знать все детали
mixer = AudioMixer()
reader = BitrateReader()
factory = CodecFactory()
reader.read(file)
data = reader.read(file)
factory.extract(data)
mixer.fix()
# ... ещё 20 строк ...
return "result"# ✅ ХОРОШО: Простой интерфейс к сложной системе
class VideoConverterFacade:
def __init__(self):
self.mixer = AudioMixer()
self.reader = BitrateReader()
self.factory = CodecFactory()
def convert(self, file: str, codec: str) -> str:
"""Один метод для всей конвертации"""
# Скрываем сложность внутри
data = self.reader.read(file)
converted = self.reader.convert(data)
codec_data = self.factory.extract(converted)
self.mixer.fix()
self.mixer.reset()
return f"Converted {file} to {codec}"
# Использование
converter = VideoConverterFacade()
result = converter.convert("video.mp4", "mp3")
# Клиент не знает о AudioMixer, BitrateReader, CodecFactoryКак это работает:
VideoConverterFacade инкапсулирует всю сложность подсистемы.convert(), который скрывает внутри себя десятки операций.AudioMixer на другую реализацию, клиентский код не изменится.Важное отличие от Adapter:
import sqlite3
from typing import Any, Dict, List, Optional
class DatabaseFacade:
"""Простой интерфейс к SQLite"""
def __init__(self, db_path: str):
self.db_path = db_path
def _get_connection(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def create_table(self, table: str, columns: Dict[str, str]):
"""Создаёт таблицу"""
cols = ", ".join(f"{name} {type_}" for name, type_ in columns.items())
with self._get_connection() as conn:
conn.execute(f"CREATE TABLE IF NOT EXISTS {table} ({cols})")
def insert(self, table: str, **values) -> int:
"""Вставляет строку, возвращает rowid"""
cols = ", ".join(values.keys())
placeholders = ", ".join("?" for _ in values)
with self._get_connection() as conn:
cursor = conn.execute(
f"INSERT INTO {table} ({cols}) VALUES ({placeholders})",
list(values.values())
)
return cursor.lastrowid
def find_all(self, table: str, **where) -> List[Dict[str, Any]]:
"""Находит все строки с условиями"""
query = f"SELECT * FROM {table}"
params = []
if where:
conditions = " AND ".join(f"{k} = ?" for k in where.keys())
query += f" WHERE {conditions}"
params = list(where.values())
with self._get_connection() as conn:
cursor = conn.execute(query, params)
return [dict(row) for row in cursor.fetchall()]
def find_one(self, table: str, **where) -> Optional[Dict[str, Any]]:
"""Находит одну строку"""
results = self.find_all(table, **where)
return results[0] if results else None
def delete(self, table: str, **where) -> int:
"""Удаляет строки, возвращает количество"""
conditions = " AND ".join(f"{k} = ?" for k in where.keys())
with self._get_connection() as conn:
cursor = conn.execute(
f"DELETE FROM {table} WHERE {conditions}",
list(where.values())
)
return cursor.rowcount
# Использование
db = DatabaseFacade("mydb.sqlite")
# Создание таблицы
db.create_table("users", {"id": "INTEGER PRIMARY KEY", "name": "TEXT", "email": "TEXT"})
# Вставка
user_id = db.insert("users", name="Alice", email="alice@example.com")
# Поиск
user = db.find_one("users", id=user_id)
print(user) # {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
# Удаление
deleted = db.delete("users", id=user_id)Почему это Facade:
DatabaseFacade скрывает всю эту сложность за простыми методами: create_table(), insert(), find_one().sqlite3.Connection или cursor напрямую.Это пример того, как Facade улучшает инкапсуляцию и снижает связанность кода.
| Аспект | Adapter | Facade |
|---|---|---|
| Назначение | Совместимость интерфейсов | Упрощение сложного интерфейса |
| Изменяет интерфейс | Да | Нет (упрощает) |
| Количество адаптируемых классов | Один или два | Много (целая подсистема) |
| Прозрачность | Скрывает несовместимость | Скрывает сложность |
Простое правило для запоминания:
Общее между ними:
from abc import ABC, abstractmethod
from typing import Optional
import redis
import sqlite3
class CacheStorage(ABC):
@abstractmethod
def get(self, key: str) -> Optional[str]:
pass
@abstractmethod
def set(self, key: str, value: str, ttl: int = None):
pass
class RedisStorage(CacheStorage):
def __init__(self, host: str, port: int):
self._redis = redis.Redis(host=host, port=port)
def get(self, key: str) -> Optional[str]:
result = self._redis.get(key)
return result.decode() if result else None
def set(self, key: str, value: str, ttl: int = None):
self._redis.setex(key, ttl or 3600, value)
class SQLiteStorage(CacheStorage):
"""Адаптер SQLite к интерфейсу CacheStorage"""
def __init__(self, db_path: str):
self._conn = sqlite3.connect(db_path)
self._conn.execute("CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, expires REAL)")
def get(self, key: str) -> Optional[str]:
cursor = self._conn.execute(
"SELECT value FROM cache WHERE key = ? AND (expires IS NULL OR expires > ?)",
(key, time.time())
)
row = cursor.fetchone()
return row[0] if row else None
def set(self, key: str, value: str, ttl: int = None):
expires = time.time() + ttl if ttl else None
self._conn.execute(
"INSERT OR REPLACE INTO cache (key, value, expires) VALUES (?, ?, ?)",
(key, value, expires)
)
self._conn.commit()
# Использование — код не зависит от реализации
def cached_operation(cache: CacheStorage, key: str):
cached = cache.get(key)
if cached:
return cached
result = "expensive computation"
cache.set(key, result, ttl=300)
return resultПочему это Adapter:
CacheStorage определяет общий интерфейс для кэширования.RedisStorage реализует интерфейс нативно — Redis уже поддерживает нужные операции.SQLiteStorage — это адаптер: он адаптирует реляционную БД к интерфейсу кэш-хранилища.cached_operation) работает с любым CacheStorage, не зная о реализации.Преимущества:
MemcachedStorage), не меняя клиентский код.import requests
from typing import Optional, Dict, Any
class HTTPClient:
"""Facade для requests"""
def __init__(self, base_url: str, timeout: int = 30):
self.base_url = base_url
self.timeout = timeout
self._session = requests.Session()
def get(self, path: str, params: Dict = None) -> Dict[str, Any]:
response = self._session.get(
f"{self.base_url}/{path}",
params=params,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
def post(self, path: str, json: Dict = None) -> Dict[str, Any]:
response = self._session.post(
f"{self.base_url}/{path}",
json=json,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
def put(self, path: str, json: Dict = None) -> Dict[str, Any]:
response = self._session.put(
f"{self.base_url}/{path}",
json=json,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
def delete(self, path: str) -> None:
response = self._session.delete(
f"{self.base_url}/{path}",
timeout=self.timeout
)
response.raise_for_status()
# Использование
api = HTTPClient("https://api.github.com")
user = api.get("users/xeonov")
print(user["login"]) # xeonovПочему это Facade:
requests мощная, но требует написания шаблонного кода: формирование URL, обработка таймаутов, проверка статус-кодов.HTTPClient упрощает работу:
raise_for_status() вызывается автоматически — не нужно проверять ошибки.Это пример тонкого фасада: он не скрывает сложную подсистему, а просто устраняет дублирование и упрощает типичные сценарии использования.
# io.StringIO — адаптер str к файловому интерфейсу
from io import StringIO
buffer = StringIO()
buffer.write("Hello") # Методы файла для строки!
buffer.seek(0)
content = buffer.read() # "Hello"
# collections.deque — адаптер списка к стеку/очереди
from collections import deque
stack = deque()
stack.append(1) # push
stack.append(2)
top = stack.pop() # popПояснения:
StringIO адаптирует строку к файловому интерфейсу. Вы можете использовать write(), read(), seek() — методы файла — для работы со строкой. Это удобно для тестирования функций, ожидающих файл.deque адаптирует список к интерфейсу стека/очереди. В отличие от обычного списка, deque имеет эффективные операции append() и pop() с обоих концов.# pathlib.Path — фасад для работы с путями
from pathlib import Path
p = Path("/home/user/file.txt")
p.exists() # Вместо os.path.exists()
p.is_file() # Вместо os.path.isfile()
p.read_text() # Вместо open().read()
p.write_text("data") # Вместо open().write()
# json — фасад для сериализации
import json
json.dumps({"key": "value"}) # Скрывает сложность сериализацииПояснения:
pathlib.Path — это фасад над множеством функций из os.path, os.stat, open(). Вместо импорта десяти функций вы работаете с одним объектом.json.dumps() — фасад над сложным алгоритмом сериализации. Вы не думаете о том, как преобразовать dict в JSON-строку — просто вызываете метод.Эти примеры показывают, что паттерны не являются «выдумкой» — они естественно возникают в хорошем коде.
| Паттерн | Когда использовать | Pythonic-реализация |
|---|---|---|
| Adapter | Несовместимые интерфейсы | Композиция + протоколы |
| Facade | Сложная подсистема | Класс-обёртка с простым API |
Главный принцип: Adapter меняет интерфейс, Facade упрощает интерфейс.
Когда применять:
Частые ошибки:
Изучите тему Bridge и Composite для разделения абстракции и реализации и работы с древовидными структурами.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.