Andrey on .NET | Структурные шаблоны: Адаптер (Adapter)

Структурные шаблоны: Адаптер (Adapter)

Название шаблона

Адаптер (Adapter / Wrapper).

Тип

Структурный шаблон проектирования (Structural).

Описание

AdapterШаблон Адаптер предназначен для приведения интерфейса объекта к требуемому виду.

Данный шаблон применяется если:

  • существующий объект, называемый адаптируемым, предоставляет необходимые функции, но не поддерживает нужного интерфейса;
  • (или) неизвестно заранее, с каким интерфейсами придется работать адаптируемому объекту;
  • (или) формат входных или выходных данных метода не совпадает с требуемым.

Главная задача Адаптера – реализация требуемого интерфейса и трансляция его вызовов адаптируемому объекту. Подобную ситуацию можно встретить при использовании сторонних библиотек. Они далеко не всегда предоставляют интерфейсы, которые необходимы в для связи с другими объектами. При этом изменить код или добавить поддержку интерфейса не предоставляется возможным.

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

Еще один пример использования шаблона – адаптация поведения. Такой подход используется, например, в .NET при работе с COM объектами: полученные от них коды ошибок из значений типа HRESULT преобразуется в выбросы исключений типа COMExceptions.

Таким образом, цель Адаптера – предоставить возможность повторного использования существующего кода, независимо от отличий в интерфейсе или поведении. Кроме того, меняя адаптируемые объекты возможно влиять на функции, выполняемые в программе.

Различают четыре роли, отводимые участвующим в работе шаблона объектам:

  1. Адаптируемый объект (Adaptee);
  2. Цель (Target), определяющая требуемый интерфейс;
  3. Адаптер (Adapter);
  4. Клиент (Client), который умеет работать с только объектами, реализующими интерфейс цели.

Стоит принять во внимание следующие моменты:

  1. Адаптер не обязательно должен содержать только вызовы адаптируемого объекта. Он может заниматься обработкой входных и выходных данных, приводя их к нужному формату.
  2. В рамках шаблона можно использовать несколько адаптируемых объектов для реализации заданного интерфейса.
  3. Реализация шаблона может самостоятельно дополнить необходимую функциональность, если такой нет у адаптируемого объекта.
  4. Адаптер может работать не только с заданным адаптируемым объектом, но и его наследниками.
  5. Возможно замещение части методов адаптируемого объекта. В этом случае от него необходимо создать подкласс, в котором произвести нужные замещения. И уже результат использовать в Адаптере.
  6. Реализация адаптера может быть двухсторонней. В этом случае адаптируемый объект реализует промежуточную логику для связи двух целей, каждая из которых требует наличие своего интерфейса.
  7. Можно использовать Отложенную инициализацию для создания экземпляра адаптируемого объекта.

Схожие шаблоны и их отличия

Адаптер Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. Позволяет повторно использовать уже существующий код. Содержит или наследует адаптируемый объект.
Фасад Объединяет группу объектов под одним специализированным интерфейсом. Упрощает работу с группой объектов, вносит новый уровень абстракции. Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса.
Мост Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников).
Декоратор Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно.
Прокси Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. Содержит объект или ссылку на него, может управлять существованием замещенного объекта.
Компоновщик Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. Включается в виде интерфейса в составные объекты и их части.
Приспособленец

Не ставит целью изменение интерфейса объекта. Но это может потребоваться для получения обратно данных из вынесенной части состояния.

Позволяет уменьшить число экземпляров объекта в приложении и тем самым сэкономить его ресурсы. Выносит контекстно-зависимую часть состояния объекта вовне, заменяя несколько его экземпляров одним.

Реализация шаблона в общем виде

По схеме, используемой для работы с адаптируемым объектом, выделяют два варианта:

  1. Адаптер объекта – использует композицию, т.е. содержит экземпляр адаптируемого объекта.
  2. Адаптер класса – использует наследование от адаптируемого объекта для получения его функциональности.

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

Но встречаются ситуации, когда требуется применение адаптера класса. Например, необходимость доступа к protected методам. В другом случае может потребоваться использовать Адаптер и вместо адаптируемого объекта.

Реализация Адаптера объекта

  • создаем класс Adapter, который будет реализовывать требуемый интерфейс ITarget или являться наследником от класса Target с нужным интерфейсом;
  • в его закрытом поле _adaptee размещаем экземпляр адаптируемого объекта;
  • реализуем интерфейс ITarget, в методах которого вызываем нужные методы адаптируемого объекта;
  • клиент использует экземпляр класса Adapter и получает требуемую функциональность.

Реализация Адаптера класса

  • создаем класс Adapter, который будет реализовывать требуемый интерфейс ITarget;
    • шаблон позволяет использовать наследование от класса Target, но в C# этот вариант не может быть реализован. Причина – запрет на множественное наследование.
  • добавлением классу Adapter наследование от адаптируемого класса;
  • реализуем интерфейс ITarget, в методах которого вызываем нужные методы адаптируемого объекта;
  • клиент использует экземпляр класса Adapter и получает требуемую функциональность.

Примеры реализации

1. Адаптер объекта

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

Интерфейс IAudioPlayer содержит описание функций самого простого медиаплеера, осуществляющего загрузку и воспроизведение аудиофайла.

/// <summary>Simple audio player interface.</summary>
public interface IAudioPlayer
{
    /// <summary>Loads the audio file.</summary>
    void Load(string file);

    /// <summary>Plays the audio file.</summary>
    void Play();
}

При реализации обратим внимание, что .NET уже содержит класс SoundPlayer, предоставляющий подобные возможности. Однако данный класс не поддерживает заданный интерфейс. Поэтому воспользуемся шаблоном Адаптер:

/// <summary>Simple audio player interface.</summary>
public class SoundPlayerAdapter : IAudioPlayer
{
    /// <summary>Adaptee object.</summary>
    private readonly Lazy<SoundPlayer> _lazyPlayer = new Lazy<SoundPlayer>();

    /// <summary>Loads the audio file.</summary>
    public void Load(string file)
    {
        this._lazyPlayer.Value.SoundLocation = file;
        this._lazyPlayer.Value.Load();
    }

    /// <summary>Plays the audio file.</summary>
    public void Play()
    {
        this._lazyPlayer.Value.Play();
    }
}

Код очень простой. Можно отметить только использование Отложенной инициализации (с помощью generic-класса Lazy) для адаптируемого класса. Это сделано на случай, если не будет загружено или воспроизведено ни одного аудиофайла.

Теперь возможно использовать возможности SoundPlayer в разрабатываемом приложении:

private IAudioPlayer _player = new SoundPlayerAdapter();

public void NotifyUser(int messageCode)
{
    string wavFile = string.Empty;
    
    /* Skipped */

    // play the audio file
    if (!string.IsNullOrEmpty(wavFile)) {
        this._player.Load(wavFile);
        this._player.Play();
    }
}

Может возникнуть вопрос: почему сразу не использовать класс SoundPlayer? В отличии от явного использования указанного класса, данный подход обеспечил независимость кода от конкретной реализации медиаплеера. Например, в дальнейшем можно легко заменить SoundPlayerAdapter на другой класс для поддержки файлов другого формата.

2. Адаптер класса

Давайте рассмотрим решение той же задачи с помощью адаптера класса.

В этот раз необходимо наследовать SoundPlayerAdapter не только от интерфейса IAudioPlayer, но и от класса SoundPlayer. В результате получаем готовый метод Play(), а вот метод Load() придется определить самостоятельно.

/// <summary>Simple audio player interface.</summary>
public class SoundPlayerAdapter : SoundPlayer, IAudioPlayer
{
    /// <summary>Loads the audio file.</summary>
    public void Load(string file)
    {
        this.SoundLocation = file;
        this.Load();
    }
}

Что изменилось по сравнению с первым вариантом?

Исчезла отложенная инициализация адаптируемого объекта, которая в первом варианте была "из коробки" и прозрачна для клиентского кода.

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

/// <summary>Simple audio player interface.</summary>
public interface IAudioPlayer
{
    /// <summary>Gets or sets the file path or URL of the .wav file to load.</summary>
    string SoundLocation { get; set; }

    /// <summary>Loads the audio file.</summary>
    void Load();

    /// <summary>Plays the audio file.</summary>
    void Play();
}

/// <summary>Simple audio player interface.</summary>
public class SoundPlayerAdapter2 : SoundPlayer, IAudioPlayer {}

Кроме того, экземпляр класса SoundPlayerAdapter теперь предоставляет полный перечень свойств и методов, унаследованных от адаптируемого объекта. Он может быть использован вместо экземпляра SoundPlayer при необходимости. Но стоит помнить, что в этом случае усиливается связь между адаптируемым объектом и кодом приложения. И это самый большой минус данного варианта.

Комментарии (13) -

Спасибо за статьи.
Но по-моему тут ошибка: if (string.IsNullOrEmpty(wavFile)): проигрывается файл с пустым путем.

Спасибо, пропустил "!". Поправил.

Евгений 17.07.2011 21:59:40

Добрый день. А подскажите: у меня сложилось мнение, что это частный случай (или, быть может, даже синоним) паттерна "Декоратор", можете более детально объяснить различия между ними?

P.S. спасибо за статьи: очень доступно подана информация, без "заходов в дебри", что очень важно для первичного ознакомления. Подача идеи реализации, на мой взгляд, гораздо эффективнее приведения примеров со стенами кода, на разбор и понимание которого неэффективно тратиться много времени

@ Евгений: Разница в том, какую задачу они решают.

Адаптер просто приводит интерфейс A к B. Изменяется описание методов, но при этом сохраняется их поведение. Может быть только добавлено приведение параметров и результата к нужному виду. Например, вывод линии: результатом обращения к методу Fx() любого интерфейса (А или B) будет одна и также линия.

Декоратор не только повторно использует код, но и изменяет его поведение. Однако не меняет определение самого метода. Например, это может быть рисование линии с эффектом свечения. Соответственно результат A.Fx() != B.Fx().

Евгений 19.07.2011 18:03:52

Спасибо за разъяснение.
Вопрос возник потому, что ранее наткнулся на статью в RSDN-Magazine, с несколько иной трактовкой Wrapper'а. Мне, как новичку в C# и шаблонах проектирования, трудно разобраться: чья же точка зрения более корректна, и как мне стоит трактовать информацию

А ссылку на статью на RSDN не подскажете? Сходу по оглавлению не нашел.

Денис 20.11.2011 11:57:35

Андрей, только не вздумайте останавливаться писать статьи. Вы очень круто пишите!!!

Константин 10.01.2012 15:59:27

Спасибо за статью, очень хорошо показано применение паттерна.
Но возник вопрос. Паттерны "Фасад" и "Адаптер" выглядят абсолютно одинаково, с той лишь только разницей, что адаптер наследует интерфейс объекта, экземпляр которого сам содержит. И тот, и другой перенапрвляют вызовы своих методов экземплярам внутренних объектов, при этом оставляя за собой право изменять эти методы (например добавлять функциональность). Можно ли считать паттерн "Адаптер" частным случаем паттерна "Фасад"?

@ Константин: IMHO нет. Дело в цели. Адаптер нужен для повторного использования кода. При этом он может использовать несколько существующих классов для реализации нужного интерфейса. Фасад же вносит новый уровень абстракции, даже если он расположен "поверх" только одного исходного интерфейса. Грань может показаться что тонкая, но IMHO она все же есть.

Здравствуйте, Андрей! Прошу пояснить, почему в рассмотренных примерах отсутствует адаптируемый класс (Adaptee)

Ivan В примерах класс SoundPlayer является адаптируемым.

Вы пишете, что встроенный класс SoundPlayer не поддерживает такой-то интерфейс. Он действительно не реализует его, но в нем есть методы Load() и Play(). Так зачем тогда всё так усложнять? шаблон какой-то использовать

Ivan Вы видимо пропустили цель шаблона - адаптация уже существующего кода. Т.е. ваш код умеет использовать некий интерфейс IX для своей цели (играть аудио). Вам надо реализовать его, но есть некий компонент, который делает то что надо, но не реализует IX. Вот тут и нужен адаптер. С его помощью вы приводите существующий код к заданному интерфейсу.

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

Добавить комментарий