Menu
Coddy logo textTech

Паттерн Адаптер

Часть раздела Объектно-ориентированное программирование путешествия по 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, не зная о лежащей в основе устаревшей реализации.

Используйте Адаптер, когда вы хотите использовать существующий класс, но его интерфейс не соответствует вашим потребностям, или когда вам нужно создать повторно используемый класс, который взаимодействует с классами, имеющими несовместимые интерфейсы.

challenge icon

Задание

Легко

Давайте создадим систему 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: Продемонстрируйте работу адаптера.

    Прочитайте два входных значения:

    1. Имя файла для воспроизведения (строка)
    2. Уровень громкости (целое число, 0-100)

    Создайте экземпляр LegacyAudioLib и AudioAdapter, который его оборачивает. Затем используйте адаптер через интерфейс MediaPlayer (сохраните его как указатель MediaPlayer*), чтобы:

    1. Установить громкость на введенный уровень
    2. Воспроизвести введенное имя файла
    3. Остановить воспроизведение

Например, при входных данных "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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование