Adapter
Adapter позволяет взаимодействовать двум изначально несовместимым интерефейсам программы. Его главная задача - реализация нужного интерфейса и передача его вызовов адаптируемому объекту. По этой причине Адаптер часто называют "переходником" или "оберткой".
Является паттерном структурного типа.
Для чего используется?
Нередко возникает ситуация, когда имеющийся функционал или код нужно использовать повторно, но это нельзя сделать из-за различий в интерфейсах. Использование данного паттерна позволяет изменить интерфейс нужного объекта так, чтобы он стал понятен другому объекту.
Пример - приложение может использовать похожие, но различные API для реализации финансовых расчетов - калькулятор налогов, анализ денежных потоков и т.д. Функционал данных модулей реализуется через сторонние библиотеки, код которых нельзя модифицировать. Изменения в функционал этих API можно внести в рамках объекта-адаптера, в то время как приложение будет использовать только свой "родной" интерфейс.
Когда удобно применять:
- Код приложения уже написан, и внесение масштабных изменений в логику основного приложения для реализации конкретного функционала нецелесообразно;
- Требуется использовать сторонний класс, интерфейс которого несовместим с приложением;
- Требуется расширить функционал подклассов приложения.
Варианты кейсов использования:
- Использование сторонних библиотек, формат данных которых несовместим с приложением;
- Адаптация поведения - например, преобразование ошибок в вызовы исключений;
- Повторное использование кода из других приложений, имеющих схожий функционал, но иной формат данных.
Какова механика реализации?
Через целевой интерфейс вызывается метод объекта-адаптера. Адаптер, в свою очередь, преобразует запрос в тот формат, который понятен адаптируемому объекту - например, функционалу сторонней библиотеки.
На основе этого запроса адаптированный объект выполняет требуемый функционал. Клиент получает результаты вызова, не зная о наличии адаптера.
Структура и отношения
Target
└── request(): string
Adaptee
└── specificRequest(): string
Adapter
├── adaptee: Adaptee
└── request(): string
Client
└── clientCode(target: Target): void
Пример реализации
// Целевой интерфейс (тот, который ожидает клиент)
class PaymentProcessor {
processPayment(amount, currency) {
throw new Error('Метод processPayment должен быть переопределен');
}
}
// Существующий класс с несовместимым интерфейсом
class LegacyPaymentSystem {
pay(amountInCents, currencyCode) {
console.log(`Обработка платежа: ${amountInCents} центов в ${currencyCode}`);
return {
success: true,
transactionId: `legacy_${Date.now()}`,
amount: amountInCents,
currency: currencyCode
};
}
}
// Адаптер
class LegacyPaymentAdapter extends PaymentProcessor {
constructor(legacySystem) {
super();
this.legacySystem = legacySystem;
}
processPayment(amount, currency) {
// Конвертируем доллары в центы
const amountInCents = Math.round(amount * 100);
// Конвертируем название валюты в код
const currencyCode = this.getCurrencyCode(currency);
// Вызываем метод старой системы
const result = this.legacySystem.pay(amountInCents, currencyCode);
// Возвращаем результат в новом формате
return {
success: result.success,
transactionId: result.transactionId,
amount: result.amount / 100, // Конвертируем обратно в доллары
currency: this.getCurrencyName(result.currency)
};
}
getCurrencyCode(currency) {
const codes = {
'USD': 'USD',
'EUR': 'EUR',
'RUB': 'RUB'
};
return codes[currency] || 'USD';
}
getCurrencyName(code) {
const names = {
'USD': 'USD',
'EUR': 'EUR',
'RUB': 'RUB'
};
return names[code] || 'USD';
}
}
// Современная система платежей (совместима с целевым интерфейсом)
class ModernPaymentSystem extends PaymentProcessor {
processPayment(amount, currency) {
console.log(`Современная обработка платежа: ${amount} ${currency}`);
return {
success: true,
transactionId: `modern_${Date.now()}`,
amount: amount,
currency: currency
};
}
}
// Клиентский код
function processPayment(processor, amount, currency) {
const result = processor.processPayment(amount, currency);
console.log(`Результат: ${JSON.stringify(result)}`);
return result;
}
// Использование
const legacySystem = new LegacyPaymentSystem();
const legacyAdapter = new LegacyPaymentAdapter(legacySystem);
const modernSystem = new ModernPaymentSystem();
// Оба работают с одинаковым интерфейсом
processPayment(legacyAdapter, 100.50, 'USD');
processPayment(modernSystem, 100.50, 'USD');
# Целевой интерфейс (тот, который ожидает клиент)
class PaymentProcessor:
def process_payment(self, amount, currency):
raise NotImplementedError('Метод process_payment должен быть переопределен')
# Существующий класс с несовместимым интерфейсом
class LegacyPaymentSystem:
def pay(self, amount_in_cents, currency_code):
print(f"Обработка платежа: {amount_in_cents} центов в {currency_code}")
return {
'success': True,
'transactionId': f"legacy_{int(time.time() * 1000)}",
'amount': amount_in_cents,
'currency': currency_code
}
# Адаптер
class LegacyPaymentAdapter(PaymentProcessor):
def __init__(self, legacy_system):
super().__init__()
self.legacy_system = legacy_system
def process_payment(self, amount, currency):
# Конвертируем доллары в центы
amount_in_cents = int(round(amount * 100))
# Конвертируем название валюты в код
currency_code = self.get_currency_code(currency)
# Вызываем метод старой системы
result = self.legacy_system.pay(amount_in_cents, currency_code)
# Возвращаем результат в новом формате
return {
'success': result['success'],
'transactionId': result['transactionId'],
'amount': result['amount'] / 100, # Конвертируем обратно в доллары
'currency': self.get_currency_name(result['currency'])
}
def get_currency_code(self, currency):
codes = {
'USD': 'USD',
'EUR': 'EUR',
'RUB': 'RUB'
}
return codes.get(currency, 'USD')
def get_currency_name(self, code):
names = {
'USD': 'USD',
'EUR': 'EUR',
'RUB': 'RUB'
}
return names.get(code, 'USD')
# Современная система платежей (совместима с целевым интерфейсом)
class ModernPaymentSystem(PaymentProcessor):
def process_payment(self, amount, currency):
print(f"Современная обработка платежа: {amount} {currency}")
return {
'success': True,
'transactionId': f"modern_{int(time.time() * 1000)}",
'amount': amount,
'currency': currency
}
# Клиентский код
def process_payment(processor, amount, currency):
result = processor.process_payment(amount, currency)
print(f"Результат: {result}")
return result
# Использование
import time
legacy_system = LegacyPaymentSystem()
legacy_adapter = LegacyPaymentAdapter(legacy_system)
modern_system = ModernPaymentSystem()
# Оба работают с одинаковым интерфейсом
process_payment(legacy_adapter, 100.50, 'USD')
process_payment(modern_system, 100.50, 'USD')
// Интерфейс целевого класса
interface MediaPlayer {
play(audioType: string, fileName: string): void;
}
// Интерфейс адаптируемого класса
interface AdvancedMediaPlayer {
playVlc(fileName: string): void;
playMp4(fileName: string): void;
}
// Конкретные классы адаптируемых плееров
class VlcPlayer implements AdvancedMediaPlayer {
playVlc(fileName: string): void {
console.log(`Воспроизведение VLC файла: ${fileName}`);
}
playMp4(fileName: string): void {
// VLC плеер не поддерживает MP4
}
}
class Mp4Player implements AdvancedMediaPlayer {
playVlc(fileName: string): void {
// MP4 плеер не поддерживает VLC
}
playMp4(fileName: string): void {
console.log(`Воспроизведение MP4 файла: ${fileName}`);
}
}
// Адаптер
class MediaAdapter implements MediaPlayer {
private advancedPlayer: AdvancedMediaPlayer;
constructor(audioType: string) {
if (audioType.toLowerCase() === 'vlc') {
this.advancedPlayer = new VlcPlayer();
} else if (audioType.toLowerCase() === 'mp4') {
this.advancedPlayer = new Mp4Player();
}
}
play(audioType: string, fileName: string): void {
if (audioType.toLowerCase() === 'vlc') {
this.advancedPlayer.playVlc(fileName);
} else if (audioType.toLowerCase() === 'mp4') {
this.advancedPlayer.playMp4(fileName);
}
}
}
// Основной плеер
class AudioPlayer implements MediaPlayer {
private mediaAdapter: MediaAdapter;
play(audioType: string, fileName: string): void {
// Встроенная поддержка MP3
if (audioType.toLowerCase() === 'mp3') {
console.log(`Воспроизведение MP3 файла: ${fileName}`);
}
// Используем адаптер для других форматов
else if (audioType.toLowerCase() === 'vlc' || audioType.toLowerCase() === 'mp4') {
this.mediaAdapter = new MediaAdapter(audioType);
this.mediaAdapter.play(audioType, fileName);
}
else {
console.log(`Неподдерживаемый формат: ${audioType}`);
}
}
}
// Использование
const audioPlayer = new AudioPlayer();
audioPlayer.play('mp3', 'song.mp3');
audioPlayer.play('mp4', 'movie.mp4');
audioPlayer.play('vlc', 'video.vlc');
audioPlayer.play('avi', 'video.avi');
Преимущества
- Логика приложения остается сфокусированной - код конкретного функционала реализуется внутри адаптера;
- Обеспечение поддержки различных интерфейсов и типов данных;
- Позволяет использовать код разных приложений повторно.
Недостатки
- Есть риск излишнего использования адаптеров для реализация мелкого функционала;
- Введение дополнительных классов усложняет код программы.
Отличия от других паттернов
- Decorator — похож на Адаптер, но Декоратор напрямую расширяет функционал класса и предоставляет такой же интерфейс. Адаптер, в свою очередь, предоставляет другой интерфейс.
- Proxy — в отличие от Адаптера, не изменяет интерфейс или поведение оригинального объекта, но контролирует доступ к нему.
Реальный пример
// Прототип для создания пользовательских интерфейсов
class UIComponent {
constructor(type, props = {}) {
this.type = type;
this.props = { ...props };
this.children = [];
}
clone() {
const cloned = new UIComponent(this.type, this.props);
cloned.children = [...this.children];
return cloned;
}
addChild(child) {
this.children.push(child);
return this;
}
render() {
return `<${this.type} ${Object.entries(this.props).map(([k, v]) => `${k}="${v}"`).join(' ')}>${this.children.map(child => child.render()).join('')}</${this.type}>`;
}
}
// Создаем прототипы компонентов
const buttonPrototype = new UIComponent('button', { class: 'btn', type: 'button' });
const inputPrototype = new UIComponent('input', { type: 'text', class: 'form-control' });
const divPrototype = new UIComponent('div', { class: 'container' });
// Создаем копии и настраиваем их
const submitButton = buttonPrototype.clone();
submitButton.props.value = 'Отправить';
submitButton.props.class = 'btn btn-primary';
const cancelButton = buttonPrototype.clone();
cancelButton.props.value = 'Отмена';
cancelButton.props.class = 'btn btn-secondary';
const form = divPrototype.clone();
form.props.class = 'form-group';
const nameInput = inputPrototype.clone();
nameInput.props.placeholder = 'Введите имя';
nameInput.props.name = 'name';
const emailInput = inputPrototype.clone();
emailInput.props.placeholder = 'Введите email';
emailInput.props.name = 'email';
emailInput.props.type = 'email';
// Собираем форму
form.addChild(nameInput);
form.addChild(emailInput);
form.addChild(submitButton);
form.addChild(cancelButton);
console.log(form.render());
Adapter — это мост между несовместимыми интерфейсами!