Summer SALE

Адаптер

Також відомий як: Wrapper, Обгортка, Adapter

Суть патерна

Адаптер — це структурний патерн проєктування, що дає змогу об’єктам із несумісними інтерфейсами працювати разом.

Патерн Адаптер

Проблема

Уявіть, що ви пишете програму для торгівлі на біржі. Ваша програма спочатку завантажує біржові котирування з декількох джерел в XML, а потім малює гарні графіки.

У якийсь момент ви вирішуєте покращити програму, застосувавши сторонню бібліотеку аналітики. Але от біда — бібліотека підтримує тільки формат даних JSON, несумісний із вашим додатком.

Структура програми до підключення сторонньої бібліотеки

Під’єднати сторонню бібліотеку неможливо через несумісність форматів даних.

Ви могли б переписати цю бібліотеку, щоб вона підтримувала формат XML, але, по-перше, це може порушити роботу наявного коду, який уже залежить від бібліотеки, по-друге, у вас може просто не бути доступу до її вихідного коду.

Рішення

Ви можете створити адаптер. Це об’єкт-перекладач, який трансформує інтерфейс або дані одного об’єкта таким чином, щоб він став зрозумілим іншому об’єкту.

Адаптер загортає один з об’єктів так, що інший об’єкт навіть не підозрює про існування першого. Наприклад, об’єкт, що працює в метричній ��истемі вимірювання, можна «обгорнути» адаптером, який буде конвертувати дані у фути.

Адаптери можуть не тільки конвертувати дані з одного формату в інший, але й допомагати об’єктам із різними інтерфейсами працювати разом. Це виглядає так:

  1. Адаптер має інтерфейс, сумісний з одним із об’єктів.
  2. Тому цей об’єкт може вільно викликати методи адаптера.
  3. Адаптер отримує ці виклики та перенаправляє їх іншому об’єкту, але вже в тому форматі та послідовності, які є зрозумілими для цього об’єкта.

Іноді вдається створити навіть двосторонній адаптер, який може працювати в обох напрямках.

Структура програми після застосування адаптера

Програма може працювати зі сторонньою бібліотекою через адаптер.

Таким чином, для програми біржових котирувань ви могли б створити клас XML_To_JSON_Adapter, який би обгортав об’єкт того чи іншого класу бібліотеки аналітики. Ваш код посилав би адаптеру запити у форматі XML, а адаптер спочатку б транслював вхідні дані у формат JSON, а потім передавав їх методам загорнутого об’єкта аналітики.

Аналогія з життя

Приклад патерна Адаптер

Вміст валіз до й після поїздки за кордон.

Під час вашої першої подорожі за кордон спроба зарядити ноутбук може стати неприємним сюрпризом, тому що стандарти розеток у багатьох країнах різняться. Ваша європейська зарядка стане непотрібом у США без спеціального адаптера, що дозволяє під’єднуватися до розетки іншого типу.

Структура

Адаптер об’єктів

Ця реалізація використовує агрегацію: об’єкт адаптера «загортає», тобто містить посилання на службовий об’єкт. Такий підхід працює в усіх мовах програмування.

Структура класів патерна Адаптер (адаптер об’єктів)Структура класів патерна Адаптер (адаптер об’єктів)
  1. Клієнт — це клас, який містить існуючу бізнес-логіку програми.

  2. Клієнтський інтерфейс описує протокол, через який клієнт може працювати з іншими класами.

  3. Сервіс — це який-небудь корисний клас, зазвичай сторонній. Клієнт не може використовувати цей клас безпосередньо, оскільки сервіс має незрозумілий йому інтерфейс.

  4. Адаптер — це клас, який може одночасно працювати і з клієнтом, і з сервісом. Він реалізує клієнтський інтерфейс і містить посилання на об’єкт сервісу. Адаптер отримує виклики від клієнта через методи клієнтського інтерфейсу, а потім конвертує їх у виклики методів загорнутого об’єкта в потрібному форматі.

  5. Працюючи з адаптером через інтерфейс, клієнт не прив’язується до конкретного класу адаптера. Завдяки цьому ви можете додавати до програми нові види адаптерів, незалежно від клієнтського коду. Це може стати в нагоді, якщо інтерфейс сервісу раптом зміниться, наприклад, після виходу нової версії сторонньої бібліотеки.

Адаптер класів

Ця реалізація базується на спадкуванні: адаптер успадковує обидва інтерфейси одночасно. Такий підхід можливий тільки в мовах, які підтримують множинне спадкування, наприклад у C++.

Структура класів патерна Адаптер (адаптер класів)Структура класів патерна Адаптер (адаптер класів)
  1. Адаптер класів не потребує вкладеного об’єкта, тому що він може одночасно успадкувати й частину існуючого класу, й частину класу сервісу.

Псевдокод

У цьому жартівливому прикладі Адаптер перетворює один інтерфейс на інший, дозволяючи поєднувати квадратні кілоч��и та круглі отвори.

Структура класів прикладу патерна Адаптер

Приклад адаптації квадратних кілочків та круглих отворів.

Адаптер обчислює найменший радіус кола, у яке можна вписати квадратний кілочок, і подає його як круглий кілочок із цим радіусом.

// Класи з сумісними інтерфейсами: КруглийОтвір та
// КруглийКілочок.
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

Застосування

Якщо ви хочете використати сторонній клас, але його інтерфейс не відповідає решті кодів програми.

Адаптер дозволяє створити об’єкт-прокладку, який перетворюватиме виклики програми у формат, зрозумілий сторонньому класу.

Якщо вам потрібно використати декілька існуючих підкласів, але в них не вистачає якої-небудь спільної функціональності, а розширити суперклас ви не можете.

Ви могли б створити ще один рівень підкласів та додати до них забраклу функціональність. Але при цьому доведеться дублювати один і той самий код в обох гілках підкласів.

Більш елегантним рішенням було б розмістити відсутню функціональність в адаптері й пристосувати його для роботи із суперкласом. Такий адаптер зможе працювати з усіма підкласами ієрархії. Це рішення сильно нагадуватиме патерн Декоратор.

Кроки реалізації

  1. Переконайтеся, що у вас є два класи з незручними інтерфейсами:

    • корисний сервіс — службовий клас, який ви не можете змінювати (він або сторонній, або від нього залежить інший код);
    • один або декілька клієнтів — існуючих класів програми, які не можуть використовувати сервіс через несумісний із ним інтерфейс.
  2. Опишіть клієнтський інтерфейс, через який класи програм могли б використовувати клас сервісу.

  3. Створіть клас адаптера, реалізувавши цей інтерфейс.

  4. Розмістіть в адаптері поле, що міститиме посилання на об’єкт сервісу. Зазвичай це поле заповнюють об’єктом, переданим у конструктор адаптера. Але цей об’єкт можна передавати й безпосередньо до методів адаптера.

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

  6. Програма повинна використовувати адаптер тільки через клієнтський інтерфейс. Це дозволить легко змінювати та додавати адаптери в майбутньому.

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

  • Відокремлює та приховує від клієнта подробиці перетворення різних інтерфейсів.
  • Ускладнює код програми внаслідок введення додаткових класів.

Відносини з іншими патернами

  • Міст проектують заздалегідь, щоб розвивати великі частини програми окремо одну від одної. Адаптер застосовується постфактум, щоб змусити несумісні класи працювати разом.

  • Адаптер надає зовсім інший інтерфейс для доступу до існуючого об’єкта. З іншого боку, з Декоратором інтерфейс або залишається тим самим, або розширюється. Крім того Декоратор підтримує рекурсивну вкладуваність, на відміну від Адаптеру.

  • З Адаптером ви отримуєте доступ до існуючого об’єкта через інший інтерфейс. Використовуючи Замісник, інтерфейс залишається незмінним. Використовуючи Декоратор, ви отримуєте доступ до об’єкта через розширений інтерфейс.

  • Фасад задає новий інтерфейс, тоді як Адаптер повторно використовує старий. Адаптер обгортає тільки один клас, а Фасад обгортає цілу підсистему. Крім того, Адаптер дозволяє двом існуючим інтерфейсам працювати спільно, замість того, щоб визначити повністю новий.

  • Міст, Стратегія та Стан (а також трохи і Адаптер) мають схожі структури класів — усі вони побудовані за принципом «композиції», тобто делегування роботи іншим об’єктам. Проте вони відрізняються тим, що вирішують різні проблеми. Пам’ятайте, що патерни — це не тільки рецепт побудови коду певним чином, але й описування проблем, які призвели до такого рішення.

Приклади реалізації патерна

Адаптер на C# Адаптер на C++ Адаптер на Go Адаптер на Java Адаптер на PHP Адаптер на Python Адаптер на Ruby Адаптер на Rust Адаптер на Swift Адаптер на TypeScript