🎉 Ура! Після трьох років роботи, я нарешті випустив англійську версію книжки про патерни! Ось вона »
Також відомий як: Обгортка, 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.radius()

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(2)
large_sqpeg = new SquarePeg(5)
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. Програма повинна використовувати адаптер тільки через клієнтський інтерфейс. Це дозволить легко змінювати та додавати адаптери в майбутньому.

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

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

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

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

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

  • Адаптер надає класу альтернативний інтерфейс. Декоратор надає розширений інтерфейс. Заступник надає той самий інтерфейс.

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

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

Реалізація на різних мовах програмування

Адаптер Java Адаптер C# Адаптер PHP