Адаптер – це структурний шаблон проектування, який використовується для організації та реалізації методів об’єкта, який можна модифікувати за коштами спеціально розробленого інтерфейсу. Інакше можна сказати, що це структурний шаблон, який дає можливість об’єктів з несумісними інтерфейсами взаємодіяти між собою.
Опис
Патерн Адаптер здійснює адаптацію між класами та об’єктами. Як і будь-адаптер в навколишньому світі, шаблон є інтерфейсом або мостом між двома об’єктами. У реальному світі у нас є адаптери для блоків живлення, для жорстких дисків, для навушників, для карт пам’яті камери і так далі. Для прикладу розглянемо кілька адаптерів для карт пам’яті. Якщо не вдається підключити карту пам’яті камери до ноутбука безпосередньо, можна використовувати адаптер: карта пам’яті камери підключається до адаптера, а адаптер – до роз’єму для ноутбука. Таким чином проблема несумісності інтерфейсів буде дозволена.
У разі розробки програмного забезпечення все виглядає приблизно таким же чином. Можна уявити ситуацію, коли певний клас, очікує якийсь тип об’єкта, і є об’єкт, що пропонує той же функціонал, але з іншим інтерфейсом. Звичайно, вигідно буде використовувати обидва з них, щоб не реалізовувати один з інтерфейсів повторно і не змінювати існуючі класи. Саме в такій ситуації буде розумно використовувати адаптер для проектування програмного забезпечення.
Реалізація
На малюнку нижче показана діаграма класів UML (ЮМЛ) патерну Адаптер.
Класи і об’єкти, які беруть участь у шаблоні проектування:
Застосування
Патерн Адаптер використовується в наступних випадках:
- Коли існує клас (Target), який викликає методи, визначені в інтерфейсі. Крім того, є інший клас (Adapter), який не реалізує інтерфейс, але реалізує операції та методи, які повинні викликатися з першого класу через інтерфейс. У програміста немає можливості змінити ні один з існуючих кодів. Адаптер реалізує свій інтерфейс і стане мостом між двома класами.
- Коли при написанні класу (Target) для загального використання важливо спиратися на деякі загальні інтерфейси, і у розробника є деякі реалізовані класи, не реалізують інтерфейс. Також цей клас (Target) повинен бути викликаною.
Хорошим прикладом для застосування адаптера можуть служити оболонки, використовувані для прийняття сторонніх бібліотек і структур: більшість додатків, що використовують сторонні бібліотеки, вживають адаптер в якості проміжного рівня між додатком і сторонньої бібліотекою для відділення додатки від бібліотеки. Якщо необхідно використовувати іншу бібліотеку, для нової бібліотеки потрібно тільки адаптер без необхідності зміни коду програми.
Адаптери об’єктів на основі делегування
Об’єкт (Adapter) є класичним прикладом шаблону адаптера. Він використовує композицію, а (Adaptee) делегує виклики самому собі, що недоступно адаптерам класів, які розширюють (Adaptee). Така поведінка дає нам кілька переваг перед адаптерами класів, однак адаптери класів можуть бути реалізовані на мовах, що допускають множинне спадкування. Основною перевагою є те, що (Adapter) адаптує не тільки (Adaptee), але і всі його підкласи. Всі ці підкласи існують з одним “невеликим” обмеженням: всі вони не можуть додавати нові методи, тому що використовуваний механізм делегування. Таким чином, для будь-якого нового методу адаптер повинен бути змінений або розширений для надання нових методів. Основним недоліком є те, що він вимагає написання нового коду для делегування всіх необхідних запитів адаптера.
Адаптери класу на основі (множинного) спадкування
Адаптери класів можуть бути реалізовані на мовах, які підтримують множинне спадкування. Мови програмування Java, C# або PHP не підтримує множинне успадкування, однак мають інтерфейси. Таким чином, такі шаблони не можуть бути легко реалізовані в цих мовах. Хорошим прикладом мови програмування, де можна з легкістю реалізувати проектування, є мова C.
Патерн Адаптер використовує спадкування замість композиції. Це означає, що замість того, щоб делегувати виклики (Adaptee), він наслідує його. На закінчення всього адаптер класу повинен розділити на підкласи і (Target), і сам (Adapter).
При такому підході є свої переваги і недоліки:
- Патерн адаптує певний клас (Adaptee). Клас розширює цю адаптацію. Якщо той підклас, він не може бути адаптований існуючим адаптером.
- Шаблон не вимагає весь код, необхідний для делегування, який повинен бути написаний для класу (Adapter).
- Якщо об’єкт (Target) представлений інтерфейсом, а не класом, ми можемо говорити про “класових” адаптерах, тому що ми можемо реалізувати стільки інтерфейсів, скільки захочемо.
Двосторонні адаптери
Двосторонні адаптери – це адаптери, які реалізують обидва інтерфейсу: і (Target), і (Adaptee). Адаптований об’єкт може використовуватися в якості (Target) в нових системах, що працюють з класами (Target), або як (Adaptee) в інших системах, що працюють з класами (Adaptee). Якщо піти далі в цьому напрямку, то у нас можуть бути адаптери, які реалізують n-ное кількість інтерфейсів, адаптуються до n-систем. Двосторонні адаптери і n-смугові адаптери складно реалізувати в системах, що не підтримують множинне спадкування. Якщо адаптер повинен розширювати клас (Target), він не може розширювати інший клас, такий як (Adaptee), тому (Adaptee) повинен бути інтерфейсом, і всі виклики можуть бути делеговані від адаптера об’єкту (Adaptee).
Крім того, якщо (Target) і (Adapter) схожі, то адаптер повинен просто делегувати запити від класу (Target) до класу (Adapter), а якщо (Target) і (Adaptee) не схожі один на одного, то адаптера може знадобитися перетворення структури даних між ними та реалізувати операції, необхідні для (Target), але не реалізовані в класі (Adaptee).
Приклад реалізації
Припустимо, у нас є клас (Bird) з методами fly () і makeSound (). А також клас (ToyDuck) з методом Squeak (). Припустимо, що у нас мало об’єктів (ToyDuck) і ми хочемо використовувати об’єкти (Bird) замість них. Птахи мають схожу функціональність, але реалізують інший інтерфейс, тому ми не можемо використовувати їх безпосередньо. Тому ми будемо використовувати шаблон адаптер. Тут наш (Client) буде (ToyDuck), а (Adaptee) – (Bird). Нижче наведено приклад реалізації проектування патерну Адаптер на Java, одному з найпоширеніших мов програмування.
interface Bird
{
public void fly();
public void makeSound();
}
class Sparrow implements Bird
{
public void fly()
{
System.out.println(“Flying”);
}
public void makeSound()
{
System.out.println(“Chirp Chirp”);
}
}
interface ToyDuck
{
public void squeak();
}
class PlasticToyDuck implements ToyDuck
{
public void squeak()
{
System.out.println(“Squeak”);
}
}
class BirdAdapter implements ToyDuck
{
Bird bird;
public BirdAdapter(Bird bird)
{
this.bird = bird;
}
public void squeak()
{
bird.makeSound();
}
}
class Main
{
public static void main(String args[])
{
Sparrow sparrow = new Sparrow();
ToyDuck toyDuck = new PlasticToyDuck();
ToyDuck birdAdapter = new BirdAdapter(sparrow);
System.out.println(“Sparrow…”);
sparrow.fly();
sparrow.makeSound();
System.out.println(“ToyDuck…”);
toyDuck.squeak();
System.out.println(“BirdAdapter…”);
birdAdapter.squeak();
}
}
Припустимо, у нас є птах, здатна робити Sound (), і пластикова іграшкова качка, яка може пищати – Squeak (). Тепер припустимо, що наш (Client) змінює вимога і хоче, щоб (ToyDuck) виконав Sound (), але як?
Рішення полягає в тому, що ми просто змінимо клас реалізації на новий клас адаптера і скажемо передати клієнту примірник птиці цього класу. От і все. Тепер змінивши лише один рядок, ми навчимо (ToyDuck) чірікать, як горобець.