Адаптер
Суть патерна
Адаптер — це структурний патерн проектування, що дає змогу об’єктам із несумісними інтерфейсами працювати разом.
Проблема
Уявіть, що ви пишете програму для торгівлі на біржі. Ваша програма спочатку завантажує біржові котирування з декількох джерел в XML, а потім малює гарні графіки.
У якийсь момент ви вирішуєте покращити програму, застосувавши сторонню бібліотеку аналітики. Але от біда — бібліотека підтримує тільки формат даних JSON, несумісний із вашим додатком.
Ви могли б переписати цю бібліотеку, щоб вона підтримувала формат XML, але, по-перше, це може порушити роботу наявного коду, який уже залежить від бібліотеки, по-друге, у вас може просто не бути доступу до її вихідного коду.
Рішення
Ви можете створити адаптер. Це об’єкт-перекладач, який трансформує інтерфейс або дані одного об’єкта таким чином, щоб він став зрозумілим іншому об’єкту.
Адаптер загортає один з об’єктів так, що інший об’єкт навіть не підозрює про існування першого. Наприклад, об’єкт, що працює в метричній системі вимірювання, можна «обгорнути» адаптером, який буде конвертувати дані у фути.
Адаптери можуть не тільки конвертувати дані з одного формату в інший, але й допомагати об’єктам із різними інтерфейсами працювати разом. Це виглядає так:
- Адаптер має інтерфейс, сумісний з одним із об’єктів.
- Тому цей об’єкт може вільно викликати методи адаптера.
- Адаптер отримує ці виклики та перенаправляє їх іншому об’єкту, але вже в тому форматі та послідовності, які є зрозумілими для цього об’єкта.
Іноді вдається створити навіть двосторонній адаптер, який може працювати в обох напрямках.
Таким чином, для програми біржових котирувань ви могли б створити клас XML_To_JSON_Adapter
, який би обгортав об’єкт того чи іншого класу бібліотеки аналітики. Ваш код посилав би адаптеру запити у форматі XML, а адаптер спочатку б транслював вхідні дані у формат JSON, а потім передавав їх методам загорнутого об’єкта аналітики.
Аналогія з життя
Під час вашої першої подорожі за кордон спроба зарядити ноутбук може стати неприємним сюрпризом, тому що стандарти розеток у багатьох країнах різняться. Ваша європейська зарядка стане непотрібом у США без спеціального адаптера, що дозволяє під’єднуватися до розетки іншого типу.
Структура
Адаптер об’єктів
Ця реалізація використовує агрегацію: об’єкт адаптера «загортає», тобто містить посилання на службовий об’єкт. Такий підхід працює в усіх мовах програмування.
-
Клієнт — це клас, який містить існуючу бізнес-логіку програми.
-
Клієнтський інтерфейс описує протокол, через який клієнт може працювати з іншими класами.
-
Сервіс — це який-небудь корисний клас, зазвичай сторонній. Клієнт не може використовувати цей клас безпосередньо, оскільки сервіс має незрозумілий йому інтерфейс.
-
Адаптер — це клас, який може одночасно працювати і з клієнтом, і з сервісом. Він реалізує клієнтський інтерфейс і містить посилання на об’єкт сервісу. Адаптер отримує виклики від клієнта через методи клієнтського інтерфейсу, а потім конвертує їх у виклики методів загорнутого об’єкта в потрібному форматі.
-
Працюючи з адаптером через інтерфейс, клієнт не прив’язується до конкретного класу адаптера. Завдяки цьому ви можете додавати до програми нові види адаптерів, незалежно від клієнтського коду. Це може стати в нагоді, якщо інтерфейс сервісу раптом зміниться, наприклад, після виходу нової версії сторонньої бібліотеки.
Адаптер класів
Ця реалізація базується на спадкуванні: адаптер успадковує обидва інтерфейси одночасно. Такий підхід можливий тільки в мовах, які підтримують множинне спадкування, наприклад у C++.
-
Адаптер класів не потребує вкладеного об’єкта, тому що він може одночасно успадкувати й частину існуючого класу, й частину класу сервісу.
Псевдокод
У цьому жартівливому прикладі Адаптер перетворює один інтерфейс на інший, дозволяючи поєднувати квадратні кілочки та круглі отвори.
Адаптер обчислює найменший радіус кола, у яке можна вписати квадратний кілочок, і подає його як круглий кілочок із цим радіусом.
Застосування
Якщо ви хочете використати сторонній клас, але його інтерфейс не відповідає решті кодів програми.
Адаптер дозволяє створити об’єкт-прокладку, який перетворюватиме виклики програми у формат, зрозумілий сторонньому класу.
Якщо вам потрібно використати декілька існуючих підкласів, але в них не вистачає якої-небудь спільної функціональності, а розширити суперклас ви не можете.
Ви могли б створити ще один рівень підкласів та додати до них забраклу функціональність. Але при цьому доведеться дублювати один і той самий код в обох гілках підкласів.
Більш елегантним рішенням було б розмістити відсутню функціональність в адаптері й пристосувати його для роботи із суперкласом. Такий адаптер зможе працювати з усіма підкласами ієрархії. Це рішення сильно нагадуватиме патерн Декоратор.
Кроки реалізації
-
Переконайтеся, що у вас є два класи з незручними інтерфейсами:
- корисний сервіс — службовий клас, який ви не можете змінювати (він або сторонній, або від нього залежить інший код);
- один або декілька клієнтів — існуючих класів програми, які не можуть використовувати сервіс через несумісний із ним інтерфейс.
-
Опишіть клієнтський інтерфейс, через який класи програм могли б використовувати клас сервісу.
-
Створіть клас адаптера, реалізувавши цей інтерфейс.
-
Розмістіть в адаптері поле, що міститиме посилання на об’єкт сервісу. Зазвичай це поле заповнюють об’єктом, переданим у конструктор адаптера. Але цей об’єкт можна передавати й безпосередньо до методів адаптера.
-
Реалізуйте всі методи клієнтського інтерфейсу в адаптері. Адаптер повинен делегувати основну роботу сервісу.
-
Програма повинна використовувати адаптер тільки через клієнтський інтерфейс. Це дозволить легко змінювати та додавати адаптери в майбутньому.
Переваги та недоліки
- Відокремлює та приховує від клієнта подробиці перетворення різних інтерфейсів.
- Ускладнює код програми внаслідок введення додаткових класів.
Відносини з іншими патернами
-
Міст проектують заздалегідь, щоб розвивати великі частини програми окремо одну від одної. Адаптер застосовується постфактум, щоб змусити несумісні класи працювати разом.
-
Адаптер надає зовсім інший інтерфейс для доступу до існуючого об’єкта. З іншого боку, з Декоратором інтерфейс або залишається тим самим, або розширюється. Крім того Декоратор підтримує рекурсивну вкладуваність, на відміну від Адаптеру.
-
З Адаптером ви отримуєте доступ до існуючого об’єкта через інший інтерфейс. Використовуючи Замісник, інтерфейс залишається незмінним. Використовуючи Декоратор, ви отримуєте доступ до об’єкта через розширений інтерфейс.
-
Фасад задає новий інтерфейс, тоді як Адаптер повторно використовує старий. Адаптер обгортає тільки один клас, а Фасад обгортає цілу підсистему. Крім того, Адаптер дозволяє двом існуючим інтерфейсам працювати спільно, замість того, щоб визначити повністю новий.
-
Міст, Стратегія та Стан (а також трохи і Адаптер) мають схожі структури класів — усі вони побудовані за принципом «композиції», тобто делегування роботи іншим об’єктам. Проте вони відрізняються тим, що вирішують різні проблеми. Пам’ятайте, що патерни — це не тільки рецепт побудови коду певним чином, але й описування проблем, які призвели до такого рішення.