Адаптер
Суть патерна
Адаптер — це структурний патерн проектування, що дає змогу об’єктам із несумісними інтерфейсами працювати разом.
![Патерн Адаптер](/images/patterns/content/adapter/adapter-en.png?id=11ef6ae6177291834323e3f918c47cd2)
Проблема
Уявіть, що ви пишете програму для торгівлі на біржі. Ваша програма спочатку завантажує біржові котирування з декількох джерел в XML, а потім малює гарні графіки.
У якийсь момент ви вирішуєте покращити програму, застосувавши сторонню бібліотеку аналітики. Але от біда — бібліотека підтримує тільки формат даних JSON, несумісний із вашим додатком.
![Структура програми до підключення сторонньої бібліотеки](/images/patterns/diagrams/adapter/problem-en.png?id=60d01f6c72ba85030cd52d5955caa3d8)
Під’єднати сторонню бібліотеку неможливо через несумісність форматів даних.
Ви могли б переписати цю бібліотеку, щоб вона підтримувала формат XML, але, по-перше, це може порушити роботу наявного коду, який уже залежить від бібліотеки, по-друге, у вас може просто не бути доступу до її вихідного коду.
Рішення
Ви можете створити адаптер. Це об’єкт-перекладач, який трансформує інтерфейс або дані одного об’єкта таким чином, щоб він став зрозумілим іншому об’єкту.
Адаптер загортає один з об’єктів так, що інший об’єкт навіть не підозрює про існування першого. Наприклад, об’єкт, що працює в метричній системі вимірювання, можна «обгорнути» адаптером, який буде конвертувати дані у фути.
Адаптери можуть не тільки конвертувати дані з одного формату в інший, але й допомагати об’єктам із різними інтерфейсами працювати разом. Це виглядає так:
- Адаптер має інтерфейс, сумісний з одним із об’єктів.
- Тому цей об’єкт може вільно викликати методи адаптера.
- Адаптер отримує ці виклики та перенаправляє їх іншому об’єкту, але вже в тому форматі та послідовності, які є зрозумілими для цього об’єкта.
Іноді вдається створити навіть двосторонній адаптер, який може працювати в обох напрямках.
![Структура програми після застосування адаптера](/images/patterns/diagrams/adapter/solution-en.png?id=5f4f1b4575236a3853f274b690bd6656)
Програма може працювати зі сторонньою бібліотекою через адаптер.
Таким чином, для програми біржових котирувань ви могли б створити клас XML_To_JSON_Adapter
, який би обгортав об’єкт того чи іншого класу бібліотеки аналітики. Ваш код посилав би адаптеру запити у форматі XML, а адаптер спочатку б транслював вхідні дані у формат JSON, а потім передавав їх методам загорнутого об’єкта аналітики.
Аналогія з життя
![Приклад патерна Адаптер](/images/patterns/content/adapter/adapter-comic-1-uk.png?id=12d290e27da1b159ac662de8719c80c2)
Вміст валіз до й після поїздки за кордон.
Під час вашої першої подорожі за кордон спроба зарядити ноутбук може стати неприємним сюрпризом, тому що стандарти розеток у багатьох країнах різняться. Ваша європейська зарядка стане непотрібом у США без спеціального адаптера, що дозволяє під’єднуватися до розетки іншого типу.
Структура
Адаптер об’єктів
Ця реалізація використовує агрегацію: об’єкт адаптера «загортає», тобто містить посилання на службовий об’єкт. Такий підхід працює в усіх мовах програмування.
![Структура класів патерна Адаптер (адаптер об’єктів)](/images/patterns/diagrams/adapter/structure-object-adapter.png?id=33dffbe3aece294162440c7ddd3d5d4f)
![Структура класів патерна Адаптер (адаптер об’єктів)](/images/patterns/diagrams/adapter/structure-object-adapter-indexed.png?id=a20b311948b361a058097e5bcdbf067a)
-
Клієнт — це клас, який містить існуючу бізнес-логіку програми.
-
Клієнтський інтерфейс описує протокол, через який клієнт може працювати з іншими класами.
-
Сервіс — це який-небудь корисний клас, зазвичай сторонній. Клієнт не може використовувати цей клас безпосередньо, оскільки сервіс має незрозумілий йому інтерфейс.
-
Адаптер — це клас, який може одночасно працювати і з клієнтом, і з сервісом. Він реалізує клієнтський інтерфейс і містить посилання на об’єкт сервісу. Адаптер отримує виклики від клієнта через методи клієнтського інтерфейсу, а потім конвертує їх у виклики методів загорнутого об’єкта в потрібному форматі.
-
Працюючи з адаптером через інтерфейс, клієнт не прив’язується до конкретного класу адаптера. Завдяки цьому ви можете додавати до програми нові види адаптерів, незалежно від клієнтського коду. Це може стати в нагоді, якщо інтерфейс сервісу раптом зміниться, наприклад, після виходу нової версії сторонньої бібліотеки.
Адаптер класів
Ця реалізація базується на спадкуванні: адаптер успадковує обидва інтерфейси одночасно. Такий підхід можливий тільки в мовах, які підтримують множинне спадкування, наприклад у C++.
![Структура класів патерна Адаптер (адаптер класів)](/images/patterns/diagrams/adapter/structure-class-adapter.png?id=e1c60240508146ed3b98ac562cc8e510)
![Структура класів патерна Адаптер (адаптер класів)](/images/patterns/diagrams/adapter/structure-class-adapter-indexed.png?id=250b5c485a7dfba7c16b89a9201538fb)
-
Адаптер класів не потребує вкладеного об’єкта, тому що він може одночасно успадкувати й частину існуючого класу, й частину класу сервісу.
Псевдокод
У цьому жартівливому прикладі Адаптер перетворює один інтерфейс на інший, дозволяючи поєднувати квадратні кілочки та круглі отвори.
![Структура класів прикладу патерна Адаптер](/images/patterns/diagrams/adapter/example.png?id=9d2b6857ce256f2c669383ce4df3d0aa)
Приклад адаптації квадратних кілочків та круглих отворів.
Адаптер обчислює найменший радіус кола, у яке можна вписати квадратний кілочок, і подає його як круглий кілочок із цим радіусом.
// Класи з сумісними інтерфейсами: КруглийОтвір та
// КруглийКілочок.
class RoundHole is
constructor RoundHole(radius) { ... }
method getRadius() is
// Повернути радіус отвору.
method fits(peg: RoundPeg) is
return this.getRadius() >= peg.getRadius()
class RoundPeg is
constructor RoundPeg(radius) { ... }
method getRadius() is
// Повернути радіус круглого кілочка.
// Застарілий несумісний клас: КвадратнийКілочок.
class SquarePeg is
constructor SquarePeg(width) { ... }
method getWidth() is
// Повернути ширину квадратного кілочка.
// Адаптер дозволяє використовувати квадратні кілочки й круглі
// отвори разом.
class SquarePegAdapter extends RoundPeg is
private field peg: SquarePeg
constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg
method getRadius() is
// Обчислити половину діагоналі квадратного кілочка за
// теоремою Піфагора.
return peg.getWidth() * Math.sqrt(2) / 2
// Десь у клієнтському програмному коді.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // TRUE
small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // Помилка компіляції, несумісні типи.
small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // TRUE
hole.fits(large_sqpeg_adapter) // FALSE
Застосування
Якщо ви хочете використати сторонній клас, але його інтерфейс не відповідає решті кодів програми.
Адаптер дозволяє створити об’єкт-прокладку, який перетворюватиме виклики програми у формат, зрозумілий сторонньому класу.
Якщо вам потрібно використати декілька існуючих підкласів, але в них не вистачає якої-небудь спільної функціональності, а розширити суперклас ви не можете.
Ви могли б створити ще один рівень підкласів та додати до них забраклу функціональність. Але при цьому доведеться дублювати один і той самий код в обох гілках підкласів.
Більш елегантним рішенням було б розмістити відсутню функціональність в адаптері й пристосувати його для роботи із суперкласом. Такий адаптер зможе працювати з усіма підкласами ієрархії. Це рішення сильно нагадуватиме патерн Декоратор.
Кроки реалізації
-
Переконайтеся, що у вас є два класи з незручними інтерфейсами:
- корисний сервіс — службовий клас, який ви не можете змінювати (він або сторонній, або від нього залежить інший код);
- один або декілька клієнтів — існуючих класів програми, які не можуть використовувати сервіс через несумісний із ним інтерфейс.
-
Опишіть клієнтський інтерфейс, через який класи програм могли б використовувати клас сервісу.
-
Створіть клас адаптера, реалізувавши цей інтерфейс.
-
Розмістіть в адаптері поле, що міститиме посилання на об’єкт сервісу. Зазвичай це поле заповнюють об’єктом, переданим у конструктор адаптера. Але цей об’єкт можна передавати й безпосередньо до методів адаптера.
-
Реалізуйте всі методи клієнтського інтерфейсу в адаптері. Адаптер повинен делегувати основну роботу сервісу.
-
Програма повинна використовувати адаптер тільки через клієнтський інтерфейс. Це дозволить легко змінювати та додавати адаптери в майбутньому.
Переваги та недоліки
- Відокремлює та приховує від клієнта подробиці перетворення різних інтерфейсів.
- Ускладнює код програми внаслідок введення додаткових класів.
Відносини з іншими патернами
-
Міст проектують заздалегідь, щоб розвивати великі частини програми окремо одну від одної. Адаптер застосовується постфактум, щоб змусити несумісні класи працювати разом.
-
Адаптер надає зовсім інший інтерфейс для доступу до існуючого об’єкта. З іншого боку, з Декоратором інтерфейс або залишається тим самим, або розширюється. Крім того Декоратор підтримує рекурсивну вкладуваність, на відміну від Адаптеру.
-
З Адаптером ви отримуєте доступ до існуючого об’єкта через інший інтерфейс. Використовуючи Замісник, інтерфейс залишається незмінним. Використовуючи Декоратор, ви отримуєте доступ до об’єкта через розширений інтерфейс.
-
Фасад задає новий інтерфейс, тоді як Адаптер повторно використовує старий. Адаптер обгортає тільки один клас, а Фасад обгортає цілу підсистему. Крім того, Адаптер дозволяє двом існуючим інтерфейсам працювати спільно, замість того, щоб визначити повністю новий.
-
Міст, Стратегія та Стан (а також трохи і Адаптер) мають схожі структури класів — усі вони побудовані за принципом «композиції», тобто делегування роботи іншим об’єктам. Проте вони відрізняються тим, що вирішують різні проблеми. Пам’ятайте, що патерни — це не тільки рецепт побудови коду певним чином, але й описування проблем, які призвели до такого рішення.