
Подробнее о проблеме
Для начала сымитируем поведение старой системы. Предположим, она генерирует причины опоздания на работу или учебу. Для этого у есть интерфейс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.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());
Вывод:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Прошу меня извинить за непрофессиональное поведение.
Теперь представим, что ты запустил сервис, собрал статистику и заметил, что большинство пользователей сервиса — студенты вузов. Чтобы улучшить его под нужды этой группы, ты заказал у другого разработчика систему генерации отговорок специально для нее.
Команда разработчика провела исследования, составила рейтинги, подключила искусственный интеллект, добавила интеграцию с пробками на дорогах, погодой и так далее. Теперь у тебя есть библиотека генерации отговорок для студентов, однако интерфейс взаимодействия с ней другой — StudentExcuse
:
public interface StudentExcuse {
String generateExcuse();
void dislikeExcuse(String excuse);
}
У интерфейса есть два метода: generateExcuse
, который генерирует отговорку, и dislikeExcuse
, который блокирует отговорку, чтобы она не появлялась в дальнейшем.
Библиотека стороннего разработчика закрыта для редактирования — ты не можешь изменять его исходный код.
В итоге в твоей системе есть два класса, реализующие интерфейс Excuse
, и библиотека с классом SuperStudentExcuse
, который реализует интерфейс StudentExcuse
:
public class SuperStudentExcuse implements StudentExcuse {
@Override
public String generateExcuse() {
// Логика нового функционала
return "Невероятная отговорка, адаптированная под текущее состояние погоды, пробки или сбои в расписании общественного транспорта.";
}
@Override
public void dislikeExcuse(String excuse) {
// Добавляет причину в черный список
}
}
Изменить код нельзя. Текущая схема будет выглядеть так:


Excuse
. Но дополнительная иерархия нежелательна в серьезных приложениях: внедрение общего корневого элемента нарушает архитектуру.
Следует реализовать промежуточный класс, который позволит использовать новый и старый функционал с минимальными потерями. Словом, тебе нужен адаптер.
Принцип работы паттерна Адаптер
Адаптер — это промежуточный объект, который делает вызовы методов одного объекта понятными другому. Реализуем адаптер для нашего примера и назовем его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
потребовал предварительного помещения отговорки в черный список базы данных. Дополнительная промежуточная обработка данных — причина, по которой любят паттерн Адаптер.
А как быть с методом likeExcuse
, который есть в интерфейсе Excuse
, но нет в StudentExcuse
? Эта операция не поддерживается в новом функционале.
Для такого случая придумали исключение UnsupportedOperationException
: оно выбрасывается, если запрашиваемая операция не поддерживается. Используем это.
Так выглядит новая реализация класса 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.
}
}
На первый взгляд это решение не кажется удачным, но имитирование функционала может привести к более сложной ситуации. Если клиент будет внимателен, а адаптер — хорошо документирован, такое решение приемлемо.
Когда использовать Адаптер
Если нужно использовать сторонний класс, но его интерфейс не совместим с основным приложением. На примере выше видно, как создается объект-прокладка, который оборачивает вызовы в понятный для целевого объекта формат.
Когда у нескольких существующих подклассов должен быть общий функционал. Вместо дополнительных подклассов (их создание приведет к дублированию кода) лучше использовать адаптер.
Преимущества и недостатки
Преимущество: Адаптер скрывает от клиента подробности обработки запросов от одного объекта к другому. Клиентский код не думает о форматировании данных или обработке вызовов целевого метода. Это слишком сложно, а программисты ленивые :) Недостаток: Кодовая база проекта усложняется дополнительными классами, а при большом количестве несовместимых точек их количество может вырасти до неконтролируемых размеров.Не путать с Фасадом и Декоратором
При поверхностном изучении Адаптер можно перепутать с паттернами Фасад и Декоратор. Отличие Адаптера от Фасада заключается в том, что Фасад внедряет новый интерфейс и оборачивает целую подсистему. Ну а Декоратор, в отличие от Адаптера, меняет сам объект, а не интерфейс.Пошаговый алгоритм реализации
Для начала убедись, что есть проблема, которую может решить этот паттерн.
Определи клиентский интерфейс, от имени которого будет использоваться другой класс.
Реализуй класс адаптера на базе интерфейса, определенного на предыдущем шаге.
В классе адаптера сделай поле, в котором хранится ссылка на объект. Эта ссылка передается в конструкторе.
Реализуй в адаптере все методы клиентского интерфейса. Метод может:
Передавать вызов без изменения;
Изменять данные, увеличивать/уменьшать количество вызовов целевого метода, дополнительно расширять состав данных и тд.
В крайнем случае, при несовместимости конкретного метода, выбросить исключение UnsupportedOperationException, которое строго нужно задокументировать.
Если приложение будет использовать адаптер только через клиентский интерфейс (как в примере выше), это позволит безболезненно расширять адаптеры в будущем.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ