Паттерн Адаптер
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 97 из 104.
Паттерн Adapter позволяет несовместимым интерфейсам работать вместе. Он действует как мост между двумя классами, преобразуя интерфейс одного класса в интерфейс, который ожидают клиенты. Это особенно полезно при интеграции устаревшего кода или сторонних библиотек, интерфейсы которых не соответствуют интерфейсу вашей системы.
Представьте, что у вас есть существующий класс, который делает то, что вам нужно, но его interface не соответствует тому, что ожидает ваш код. Вместо изменения любой из сторон вы создаёте adapter, который оборачивает существующий класс и преобразует вызовы:
#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");
}adapter хранит ссылку на LegacyPrinter и реализует интерфейс Printer. Когда вызывается print(), он делегирует выполнение printMessage(). Код клиента работает с интерфейсом Printer, не зная о лежащей в основе реализации legacy.
Используйте Adapter, когда хотите использовать существующий класс, но его interface не соответствует тому, что вам нужно, или когда вам нужно создать повторно используемый класс, взаимодействующий с классами с incompatible interface.
Задание
ЛегкоДавайте создадим систему Media Player Adapter, которая объединяет интерфейсы разных аудиобиблиотек. Представьте, что вы интегрируете устаревшую аудиобиблиотеку в современное приложение медиаплеера: старая библиотека работает безупречно, но её интерфейс не соответствует тому, что ожидает ваш плеер. Именно здесь проявляются преимущества паттерна Adapter.
Вы организуете код в четырёх файлах:
MediaPlayer.h: Define целевой интерфейс, который ожидает ваш современный медиаплеер.Create abstract
MediaPlayerclass с этими чистыми виртуальными методами:play(const std::string& filename): воспроизводит указанный файлstop(): останавливает воспроизведениеsetVolume(int level): устанавливает громкость (0-100)
Include виртуальный destructor.
LegacyAudioLib.h: Create существующую "legacy" аудиобиблиотеку с incompatible интерфейсом.Ваш
LegacyAudioLibclass представляет старую библиотеку, которая работает иначе, чем ожидает ваш плеер. В ней должны быть:loadAudio(const std::string& path): выводитLegacy: Loading [path]startPlayback(): выводитLegacy: Playback startedstopPlayback(): выводитLegacy: Playback stoppedadjustVolume(double percentage): принимает decimal (0.0 to 1.0) и выводитLegacy: Volume set to [percentage](показывая значение decimal)
AudioAdapter.h: Создайте adapter, который объединяет два интерфейса.Ваш
AudioAdapterclass должен наследоваться отMediaPlayerи хранить ссылку на объектLegacyAudioLib. Adapter преобразует вызовы современного интерфейса в вызовы legacy-интерфейса:play()должен вызывать иloadAudio(), иstartPlayback()у legacy-библиотекиstop()должен delegate вызовstopPlayback()setVolume()получает целое число (0-100), но legacy-библиотека expects decimal (0.0-1.0), поэтому его необходимо convert
main.cpp: Продемонстрируйте работу adapter.Считайте два входных значения:
- Имя файла для воспроизведения (string)
- Уровень громкости (целое число, 0-100)
Create экземпляр
LegacyAudioLibиAudioAdapter, который оборачивает его. Затем используйте adapter через интерфейс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. Он не знает, что фактическую работу выполняет находящаяся внутри legacy-библиотека. Adapter обрабатывает весь перевод, включая преобразование целочисленного значения громкости в decimal. В этом и заключается суть паттерна Adapter: заставить incompatible интерфейсы работать вместе, не изменяя ни одну из сторон.
Попробуйте сами
#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. Воспроизвести входное имя файла
// 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Итоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++