Докладніше про проблему
Для початку зімітуємо поведінку старої системи. Припустимо, вона генерує причини запізнення працювати чи навчання. Для цього є інтерфейс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. Переписувати код не можна: у великій програмі такі редагування можуть затягнутися або порушити логіку програми. Можна запропонувати використання основного інтерфейсу та збільшення ієрархії: Для цього необхідно перейменувати інтерфейс 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, яке суворо потрібно задокументувати.
-
-
Якщо програма буде використовувати адаптер тільки через клієнтський інтерфейс (як у прикладі вище), це дозволить безболісно розширювати адаптери в майбутньому.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ