Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Adapter и Facade
adapter_facade

Adapter и Facade

Адаптация интерфейсов через множественное наследование и композицию. Facade для упрощения сложных подсистем.

Adapter и Facade

Adapter превращает несовместимое в совместимое. Facade превращает сложное в простое.

Эти два паттерна часто путают, поскольку оба работают с интерфейсами. Однако их цели принципиально различаются:

  • Adapter решает проблему несовместимости — когда у вас есть готовый класс, но его интерфейс не подходит для вашего кода.
  • Facade решает проблему сложности — когда подсистема состоит из множества классов и требует знания деталей для работы с ней.

Оба паттерна следуют принципу «композиция важнее наследования» — они оборачивают объекты, а не наследуются от них.


#Adapter: совместимость интерфейсов

Adapter — структурный паттерн, позволяющий объектам с несовместимыми интерфейсами работать вместе.

#Идея паттерна

Представьте, что вы разрабатываете приложение, которое использует современный API платежей. Вдруг заказчик говорит: «А можно подключить старую платёжную систему, которую мы использовали 5 лет назад?» Проблема в том, что старая система ожидает данные в другом формате, использует другие имена методов и возвращает другие типы значений.

Вы можете переписать всю старую систему? Нет. Можете ли вы изменить свой код, чтобы он работал со старым интерфейсом? Теоретически да, но это нарушит принцип открытости/закрытости (Open/Closed Principle) — ваш код станет зависеть от конкретной реализации.

Adapter выступает в роли «переводчика» между двумя интерфейсами. Он принимает вызовы в одном формате и преобразует их в другой. Клиентский код даже не подозревает, что работает не с родным объектом, а с адаптером.

#Ключевые принципы Adapter

  1. Один отвечает за интерфейс, другой — за реализацию. У вас есть целевой интерфейс (который ожидает клиент) и адаптируемый класс (который уже существует).
  2. Адаптер не меняет логику. Он только преобразует вызовы: имена методов, параметры, типы возвращаемых значений.
  3. Композиция предпочтительнее наследования. В Python чаще используют композицию (адаптер хранит ссылку на адаптируемый объект), так как это гибче.

#Проблема: несовместимый интерфейс

# ❌ ПЛОХО: Клиент зависит от конкретного класса 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 несовместим!

#Решение 1: Adapter через наследование

# ✅ ХОРОШО: 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 чаще предпочитают композицию, так как она гибче и не требует наследования.

#Решение 2: Adapter через композицию

# ✅ Более гибкий: 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, а просто реализует метод с тем же именем.
  • Это называется утиная типизация (duck typing): если объект «крякает как утка», он считается уткой.
  • Адаптер хранит ссылку на PayPalProcessor и делегирует ему вызовы, преобразуя интерфейс на лету.

Преимущества композиции:

  1. Гибкость: можно адаптировать несколько классов одновременно.
  2. Тестируемость: легко подменить адаптируемый объект моком.
  3. Нет жёсткой связи: адаптер не зависит от иерархии наследования.

В современном Python этот подход считается предпочтительным.

#Решение 3: Adapter для сторонних библиотек

# Адаптация 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.
  • Сторонняя библиотека предоставляет XML-парсер с методом parse_xml(), возвращающий ET.Element.

Решение: адаптер оборачивает XML-парсер и предоставляет нужный интерфейс:

  1. Принимает XML-строку.
  2. Вызывает parse_xml() для получения дерева элементов.
  3. Рекурсивно преобразует дерево в словарь.

Таким образом, клиентский код работает с адаптером как с обычным JSON-парсером, не зная о внутренней сложности.


#Facade: упрощение сложного

Facade — структурный паттерн, предоставляющий простой интерфейс к сложной системе классов, библиотеке или фреймворку.

#Идея паттерна

Представьте, что вы работаете с видеоконвертером. Внутри система состоит из десятков компонентов: декодеров, аудио-микшеров, буферов, кодеков. Для конвертации файла нужно вызвать методы в правильном порядке, обработать ошибки, управлять памятью.

Без Facade клиентский код выглядит так:

# Клиент должен знать всё о внутренней устройстве mixer = AudioMixer() reader = BitrateReader() factory = CodecFactory() decoder = VideoDecoder() encoder = VideoEncoder() # 50 строк кода для настройки... reader.read(file) decoder.decode(...) mixer.fix() # и так далее

Проблема: клиентский код зависит от конкретных классов подсистемы. Любое изменение внутри системы сломает клиентский код.

Facade создаёт единый «фасад» над всей подсистемой:

  • Клиент вызывает один метод convert(file, codec).
  • Facade знает, в каком порядке вызывать компоненты подсистемы.
  • Клиент не знает о существовании AudioMixer, BitrateReader и других.

#Преимущества Facade

  1. Изоляция: изменения внутри подсистемы не затрагивают клиентский код.
  2. Простота: клиент работает с одним классом вместо десяти.
  3. Снижение связанности: подсистема становится независимой от клиентов.

#Проблема: сложная подсистема

# ❌ ПЛОХО: Клиент знает о всех классах подсистемы 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"

#Решение: Facade

# ✅ ХОРОШО: Простой интерфейс к сложной системе 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:

  • Adapter меняет интерфейс одного или нескольких объектов.
  • Facade не меняет существующие интерфейсы — он создаёт новый, более простой интерфейс над целой подсистемой.

#Facade для работы с БД

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:

  • SQLite требует работы с соединениями, курсорами, SQL-запросами.
  • DatabaseFacade скрывает всю эту сложность за простыми методами: create_table(), insert(), find_one().
  • Клиентский код не работает с sqlite3.Connection или cursor напрямую.
  • Если вы решите сменить SQLite на PostgreSQL, клиентский код не изменится — нужно будет поменять только реализацию Facade.

Это пример того, как Facade улучшает инкапсуляцию и снижает связанность кода.


#Adapter vs Facade

АспектAdapterFacade
НазначениеСовместимость интерфейсовУпрощение сложного интерфейса
Изменяет интерфейсДаНет (упрощает)
Количество адаптируемых классовОдин или дваМного (целая подсистема)
ПрозрачностьСкрывает несовместимостьСкрывает сложность

Простое правило для запоминания:

  • Adapter — это «переводчик». У вас есть два объекта с разными интерфейсами, и вы хотите заставить их работать вместе.
  • Facade — это «пульт управления». У вас есть сложная система с кучей кнопок, и вы хотите сделать одну большую красную кнопку «Сделать хорошо».

Общее между ними:

  • Оба используют композицию: хранят ссылку на объект(ы), которые оборачивают.
  • Оба скрывают детали: клиент не знает о внутренней сложности.
  • Оба следуют принципу Single Responsibility: один класс — одна задача.

#Практическое применение

#Adapter для разных хранилищ

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, не зная о реализации.

Преимущества:

  • Можно легко переключаться между Redis и SQLite.
  • Можно добавить новый бэкенд (например, MemcachedStorage), не меняя клиентский код.
  • Это пример Dependency Inversion Principle: код зависит от абстракции, а не от реализации.

#Facade для HTTP-клиента

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 упрощает работу:
    • Базовый URL задаётся один раз при создании.
    • Таймаут настраивается централизованно.
    • raise_for_status() вызывается автоматически — не нужно проверять ошибки.
    • Ответ сразу парсится как JSON.
  • Клиентский код становится короче и понятнее.

Это пример тонкого фасада: он не скрывает сложную подсистему, а просто устраняет дублирование и упрощает типичные сценарии использования.


#Python в стандартной библиотеке

#Adapter в action

# 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() с обоих концов.

#Facade в action

# 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 упрощает интерфейс.

Когда применять:

  • Adapter: у вас есть готовый класс, но его интерфейс не подходит вашему коду.
  • Facade: вы работаете со сложной библиотекой/фреймворком и хотите упростить API для клиентов.

Частые ошибки:

  • Использовать Adapter там, где проще изменить интерфейс клиента.
  • Создавать Facade prematurely, когда подсистема ещё не стабилизировалась.
  • Путать Adapter и Facade: помните, Adapter — про совместимость, Facade — про простоту.

#Что дальше?

Изучите тему Bridge и Composite для разделения абстракции и реализации и работы с древовидными структурами.

Далее: Bridge и Composite