Что такое паттерн адаптер java
Паттерн проектирования «Адаптер» / «Adapter»
Почитать описание других паттернов.
Интро
Как Вы уже успели заметить, данная секция является не обязательной в моих постах про паттерны. Но я не мог начать писать о конкретном паттерне, не объяснив, зачем они собственно нужны.
Не так давно, когда я был на младших курсах, на мое высказывание «Да мы уже умеем писать программы!», мой коллега сказал — «Максимум, что вы умеет писать — это алгоритмы, но не программы.» Эти его слова я вспоминаю до сих пор с улыбкой на лице. Он был совершенно прав, все чему нас научили за 3 года (я тогда был на третьем курсе) — реализация базовых алгоритмов из Кормена/Кнута. Мы действительно могли писать эти алгоритмы, могли писать функции, процедуры реализующие их. Даже могли написать класс на C++ или Java в котором описать все логику работы с данным классом объектов. Но когда таких классов становилось два или даже три 🙂 начинались проблемы. И при любой попытке написания какой-либо «программы» начиналось изобретение велосипеда (я думаю, что каждый кто читает этот пост, сам изобрел несколько таких велосипедов). Тогда, я начал подозревать, что должно быть что-то, какая теоретическая база, аппарат, механизм, называйте это как хотите, которая в принципе расскажет мне — «как писать программы».
Оказалось, такая база есть — это паттерны проектирования. По большому счету, паттерн — это типичное решение общих проблем проектирования. Паттерны решают ряд основных проблем, связанных с программированием/проектированием. Это — изобретение велосипеда, проблема повторного использования кода, проблема сопровождения кода.
Безусловно, шаблоны проектирования если не решают, то упрощают решение данных проблем. Так, разработчик или проектировщик, зная хотя бы базовый набор паттернов, использует их в замену придумываемым велосипедам. Повторное использование кода становится более прозрачное и оправданное. А сопровождать код, написанный с использованием паттернов как минимум понятно и не затруднительно.
Теперь, мне кажется можно перейти к рассмотрению конкретного паттерна — «Адаптера» («Adapter»).
Проблема
Обеспечить взаимодействие объектов с различными интерфейсами. Адаптировать, а не переписывать существующий код к требуемому интерфейсу.
Описание
Паттерн «Адаптер», на самом деле, является одним из немногих, который программисты применяют на практике, сами того не осознавая. Адаптер можно найти, пожалуй в любой современной программой системе — будь это простое приложение или, например, Java API.
Взглянем более детально на проблему, для понимая того как должен выглядеть Адаптер. Проблема, опять-таки, заключается в повторном использовании кода. Иными словами, есть клиент, который умеет работать с некоторым интерфейсом, назовем его клиентским. Есть класс, который, в принципе, делает то, что нужно клиенту но не реализует клиентский интерфейс. Безусловно, программирование нового класса довольно бессмысленная трата времени и ресурсов. Проще адаптировать уже существующий код к виду, пригодному для использования клиентом. Для этого и существует адаптер. Причем, разделяют два вида адаптеров — Object Adapter (адаптер на уровне объекта) и Class Adapter (адаптер на уровне класса). Мы рассмотри оба, но по порядку.
Практическая задача
Для примера, рассмотрим простую ситуацию. Есть некоторый класс — SequenceGenerator, генерирующий последовательности целых чисел, по определенному закону — это и есть наш клиент. Есть интерфейс — Generator, который использует клиент непосредственно для генерации каждого отдельного элемента последовательности — это наш клиентский интерфейс. К тому-же, есть класс RandomGenerator, который уже умеет генерировать случайные числа. Конечно, SequenceGenerator не может использовать RandomGenerator для генерации элементов, потому что он не соответствует клиентскому интерфейсу. Наша задача — написать адаптер (двумя способами) RandomGenerator к SequenceGenerator.
Диаграммы классов
Object Adapter
Class Adapter
Итак, имея диаграммы классов давайте поговорим об отличиях между адаптером на уровне объекта и адаптером на уровне класса. На самом деле различия видны уже из названия. В первом случае, адаптируемый объект (RandomGenerator) является полем (ссылкой) в классе адаптера (RandomGeneratorAdapter), во втором же он и является адаптером за счет использования механизма наследования. В реальных проектах, рекомендуется использовать Object Adapter, за счет его меньшей связности с адаптируемым объектом.
Реализация
Рассмотрим реализацию поставленной задачи. Object Adapter я реализовывал на С++, Class Adapter на Java.
Object Adapter
class Generator <
public :
virtual int next() = 0;
class SequenceGenerator <
private :
Generator *generator;
protected :
public :
SequenceGenerator(Generator& generator);
int * generate( int length);
>;
int * SequenceGenerator::generate( int length) <
int *ret = new int [length];
class RandomGenerator <
public :
inline int getRandomNumber() < return 4; >; // It`s really random number.
class RandomGeneratorAdapter : public Generator <
private :
RandomGenerator *adaptee;
public :
int main( int argc, char *argv[]) <
RandomGenerator rgenerator;
RandomGeneratorAdapter adapter(rgenerator);
SequenceGenerator sgenerator(adapter);
const int SIZE = 10;
int *seq = sgenerator.generate(SIZE);
Class Adapter
Классическая реализация паттерна Class Adapter подразумевает использование множественного наследования. В Java, можно воспользоваться имплементацией интерфейсов. На мой взгляд, это даже как-то корректнее.
public class SequenceGenerator <
private Generator generator;
public int [] generate( int length) <
int ret[] = new int [length];
for ( int i=0; i return ret;
>
>
public class RandomGenerator <
public int getRandomNumber() <
return 4;
>
>
public class RandomGeneratorAdapter extends RandomGenerator implements Generator <
@Override
public int next() <
return getRandomNumber();
>
// Использование
public class Main <
public static void main( String [] args) <
RandomGeneratorAdapter adapter = new RandomGeneratorAdapter();
SequenceGenerator generator = new SequenceGenerator(adapter);
На этом все. Жду Ваших отзывов в комментариях.
Адаптеры
— Привет, Амиго! Сегодня я расскажу тебе, что же такое « адаптер ». Надеюсь, что после его изучения ты начнешь понимать потоки ввода-вывода гораздо лучше.
Представь, что в твоей программе ты используешь два фреймворка, написанные другими программистами/компаниями. Оба фреймворка очень хорошие и используют принципы ООП: абстракцию, полиморфизм, инкапсуляцию. Они вместе практически полностью покрывают задачи твоей программы. За тобой осталось простая задача — объекты, которые создает один фреймворк нужно передать во второй. Но оба фреймворка совершенно разные и «не знают друг о друге» — т.е. не имеют общих классов. Тебе нужно как-то преобразовывать объекты одного фреймворка в объекты другого.
Эту задачу можно красиво решить, применив подход (паттерн проектирования) «адаптер»:
Код на Java | Описание |
---|---|
Это схематическое описание «паттерна проектирования адаптер». Суть его в том, что класс MyClass является преобразователем (адаптером) одного интерфейса к другому. |
— А можно более конкретный пример?
— Ок. Допустим, что у каждого фреймворка есть свой уникальный интерфейс «список», вот как это может выглядеть:
Код на Java | Описание |
---|---|
Код из первого( Alpha ) фреймворка. AlphaList – это один из интерфейсов, для взаимодействия кода фреймворка и кода, который будет использовать этот фреймворк. | |
AlphaList Manager – класс фреймворка, метод которого createList создает объект типа AlphaList | |
Код из второго( Beta ) фреймворка. BetaList – это один из интерфейсов, для взаимодействия кода фреймворка и кода, который будет использовать этот фреймворк. BetaSaveManager – класс фреймворка, метод которого saveList сохраняет на диск объект типа BetaList | |
Класс «адаптер» (т.е. переходник) от интерфейса AlphaList к интерфейсу BetaList Класс ListAdapter реализует интерфейс BetaList из второго фреймворка. Объект типа AlphaList передается в конструктор ListAdapter в момент создания Метод setSize работает по принципу: если нужно увеличить размер списка – добавим туда пустых (null) элементов. Если нужно уменьшить – удалим несколько последних. | |
Пример использования |
— Больше всего понравился пример использования. Очень компактно и понятно.
Какие задачи решает шаблон проектирования Адаптер
Подробнее о проблеме
Принцип работы паттерна Адаптер
Когда использовать Адаптер
Если нужно использовать сторонний класс, но его интерфейс не совместим с основным приложением. На примере выше видно, как создается объект-прокладка, который оборачивает вызовы в понятный для целевого объекта формат.
Когда у нескольких существующих подклассов должен быть общий функционал. Вместо дополнительных подклассов (их создание приведет к дублированию кода) лучше использовать адаптер.
Преимущества и недостатки
Не путать с Фасадом и Декоратором
Пошаговый алгоритм реализации
Для начала убедись, что есть проблема, которую может решить этот паттерн.
Определи клиентский интерфейс, от имени которого будет использоваться другой класс.
Реализуй класс адаптера на базе интерфейса, определенного на предыдущем шаге.
В классе адаптера сделай поле, в котором хранится ссылка на объект. Эта ссылка передается в конструкторе.
Реализуй в адаптере все методы клиентского интерфейса. Метод может:
Передавать вызов без изменения;
Изменять данные, увеличивать/уменьшать количество вызовов целевого метода, дополнительно расширять состав данных и тд.
В крайнем случае, при несовместимости конкретного метода, выбросить исключение UnsupportedOperationException, которое строго нужно задокументировать.
Если приложение будет использовать адаптер только через клиентский интерфейс (как в примере выше), это позволит безболезненно расширять адаптеры в будущем.
Само собой, паттерн проектирования — это не панацея от всех бед, но с его помощью можно элегантно решить задачу несовместимости объектов с разными интерфейсами. Разработчик, знающий базовые паттерны, — на несколько ступенек выше тех, кто просто умеет писать алгоритмы, ведь они нужны для создания серьезных приложений. Повторно использовать код становится не так сложно, а поддерживать — одно удовольствие. На сегодня все! Но мы скоро продолжим знакомство с разными шаблонами проектирования 🙂
Java. Adapter Pattern in Game Server
Адаптер — это шаблон структурного проектирования, который позволяет объектам с несовместимыми интерфейсами взаимодействовать друг с другом.
Также известен как “Обертка”.
Проблема
Чтобы продемонстрировать этот шаблон, я буду использовать упрощенный пример игровой механики, в которой есть интерфейс IEnemy, но один из врагов отличается от других и не имеет реализации метода атаки. Вместо этого, этот конкретный враг (SpecialEnemy) накладывает заклинания.
Решение
Вы можете создать адаптер. Это специальный объект, который преобразует интерфейс одного объекта, чтобы другой объект мог его понять. Адаптер оборачивает один из объектов, чтобы скрыть сложность преобразования, происходящего за кулисами. Обернутый объект даже не знает об адаптере. Например, вы можете обернуть объект, который работает в метрах и километрах, с помощью адаптера, который преобразует все данные в британские единицы, такие как футы и мили. Адаптеры могут не только преобразовывать данные в различные форматы, но также могут помогать объектам с разными интерфейсами взаимодействовать друг с другом. Вот как это работает:
Адаптер получает интерфейс, совместимый с одним из существующих объектов.
Используя этот интерфейс, существующий объект может безопасно вызывать методы адаптера.
При получении вызова адаптер передает запрос второму объекту, но в формате и порядке, которые ожидает второй объект.
Иногда даже можно создать двусторонний адаптер, который может конвертировать вызовы в обоих направлениях. Вернемся к нашему игровому приложению. Чтобы решить дилемму несовместимых врагов, вы можете создать адаптер EnemyAdapter для каждого класса особенных врагов, с которым ваш код работает напрямую. Затем вы настраиваете свой код для связи с SpecialEnemy только через эти адаптеры. Когда адаптер получает вызов, он переводит входящие атаки в кастование спеллов.
Структура
Существует несколько видов адаптеров
Object adapter
В этой реализации используется принцип композиции объектов: адаптер реализует интерфейс одного объекта и обертывает другой. Его можно реализовать на всех популярных языках программирования.
Class adapter
В этой реализации используется наследование: адаптер наследует интерфейсы от обоих объектов одновременно. Обратите внимание, что этот подход может быть реализован только в языках программирования, поддерживающих множественное наследование, таких как C ++.
Применимость
Используйте класс Adapter, если вы хотите использовать какой-либо существующий класс, но его интерфейс несовместим с остальной частью вашего кода.
Шаблон адаптера позволяет вам создать класс среднего уровня, который служит транслятором между вашим кодом и унаследованным классом, сторонним классом или любым другим классом со странным интерфейсом.
Используйте шаблон, если вы хотите повторно использовать несколько существующих подклассов, в которых отсутствуют некоторые общие функции и нельзя добавить в суперкласс.
Вы можете расширить каждый подкласс и добавить недостающие функции в новые дочерние классы. Однако вам придется продублировать код во всех этих новых классах, что очень плохо «пахнет».
Гораздо более элегантным решением было бы поместить недостающие функции в класс адаптера. Затем вы должны обернуть объекты с недостающими функциями внутри адаптера, динамически получая необходимые функции. Чтобы это работало, целевые классы должны иметь общий интерфейс, а поле адаптера должно следовать за этим интерфейсом. Этот подход очень похож на шаблон Decorator.
Реализация
Добавляем специальных врагов.
Создадим класс SpecialEnemy, использующий метод CastSpell.
Я буду использовать строку возврата, чтобы упростить процесс.
Затем, создадим интерфейс IEnemy для управления поведением врагов, в данном случае это только один метод для атаки
Теперь нам нужно реализовать этот интерфейс в классе EnemyAdapter, который соединит обе части вместе.
Нам нужно добавить ссылку на SpecialEnemy, чтобы иметь доступ к методу CastSpell. Таким образом, мы можем использовать метод атаки без реализации его в SpecialEnemy. Или мы можем создать конструктор и передать туда экземпляр SpecialEnemy.
В качестве вывода мы получаем строку «использование заклинания». Этот пример был слишком упрощен, но он демонстрирует идею, лежащую в основе этого простого шаблона. Надеюсь, он вам понравился и был полезен.
Паттерны: Adapter, Proxy, Bridge
— У нас еще осталось немного времени, поэтому я расскажу тебе про еще три паттерна.
— Еще три, а сколько их всего?
— Ну, сейчас есть несколько десятков популярных паттернов, но количество «удачных решений» не ограничено.
— Ясно. И что, мне придется учить несколько десятков паттернов?
— Пока у тебя нет опыта реального программирования, они дадут тебе не очень много.
Ты лучше поднаберись опыта, а потом, через годик, вернись к этой теме и попробуй разобраться в них более основательно. Хотя бы пару десятков самых популярных.
Грех не пользоваться чужим опытом и самому что-то изобретать в очередной 110-й раз.
Паттерн Adapter(Wrapper) – Адаптер (Обертка)
Представь, что ты приехал в Китай, а там другой стандарт розеток. Отверстия не круглые, а плоские. Тогда тебе понадобится переходник, или другими словами – адаптер.
В программировании тоже может быть что-то подобное. Классы оперируют похожими, но различными интерфейсами. И надо сделать переходник между ними.
Вот как это может выглядеть:
Интерфейс Time позволяет узнать текущее время с помощью методов getSeconds (), getMinutes () и getHours ().
Интерфейс TotalTime позволяет получить количество секунд, которое прошло от полночи до текущего момента.
Для этого мы можем написать классы-адаптеры. Пример:
И адаптер в другую сторону:
— Ага. Мне нравится. А примеры есть?
— Конечно, например, InputStreamReader – это классический адаптер. Преобразовывает тип InputStream к типу Reader.
Иногда этот паттерн еще называют обертка, потому что новый класс как бы «оборачивает» собой другой объект.
Другие интересные вещи почитать можно тут.
Паттерн Proxy — Заместитель
Паттерн прокси чем-то похож на паттерн «обертка». Но его задача – не преобразовывать интерфейсы, а контролировать доступ к оригинальному объекту, сохраненному внутри прокси-класса. При этом и оригинальный класс и прокси обычно имеют один и тот же интерфейс, что облегчает подмену объекта оригинального класса, на объект прокси.
Этот интерфейс позволяет получить или изменить количество денег на счету пользователя.
Вот как это работает на деле:
В первом примере мы создаем объект банк и вызываем у него метод setUserMoney.
— Ага. Таких прокси может быть много. Например, можно добавить еще один прокси, который будет проверять – не слишком ли большая сумма. Может менеджер банка решил положить себе на счет кучу денег и сбежать с ними на Кубу.
Более того. Создание всех этих цепочек объектов можно поместить в класс BankFactory и подключать/отключать нужные из них.
По похожему принципу работает BufferedReader. Это Reader, но который делает еще дополнительную работу.
Такой подход позволяет «собирать» из «кусочков» объект нужной тебе функциональности.
Чуть не забыл. Прокси используются гораздо шире, чем я только что тебе показал. Об остальных типах использования ты можешь почитать здесь.
Паттерн Bridge – Мост
Иногда, в процессе работы программы, надо сильно поменять функциональность объекта. Например, был у тебя в игре персонаж осел, а потом маг превратил его в дракона. У дракона совсем другое поведение и свойства, но(!) это – тот же самый объект.
— А нельзя просто создать новый объект и все?
— Не всегда. Допустим, твой осел был в друзьях у кучи персонажей, или например, на нем были наложены некоторые заклинания, или он участвовал в каких-то квестах. Т.е. этот объект уже может быть задействован в куче мест и привязан к куче других объектов. Так что просто создать новый другой объект в этом случае – не вариант.
— Одним из наиболее удачных решений есть паттерн Мост.
Этот паттерн предлагает разделить объект на два объекта. На «объект интерфейса» и «объект реализации».
— А в чем отличие от интерфейса и класса, который его реализует?
— В случае с интерфейсом и классом в результате будет создан один объект, а тут — два. Смотри пример:
А потом можно объявить несколько классов наследников от UserImpl, например UserDonkey (осел) и UserDragon (дракон).
— Все равно не очень понял, как это будет работать.
— Чем-то напоминает прокси.
— Да, только в прокси главный объект мог храниться где-то отдельно, а код работал с прокси-объектами вместо него. Здесь же предлагается, что все работают именно с главным объектом, а меняются его внутренние части.
— Ага. Спасибо. А дашь ссылку почитать еще про это?
— Конечно, друг Амиго. Держи: Паттерн Bridge