Паттерн Адаптер
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 97 из 104.
Паттерн Адаптер (Adapter pattern) позволяет несовместимым интерфейсам работать вместе. Он выступает в качестве моста между двумя классами, преобразуя интерфейс одного класса в интерфейс, который ожидают клиенты. Это особенно полезно при интеграции устаревшего кода или сторонних библиотек, которые не соответствуют интерфейсу вашей системы.
Представьте, что у вас есть существующий класс, который делает то, что вам нужно, но его интерфейс не соответствует тому, что ожидает ваш код. Вместо того чтобы изменять какую-либо из сторон, вы создаете адаптер, который оборачивает существующий класс и транслирует вызовы:
#include <iostream>
#include <string>
// Существующий класс с несовместимым интерфейсом
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// Целевой интерфейс, который ожидает клиент
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// Адаптер оборачивает LegacyPrinter и реализует интерфейс Printer
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // Трансляция вызова
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// Клиент использует ожидаемый интерфейс
adapter.print("Hello World");
}Адаптер хранит ссылку на LegacyPrinter и реализует интерфейс Printer. При вызове print() он делегирует выполнение методу printMessage(). Клиентский код работает с интерфейсом Printer, не зная о лежащей в основе устаревшей реализации.
Используйте Адаптер, когда вы хотите использовать существующий класс, но его интерфейс не соответствует вашим потребностям, или когда вам нужно создать повторно используемый класс, который взаимодействует с классами, имеющими несовместимые интерфейсы.
Задание
ЛегкоДавайте создадим систему Media Player Adapter, которая связывает различные интерфейсы аудиобиблиотек. Представьте, что вы интегрируете устаревшую аудиобиблиотеку в современное приложение медиаплеера — старая библиотека работает отлично, но её интерфейс не соответствует тому, что ожидает ваш плеер. Это именно та ситуация, где паттерн Адаптер (Adapter) проявляет себя наилучшим образом.
Вы организуете свой код в четырех файлах:
MediaPlayer.h: Определите целевой интерфейс, который ожидает ваш современный медиаплеер.Создайте абстрактный класс
MediaPlayerсо следующими чисто виртуальными методами:play(const std::string& filename)— воспроизводит указанный файлstop()— останавливает воспроизведениеsetVolume(int level)— устанавливает громкость (0-100)
Включите виртуальный деструктор.
LegacyAudioLib.h: Создайте существующую «устаревшую» аудиобиблиотеку с несовместимым интерфейсом.Ваш класс
LegacyAudioLibпредставляет собой старую библиотеку, которая работает иначе, чем ожидает ваш плеер. Он должен содержать:loadAudio(const std::string& path)— выводит"Legacy: Loading [path]"startPlayback()— выводит"Legacy: Playback started"stopPlayback()— выводит"Legacy: Playback stopped"adjustVolume(double percentage)— принимает десятичное число (от 0.0 до 1.0) и выводит"Legacy: Volume set to [percentage]"(покажите десятичное значение)
AudioAdapter.h: Создайте адаптер, который связывает два интерфейса.Ваш класс
AudioAdapterдолжен наследоваться отMediaPlayerи хранить ссылку на объектLegacyAudioLib. Адаптер транслирует вызовы из современного интерфейса в устаревший:play()должен вызывать иloadAudio(), иstartPlayback()в устаревшей библиотекеstop()должен делегировать выполнение методуstopPlayback()setVolume()получает целое число (0-100), но устаревшая библиотека ожидает десятичное число (0.0-1.0), поэтому вам нужно будет его преобразовать
main.cpp: Продемонстрируйте работу адаптера.Прочитайте два входных значения:
- Имя файла для воспроизведения (строка)
- Уровень громкости (целое число, 0-100)
Создайте экземпляр
LegacyAudioLibиAudioAdapter, который его оборачивает. Затем используйте адаптер через интерфейсMediaPlayer(сохраните его как указательMediaPlayer*), чтобы:- Установить громкость на введенный уровень
- Воспроизвести введенное имя файла
- Остановить воспроизведение
Например, при входных данных "song.mp3" и 75:
Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stoppedПри входных данных "podcast.wav" и 50:
Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stoppedОбратите внимание, как клиентский код полностью работает с интерфейсом MediaPlayer — он и не подозревает, что на самом деле всю работу выполняет устаревшая библиотека. Адаптер берет на себя всю трансляцию, включая преобразование целочисленной громкости в десятичную. В этом и заключается суть паттерна Адаптер: заставить несовместимые интерфейсы работать вместе без изменения кода обеих сторон.
Шпаргалка
Паттерн Адаптер позволяет несовместимым интерфейсам работать вместе, выступая в качестве моста между двумя классами. Он преобразует интерфейс одного класса в интерфейс, который ожидают клиенты.
Используйте паттерн Адаптер, когда вы хотите использовать существующий класс, но его интерфейс не соответствует вашим потребностям, или при интеграции устаревшего кода или сторонних библиотек.
Структура
Паттерн состоит из трех основных компонентов:
- Целевой интерфейс (Target Interface): Интерфейс, который ожидает клиент
- Адаптируемый класс (Adaptee): Существующий класс с несовместимым интерфейсом
- Адаптер (Adapter): Оборачивает адаптируемый класс и реализует целевой интерфейс, транслируя вызовы между ними
Пример
#include <iostream>
#include <string>
// Существующий класс с несовместимым интерфейсом (Adaptee)
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// Целевой интерфейс, который ожидает клиент
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// Адаптер оборачивает LegacyPrinter и реализует интерфейс Printer
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // Трансляция вызова
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// Клиент использует ожидаемый интерфейс
adapter.print("Hello World");
}Адаптер хранит ссылку на адаптируемый объект и реализует целевой интерфейс. Когда методы вызываются у адаптера, он делегирует их методам адаптируемого объекта, транслируя вызовы по мере необходимости. Клиентский код работает с целевым интерфейсом, не зная об устаревшей реализации.
Попробуйте сами
#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"
using namespace std;
int main() {
// Чтение входных данных
string filename;
int volumeLevel;
cin >> filename;
cin >> volumeLevel;
// TODO: Создать экземпляр LegacyAudioLib
// TODO: Создать AudioAdapter, который оборачивает устаревшую библиотеку
// TODO: Сохранить адаптер как указатель MediaPlayer
// TODO: Использовать интерфейс MediaPlayer, чтобы:
// 1. Установить громкость на входной уровень
// 2. Воспроизвести файл с входным именем filename
// 3. Остановить воспроизведение
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер