JavaRush /Java блог /Random UA /Які завдання розв'язує шаблон проектування Адаптер

Які завдання розв'язує шаблон проектування Адаптер

Стаття з групи Random UA
Часто розробку програмного забезпечення ускладнює несумісність компонентів, що працюють один з одним. Наприклад, якщо потрібно інтегрувати нову бібліотеку зі старою платформою, написаною на ранніх версіях Java, можна зіткнутися з несумісністю об'єктів, а точніше — інтерфейсів. Що робити у такому разі? Переписати код? Але це неможливо: аналіз системи займе багато часу або буде порушена внутрішня логіка роботи. Які завдання вирішує шаблон проектування Адаптер - 1Для вирішення цієї проблеми вигадали патерн Адаптер, який допомагає об'єктам з несумісними інтерфейсами працювати разом. Давай подивимося, як його використати!

Докладніше про проблему

Для початку зімітуємо поведінку старої системи. Припустимо, вона генерує причини запізнення працювати чи навчання. Для цього є інтерфейс 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) {
       // Добавляет причину в черный список
   }
}
Змінити код не можна. Поточна схема виглядатиме так: Які завдання вирішує шаблон проектування Адаптер - 2Ця версія системи працює лише з інтерфейсом Excuse. Переписувати код не можна: у великій програмі такі редагування можуть затягнутися або порушити логіку програми. Можна запропонувати використання основного інтерфейсу та збільшення ієрархії: Які завдання розв'язує шаблон проектування Адаптер - 3Для цього необхідно перейменувати інтерфейс 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.
   }
}
На перший погляд, це рішення не здається вдалим, але імітування функціоналу може призвести до більш складної ситуації. Якщо клієнт буде уважним, а адаптер добре документований, таке рішення прийнятне.

Коли використовувати Адаптер

  1. Якщо потрібно використовувати сторонній клас, але його інтерфейс не сумісний із основною програмою. На прикладі вище видно, як створюється об'єкт-прокладка, який обертає дзвінки у зрозумілий для цільового об'єкта формат.

  2. Коли у кількох існуючих підкласів має бути загальний функціонал. Замість додаткових підкласів (їх створення призведе до дублювання коду) краще використовувати адаптер.

Переваги і недоліки

Перевага: Адаптер приховує від клієнта подробиці обробки запитів від одного об'єкта до іншого. Клієнтський код не думає про форматування даних або обробку викликів цільового методу. Це занадто складно, а програмісти ліниві :) Недолік: Кодова база проекту ускладнюється додатковими класами, а за великої кількості несумісних точок їх кількість може зрости до неконтрольованих розмірів.

Не плутати з Фасадом та Декоратором

При поверхневому вивченні Адаптер можна переплутати з патернами Фасад та Декоратор. Відмінність Адаптера від Фасаду полягає в тому, що Фасад впроваджує новий інтерфейс та обертає цілу підсистему. Ну а Декоратор, на відміну Адаптера, змінює сам об'єкт, а чи не інтерфейс.

Покроковий алгоритм реалізації

  1. Спочатку переконайся, що є проблема, яку може вирішити цей патерн.

  2. Визнач клієнтський інтерфейс, від імені якого використовуватиметься інший клас.

  3. Реалізуй клас адаптера на основі інтерфейсу, визначеного на попередньому етапі.

  4. У класі адаптера зроби поле, де зберігається посилання на об'єкт. Це посилання передається у конструкторі.

  5. Реалізуй у адаптері усі методи клієнтського інтерфейсу. Метод може:

    • Передавати виклик без зміни;

    • Змінювати дані, збільшувати/зменшувати кількість викликів цільового методу, додатково розширювати склад даних тощо.

    • У крайньому випадку, при несумісності конкретного методу, викинути виняток UnsupportedOperationException, яке суворо потрібно задокументувати.

  6. Якщо програма буде використовувати адаптер тільки через клієнтський інтерфейс (як у прикладі вище), це дозволить безболісно розширювати адаптери в майбутньому.

Само собою, патерн проектування - це не панацея від усіх бід, але з його допомогою можна елегантно вирішити задачу несумісності об'єктів з різними інтерфейсами. Розробник, який знає базові патерни, — на кілька сходинок вище за тих, хто просто вміє писати алгоритми, адже вони потрібні для створення серйозних додатків. Повторно використовувати код стає не так складно, а підтримувати одне задоволення. На сьогодні все! Але ми скоро продовжимо знайомство з різними шаблонами проектування:)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ