Подробнее о проблеме
Для начала сымитируем поведение старой системы. Предположим, она генерирует причины опоздания на работу or учебу. Для этого у есть интерфейс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 = {"по невероятному стечению обстоятельств у нас в доме закончилась горячая вода и я ждал, пока солнечный свет, сконцентрированный через лупу, нагреет кружку воды, чтобы я мог умыться.",
"искусственный интеллект в моем будильнике подвел меня и разбудил на час раньше обычного. Поскольку сейчас зима, я думал что еще ночь и уснул. Дальше все How в тумане.",
"предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице."};
private String[] sorryOptions = {"Это, конечно, не повторится, мне очень жаль.", "Прошу меня извинить за непрофессиональное поведение.", "Нет оправдания моему поступку. Я недостоин этой должности."};
@Override
public String generateExcuse() { // Случайно выбираем отговорку из массива
String result = "Я сегодня опоздал, потому что " + reasonOptions[(int) Math.round(Math.random() + 1)] + "\n" +
sorryOptions[(int) Math.round(Math.random() + 1)];
return result;
}
@Override
public void likeExcuse(String excuse) {
// Дублируем элемент в массиве, чтобы шанс его выпадения был выше
}
@Override
public void dislikeExcuse(String excuse) {
// Удаляем элемент из массива
}
}
Протестируем пример:
Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Вывод:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Прошу меня извинить за непрофессиональное поведение.
Теперь представим, что ты запустил сервис, собрал статистику и заметил, что большинство пользователей сервиса — студенты вузов. Whatбы улучшить его под нужды этой группы, ты заказал у другого разработчика систему генерации отговорок специально для нее. Команда разработчика провела исследования, составила рейтинги, подключила искусственный интеллект, добавила интеграцию с пробками на дорогах, погодой и так далее. Теперь у тебя есть библиотека генерации отговорок для студентов, однако интерфейс взаимодействия с ней другой — StudentExcuse
:
public interface StudentExcuse {
String generateExcuse();
void dislikeExcuse(String excuse);
}
У интерфейса есть два метода: generateExcuse
, который генерирует отговорку, и dislikeExcuse
, который блокирует отговорку, чтобы она не появлялась в дальнейшем. Библиотека стороннего разработчика закрыта для редактирования — ты не можешь изменять его исходный code. В итоге в твоей системе есть два класса, реализующие интерфейс Excuse
, и библиотека с классом SuperStudentExcuse
, который реализует интерфейс StudentExcuse
:
public class SuperStudentExcuse implements StudentExcuse {
@Override
public String generateExcuse() {
// Логика нового функционала
return "Невероятная отговорка, адаптированная под текущее состояние погоды, пробки or сбои в расписании общественного транспорта.";
}
@Override
public void dislikeExcuse(String excuse) {
// Добавляет причину в черный список
}
}
Изменить code нельзя. Текущая схема будет выглядеть так: Эта version системы работает только с интерфейсом Excuse. Переписывать code нельзя: в большом приложении такие правки могут затянуться or нарушить логику applications. Можно предложить внедрение основного интерфейса и увеличение иерархии: Для этого нужно переименовать интерфейс Excuse
. Но дополнительная иерархия нежелательна в серьезных applicationsх: внедрение общего корневого element нарушает архитектуру. Следует реализовать промежуточный класс, который позволит использовать новый и старый функционал с минимальными потерями. Словом, тебе нужен адаптер.
Принцип работы паттерна Адаптер
Адаптер — это промежуточный an object, который делает вызовы методов одного an object понятными другому. Реализуем адаптер для нашего примера и назовем егоMiddleware
. Наш адаптер должен реализовывать интерфейс, совместимый с одним из an objectов. Пусть это будет Excuse
. Благодаря этому Middleware
может вызывать методы первого an object. Middleware
получает вызовы и передает их второму an objectу в совместимом формате. Так выглядит реализация метода Middleware
с методами generateExcuse
и dislikeExcuse
:
public class Middleware implements Excuse { // 1. Middleware становится совместимым с an objectом WorkExcuse через интерфейс Excuse
private StudentExcuse superStudentExcuse;
public Middleware(StudentExcuse excuse) { // 2. Получаем ссылку на адаптируемый an object
this.superStudentExcuse = excuse;
}
@Override
public String generateExcuse() {
return superStudentExcuse.generateExcuse(); // 3. Адаптер реализовывает метод интерфейса
}
@Override
public void dislikeExcuse(String excuse) {
// Метод предварительно помещает отговорку в черный список БД,
// Затем передает ее в метод dislikeExcuse an object superStudentExcuse.
}
// Метод likeExcuse появятся позже
}
Тестирование (в клиентском codeе):
public class Test {
public static void main(String[] args) {
Excuse excuse = new WorkExcuse(); // Создаются an objectы классов,
StudentExcuse newExcuse = new SuperStudentExcuse(); // Которые должны быть совмещены.
System.out.println("Обычная причина для работника:");
System.out.println(excuse.generateExcuse());
System.out.println("\n");
Excuse adaptedStudentExcuse = new Middleware(newExcuse); // Оборачиваем новый функционал в an object-адаптер
System.out.println("Использование нового функционала с помощью адаптера:");
System.out.println(adaptedStudentExcuse.generateExcuse()); // Адаптер вызывает адаптированный метод
}
}
Вывод:
Обычная причина для работника:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Нет оправдания моему поступку. Я недостоин этой должности.
Использование нового функционала с помощью адаптера
Невероятная отговорка, адаптированная под текущее состояние погоды, пробки or сбои в расписании общественного транспорта. В методе generateExcuse
выполнена простая передача вызова другому an objectу, без дополнительных преобразований. Метод dislikeExcuse
потребовал предварительного помещения отговорки в черный список базы данных. Дополнительная промежуточная обработка данных — причина, по которой любят паттерн Адаптер. А How быть с методом likeExcuse
, который есть в интерфейсе Excuse
, но нет в StudentExcuse
? Эта операция не поддерживается в новом функционале. Для такого случая придумали исключение UnsupportedOperationException
: оно выбрасывается, если запрашиваемая операция не поддерживается. We use это. Так выглядит новая реализация класса 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 an object superStudentExcuse.
}
}
На первый взгляд это решение не кажется удачным, но имитирование функционала может привести к более сложной ситуации. Если клиент будет внимателен, а адаптер — хорошо documentирован, такое решение приемлемо.
Когда использовать Адаптер
-
Если нужно использовать сторонний класс, но его интерфейс не совместим с основным приложением. На примере выше видно, How создается an object-прокладка, который оборачивает вызовы в понятный для целевого an object формат.
-
Когда у нескольких существующих подклассов должен быть общий функционал. Вместо дополнительных подклассов (их создание приведет к дублированию codeа) лучше использовать адаптер.
Преимущества и недостатки
Преимущество: Адаптер скрывает от клиента подробности обработки requestов от одного an object к другому. Клиентский code не думает о форматировании данных or обработке вызовов целевого метода. Это слишком сложно, а программисты ленивые :) Недостаток: Кодовая база проекта усложняется дополнительными классами, а при большом количестве несовместимых точек их количество может вырасти до неконтролируемых размеров.Не путать с Фасадом и Декоратором
При поверхностном изучении Адаптер можно перепутать с паттернами Фасад и Декоратор. Отличие Адаптера от Фасада заключается в том, что Фасад внедряет новый интерфейс и оборачивает целую подсистему. Ну а Декоратор, в отличие от Адаптера, меняет сам an object, а не интерфейс.Пошаговый алгоритм реализации
-
Для начала убедись, что есть проблема, которую может решить этот паттерн.
-
Определи клиентский интерфейс, от имени которого будет использоваться другой класс.
-
Реализуй класс адаптера на базе интерфейса, определенного на предыдущем шаге.
-
В классе адаптера сделай поле, в котором хранится link на an object. Эта link передается в конструкторе.
-
Реализуй в адаптере все методы клиентского интерфейса. Метод может:
-
Передавать вызов без изменения;
-
Изменять данные, увеличивать/уменьшать количество вызовов целевого метода, дополнительно расширять состав данных и тд.
-
В крайнем случае, при несовместимости конкретного метода, выбросить исключение UnsupportedOperationException, которое строго нужно заdocumentировать.
-
-
Если приложение будет использовать адаптер только через клиентский интерфейс (How в примере выше), это позволит безболезненно расширять адаптеры в будущем.
GO TO FULL VERSION