Middleware, случаи применения, плюсы и минусы, отличия от Фасада и Декоратора и пошаговый алгоритм.Кратко
- Адаптер оборачивает объект с чужим интерфейсом и отдает клиенту тот интерфейс, который он ожидает; код обеих сторон не меняется.
- Реализация: класс-адаптер реализует целевой интерфейс (
Excuse), хранит ссылку на адаптируемый объект (StudentExcuse) и в каждом методе переводит вызов. - Метод адаптера может просто пробросить вызов, преобразовать данные или добавить обработку, например занести отговорку в черный список перед вызовом.
- Если у адаптируемого объекта нет аналога метода, адаптер бросает
UnsupportedOperationException, и это нужно задокументировать. - Когда применять: сторонний класс с несовместимым интерфейсом или старый код, который нельзя менять. Цена: лишние классы в проекте.
- Не путать: Фасад дает новый упрощенный интерфейс к целой подсистеме, Декоратор добавляет поведение, не меняя интерфейс, Прокси контролирует доступ к объекту с тем же интерфейсом.
Для решения этой проблемы придумали паттерн Адаптер, который помогает объектам с несовместимыми интерфейсами работать вместе. Давай посмотрим, как его использовать!Подробнее о проблеме
Старая система: интерфейс Excuse
Для начала сымитируем поведение старой системы. Предположим, она генерирует причины опоздания на работу или учебу. Для этого у нас есть интерфейсExcuse, который содержит методы generateExcuse(), likeExcuse() и dislikeExcuse().
public interface Excuse {
String generateExcuse();
void likeExcuse(String excuse);
void dislikeExcuse(String excuse);
}
Этот интерфейс реализует класс WorkExcuse:
public class WorkExcuse implements Excuse {
private String[] reasonOptions = {"по невероятному стечению обстоятельств у нас в доме закончилась горячая вода и я ждал, пока солнечный свет, сконцентрированный через лупу, нагреет кружку воды, чтобы я мог умыться.",
"искусственный интеллект в моем будильнике подвел меня и разбудил на час раньше обычного. Поскольку сейчас зима, я думал что еще ночь и уснул. Дальше все как в тумане.",
"предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице."};
private String[] sorryOptions = {"Это, конечно, не повторится, мне очень жаль.", "Прошу меня извинить за непрофессиональное поведение.", "Нет оправдания моему поступку. Я недостоин этой должности."};
@Override
public String generateExcuse() { // Случайно выбираем отговорку из массива
String result = "Я сегодня опоздал, потому что " + reasonOptions[(int) (Math.random() * reasonOptions.length)] + "\n" +
sorryOptions[(int) (Math.random() * sorryOptions.length)];
return result;
}
@Override
public void likeExcuse(String excuse) {
// Дублируем элемент в массиве, чтобы шанс его выпадения был выше
}
@Override
public void dislikeExcuse(String excuse) {
// Удаляем элемент из массива
}
}
Протестируем пример:
Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Вывод:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Прошу меня извинить за непрофессиональное поведение.
Новая библиотека: интерфейс StudentExcuse
Теперь представим, что ты запустил сервис, собрал статистику и заметил, что большинство пользователей сервиса это студенты вузов. Чтобы улучшить его под нужды этой группы, ты заказал у другого разработчика систему генерации отговорок специально для нее. Команда разработчика провела исследования, составила рейтинги, подключила искусственный интеллект, добавила интеграцию с пробками на дорогах, погодой и так далее. Теперь у тебя есть библиотека генерации отговорок для студентов, однако интерфейс взаимодействия с ней другой:StudentExcuse:
public interface StudentExcuse {
String generateExcuse();
void dislikeExcuse(String excuse);
}
У интерфейса есть два метода: generateExcuse, который генерирует отговорку, и dislikeExcuse, который блокирует отговорку, чтобы она не появлялась в дальнейшем.
Библиотека стороннего разработчика закрыта для редактирования: ты не можешь изменять ее исходный код.
В итоге в твоей системе есть класс WorkExcuse, реализующий интерфейс Excuse, и библиотека с классом SuperStudentExcuse, который реализует интерфейс StudentExcuse:
public class SuperStudentExcuse implements StudentExcuse {
@Override
public String generateExcuse() {
// Логика нового функционала
return "Невероятная отговорка, адаптированная под текущее состояние погоды, пробки или сбои в расписании общественного транспорта.";
}
@Override
public void dislikeExcuse(String excuse) {
// Добавляет причину в черный список
}
}
Почему нельзя просто расширить иерархию
Изменить код нельзя. Текущая схема будет выглядеть так:
Эта версия системы работает только с интерфейсом Excuse. Переписывать код нельзя: в большом приложении такие правки могут затянуться или нарушить логику приложения.
Можно предложить внедрение основного интерфейса и увеличение иерархии:
Для этого нужно переименовать интерфейс Excuse. Но дополнительная иерархия нежелательна в серьезных приложениях: внедрение общего корневого элемента нарушает архитектуру.
Следует реализовать промежуточный класс, который позволит использовать новый и старый функционал с минимальными потерями. Словом, тебе нужен адаптер.
Принцип работы паттерна Адаптер
Адаптер это промежуточный объект, который делает вызовы методов одного объекта понятными другому.Реализация адаптера Middleware
Реализуем адаптер для нашего примера и назовем егоMiddleware.
Наш адаптер должен реализовывать интерфейс, совместимый с одним из объектов. Пусть это будет Excuse. Благодаря этому Middleware может вызывать методы первого объекта.
Middleware получает вызовы и передает их второму объекту в совместимом формате. Так выглядит реализация класса Middleware с методами generateExcuse и dislikeExcuse:
public class Middleware implements Excuse { // 1. Middleware становится совместимым с объектом WorkExcuse через интерфейс Excuse
private StudentExcuse superStudentExcuse;
public Middleware(StudentExcuse excuse) { // 2. Получаем ссылку на адаптируемый объект
this.superStudentExcuse = excuse;
}
@Override
public String generateExcuse() {
return superStudentExcuse.generateExcuse(); // 3. Адаптер реализовывает метод интерфейса
}
@Override
public void dislikeExcuse(String excuse) {
// Метод предварительно помещает отговорку в черный список БД,
// Затем передает ее в метод dislikeExcuse объекта superStudentExcuse.
}
// Метод likeExcuse появится позже
}
Тест в клиентском коде
Тестирование (в клиентском коде):
public class Test {
public static void main(String[] args) {
Excuse excuse = new WorkExcuse(); // Создаются объекты классов,
StudentExcuse newExcuse = new SuperStudentExcuse(); // Которые должны быть совмещены.
System.out.println("Обычная причина для работника:");
System.out.println(excuse.generateExcuse());
System.out.println("\n");
Excuse adaptedStudentExcuse = new Middleware(newExcuse); // Оборачиваем новый функционал в объект-адаптер
System.out.println("Использование нового функционала с помощью адаптера:");
System.out.println(adaptedStudentExcuse.generateExcuse()); // Адаптер вызывает адаптированный метод
}
}
Вывод:
Обычная причина для работника:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Нет оправдания моему поступку. Я недостоин этой должности.
Использование нового функционала с помощью адаптера:
Невероятная отговорка, адаптированная под текущее состояние погоды, пробки или сбои в расписании общественного транспорта.
В методе generateExcuse выполнена простая передача вызова другому объекту, без дополнительных преобразований. Метод dislikeExcuse потребовал предварительного помещения отговорки в черный список базы данных. Дополнительная промежуточная обработка данных это причина, по которой любят паттерн Адаптер.
Метод, которого нет: UnsupportedOperationException
А как быть с методомlikeExcuse, который есть в интерфейсе Excuse, но нет в StudentExcuse? Эта операция не поддерживается в новом функционале.
Для такого случая придумали исключение UnsupportedOperationException: по документации Oracle оно выбрасывается, если запрашиваемая операция не поддерживается. Используем это.
Так выглядит новая реализация класса Middleware:
public class Middleware implements Excuse {
private StudentExcuse superStudentExcuse;
public Middleware(StudentExcuse excuse) {
this.superStudentExcuse = excuse;
}
@Override
public String generateExcuse() {
return superStudentExcuse.generateExcuse();
}
@Override
public void likeExcuse(String excuse) {
throw new UnsupportedOperationException("Метод likeExcuse не поддерживается в новом функционале");
}
@Override
public void dislikeExcuse(String excuse) {
// Метод обращается за дополнительной информацией к БД,
// Затем передает ее в метод dislikeExcuse объекта superStudentExcuse.
}
}
На первый взгляд это решение не кажется удачным, но имитирование функционала может привести к более сложной ситуации. Если клиент будет внимателен, а адаптер хорошо документирован, такое решение приемлемо.
Когда использовать Адаптер
Если нужно использовать сторонний класс, но его интерфейс не совместим с основным приложением. На примере выше видно, как создается объект-прокладка, который оборачивает вызовы в понятный для целевого объекта формат.
Когда у нескольких существующих подклассов должен быть общий функционал. Вместо дополнительных подклассов (их создание приведет к дублированию кода) лучше использовать адаптер.
Преимущества и недостатки
Преимущество: Адаптер скрывает от клиента подробности обработки запросов от одного объекта к другому. Клиентский код не думает о форматировании данных или обработке вызовов целевого метода. Это слишком сложно, а программисты ленивые :) Недостаток: Кодовая база проекта усложняется дополнительными классами, а при большом количестве несовместимых точек их количество может вырасти до неконтролируемых размеров.Не путать с Фасадом и Декоратором
При поверхностном изучении Адаптер можно перепутать с паттернами Фасад и Декоратор. Отличие Адаптера от Фасада заключается в том, что Фасад внедряет новый интерфейс и оборачивает целую подсистему. Ну а Декоратор, в отличие от Адаптера, меняет сам объект, а не интерфейс. Есть и четвертый похожий паттерн, Прокси: он сохраняет интерфейс объекта, но контролирует доступ к нему.Адаптер, Фасад, Декоратор, Прокси: сводная таблица
Четыре структурных паттерна часто путают, потому что все они «оборачивают» объект. Разница в том, что именно меняется:| Паттерн | Что меняет | Зачем | Пример |
|---|---|---|---|
| Адаптер (Adapter) | интерфейс: подгоняет чужой интерфейс под тот, что ожидает клиент | совместить несовместимое, не меняя ни клиент, ни библиотеку | Middleware из этой статьи, InputStreamReader в JDK |
| Фасад (Facade) | интерфейс: дает один простой вход вместо десятка классов | спрятать сложность подсистемы | один класс OrderService вместо вызовов склада, оплаты и доставки |
| Декоратор (Decorator) | поведение: добавляет обязанности объекту, интерфейс тот же | наращивать функциональность без наследования | BufferedReader поверх FileReader |
| Прокси (Proxy) | доступ: подменяет объект заместителем с тем же интерфейсом | ленивая загрузка, кеш, проверка прав, логирование | прокси Spring вокруг бина с @Transactional |
Пошаговый алгоритм реализации
Для начала убедись, что есть проблема, которую может решить этот паттерн.
Определи клиентский интерфейс, от имени которого будет использоваться другой класс.
Реализуй класс адаптера на базе интерфейса, определенного на предыдущем шаге.
В классе адаптера сделай поле, в котором хранится ссылка на объект. Эта ссылка передается в конструкторе.
Реализуй в адаптере все методы клиентского интерфейса. Метод может:
Передавать вызов без изменения;
Изменять данные, увеличивать/уменьшать количество вызовов целевого метода, дополнительно расширять состав данных и т. д.
В крайнем случае, при несовместимости конкретного метода, выбросить исключение UnsupportedOperationException, которое строго нужно задокументировать.
Если приложение будет использовать адаптер только через клиентский интерфейс (как в примере выше), это позволит безболезненно расширять адаптеры в будущем.
Вопросы и ответы
Что такое паттерн Адаптер простыми словами?
Это переходник между двумя объектами, которые не понимают друг друга, как переходник между европейской вилкой и американской розеткой. Клиент вызывает методы привычного интерфейса, адаптер принимает вызов и переводит его в методы адаптируемого объекта. Ни клиент, ни адаптируемый класс при этом не меняются, поэтому паттерн незаменим при подключении сторонних библиотек и старого кода.Чем Адаптер отличается от Декоратора и Фасада?
Адаптер меняет интерфейс объекта, чтобы он подошел клиенту, и не добавляет поведения. Декоратор сохраняет интерфейс и добавляет поведение:BufferedReader поверх FileReader читает тот же текст, но с буфером. Фасад тоже создает новый интерфейс, но его цель другая: спрятать целую подсистему за одним простым классом, а не совместить два конкретных объекта.
Чем Адаптер отличается от Прокси?
Прокси реализует тот же интерфейс, что и настоящий объект, и подменяет его, чтобы контролировать доступ: лениво создать, закешировать результат, проверить права, залогировать вызов. Адаптер реализует другой интерфейс, который нужен клиенту. Проверка простая: если интерфейсы обертки и объекта совпадают, перед вами Прокси или Декоратор, если различаются, Адаптер.Что такое адаптер объектов и адаптер классов?
Адаптер объектов, какMiddleware в статье, хранит ссылку на адаптируемый объект и делегирует ему вызовы, то есть построен на композиции. Адаптер классов наследует адаптируемый класс и одновременно реализует целевой интерфейс. В Java множественного наследования классов нет, поэтому адаптер классов возможен, только если адаптируемая сторона это интерфейс; на практике почти всегда используют адаптер объектов.
Где адаптер встречается в самой Java?
Классический пример:InputStreamReader, который в документации Oracle назван мостом от байтовых потоков к символьным: он адаптирует InputStream под интерфейс Reader. Еще Arrays.asList подает массив как List, а Collections.enumeration и Collections.list переводят между Enumeration и коллекциями.
Что делать, если у адаптируемого объекта нет нужного метода?
Три варианта. Реализовать метод в адаптере своими силами, если это возможно. Сделать его пустым, если операция не критична, и написать об этом в документации. Или броситьUnsupportedOperationException, как в статье: так поступает и сама JDK, например Collections.unmodifiableList бросает его при попытке изменить список. Главное, чтобы клиент заранее знал, какие методы не работают.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ