Намір

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

Також відомий як

Обгортувач (Wrapper)

Мотивація

Інколи клас інструментарію, який створення для багаторазового використання не може бути використаним через те, що його інтерфейс не співпадає доменно-залежному інтерфейсу, який вимагає програма. Розглянемо приклад графічного редактора, який дозволяє користувачам намалювати і пристосувати графічні елементи (лінії, багатокутники, текст і т.д.) у зображення і діаграми. Ключова абстракція графічного редактора полягає у графічних об'єктах, які мають змінну форму і можуть відмалювати себе. Інтерфейс до графічних об'єктів визначений абстрактним класом, який називається Shape (Форма). Редактор визначає потомок класу Shape кожного типу графічного об'єкту: клас LineShape для ліній, клас PolygonShape для багатокутників і так далі. Класи для елементарних графічних форм на подобі LineShape і PolygonShape скоріш легко реалізувати, через їхні можливості відмальовування і редагування вони спадково обмежені. Але дочірній клас TextShape, який може відображати і редагувати текст порівняно більш складно реалізувати, оскільки навіть базове редагування тексту включає складне оновлення екрану і управління буферами. Тим часом, інструментарій готового інтерфейсу користувача може уже постачати хороший клас TextView для відображення і редагування тексту. В ідеальному випадку, ми б хотіли повторно використати TextView для реалізації TextShape, але інструментарій не створювався з думкою про клас Shape. Отож ми не можемо рівнозначно використати об'єкти TextView і Shape. Як можуть існуючі і незв'язані класи на подобі TextView працювати у програмах які очікують класи з різними і несумісними інтерфейсами? Ми можемо змінити клас TextView так, щоб він відповідав батьківському інтерфейсу Shape, але це не може бути вибором, хіба що ми маємо вихідний код інструментарію. Навіть якщо ми і маємо, це не розумно правити TextView; інструментарій не повинен приймати домено-залежні інтерфейси, просто щоб запрацювала одна програма. Замість того, ми можемо визначати TextShape так, щоб він пристосував інтерфейс TextView до інтерфейсу Shape. Ми можемо виконати це одним з двох способів: (1) успадковуванням інтерфейсу Shape і реалізацію TextView чи (2) компонуванням примірника TextView за допомогою TextShape і реалізовуванням TextShape у термінах інтерфейсу TextView. Ці два способи відповідають класовій і об'єктній версій шаблону Адаптер. Ми назвемо TextShape адаптером. fig.4.fig1_Adapter_example1 Ця діаграма ілюструє випадок адаптера як об'єкта. Він показує як запит BoundingBox, оголошений у класі Shape, перетворюється у запит GetExtent оголошеного у TextView. Оскільки TextShape пристосовує TextView до інтерфейсу Shape, графічний редактор може повторно використати, в іншому випадку несумісний, клас TextView. Часто адаптер відповідальний за функціональність, яку не забезпечує пристосовуваний клас. Діаграма показує як адаптер може виконати такі відповідальності. Користувач повинен мати можливість інтерактивно перетягувати (“drag”) кожен об'єкт Shape у нове місце, але TextView не створений для цього. TextShape може додати дану відсутню функціональність реалізовуванням операції CreateManipulator класу Shape, яка повертає примірник відповідного потомка класу Manipulator. Manipulator являється абстрактним класом для об'єктів, які знають як анімувати примірник класу Shape у відповідь на дії користувача, на подобі перетягування фігури на нове місце розміщення. Існують потомки класу Manipulator для різних фігур; Textmanipulator, наприклад, являється відповідним потомком для TextShape. Повертаючи примірник TextManipulator, TextShape додає функціональність, яка відсутня у класі TextView, але вимагається класом Shape.

Застосовуваність

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

Структура

Клас адаптер використовує множинне успадковування для пристосовування одного інтерфейсу до іншого: fig.4.fig2_class_adapter_structure Об'єкт адаптер покладається на об'єктну композицію: fig.4.fig2_object_adapter_structure

Учасники

  • Target (Ціль — Shape)
    • визначає доменно-залежний інтерфейс, який використовує Client.
  • Client (Клієнт — DrawEditor)
    • співпрацює з об'єктами, які відповідають інтерфейсу Target.
  • Adaptee (адаптоване — TextView)
    • визначає існуючий інтерфейс, який повинен бути пристосованим.
  • Adapter (Адаптер — TextShape)
    • пристосовує інтерфейс класу Adaptee до інтерфейсу Target.

Співробітництво

  • Клієнти викликають операції з примірника Adapter. У свою чергу, адаптер викликає операції Adaptee, які обробляють запит.

Наслідки

Класовий і об'єктний адаптер має різні компроміси. Класовий адаптер
  • пристосовує Adaptee до Target застосовуванням конкретним класом Adapter. Як наслідок, клас адаптер не буде працювати коли ми бажаємо адаптувати клас і усі його потомки.
  • дозволяє класу Adapter перевантажити деяку поведінку Adaptee (Адаптованого), оскільки Adapter являється потомком Adaptee.
  • Впроваджує тільки один об'єкт, і непотрібний додатковий вказівник для того, щоб отримати доступ до адаптованого.
Об'єктний адаптер
  • дозволяє одному адаптеру працювати з багатьма адаптованими - тобто, з самим Adaptee і усіма його потомками (якщо вони існують). Adapter також може додавати функціональність до усіх адаптованих.
  • робить важчим перевантаження поведінки адаптованого (Adaptee). Це вимагатиме створення потомків класу Adaptee і виконання об'єкту Adapter з посиланням на потомок, а не на самий примірник Adaptee.
Ось декілька інших пунктів, які необхідно розглянути під час використання шаблону Адаптер:
  1. Скільки адаптування виконує клас Adapter? Адаптери варіюються у кількості роботи, яку вони виконують для адаптування Adaptee до інтерфейсу Target. Існує спектр можливої роботи, від простого перетворення інтерфейсу, наприклад, зміни імен операцій, до підтримки повністю іншої множини операцій. Кількість роботи, яку виконує Adapter залежить від того, наскільки подібний інтерфейс Target до його Adaptee.
  2. Змінні адаптери. Клас являється частіше повторно використовуваним коли ви мінімізовуєте припущення, які інші класи повинні виконувати,Ж щоб використати його. Вбудовуванням адаптацію інтерфейсу у клас, ви усуваєте припущення, що інші класи бачать той самий інтерфейс. Перегляньте з іншого боку, адаптація інтерфейсу дозволяє нам включати наш клас у існуючі системи, які можуть очікувати інший інтерфейс до класу. ObjectWorks\Smalltalk [Par90] використовують термін змінний (вставний) адаптер (pluggable adapter) для описування класів з вбудованою адаптацією інтерфейсу. Розглянемо віджет TreeDisplay, який може відображати деревоподібні структури графічно. Якщо б це був віджет спеціального призначення для використання тільки в одній програмі, тоді ми можемо вимагати щоб об'єкти, які він відображає, мали спеціальний інтерфейс; тобто, усі повинні походити від абстрактного класу Tree. Але, якщо ми бажаємо зробити TreeDisplay більш багаторазовим (скажем, ми бажаємо зробити його частиною інструментарію корисних віджетів), тоді ця вимога буде безрозсудливою. Програми визначатимуть їхні власні класи для деревоподібних структур. Вони не повинні бути змушеними використовувати наш абстрактний клас Tree. Інші деревоподібні структури будуть мати інші інтерфейси. У ієрархії директорій, наприклад, до потомків можна отримати доступ за допомогою операції GetSubdirectories, а у спадковій ієрархії, відповідна операція може бути названою GetSubclasses. Багаторазовий віджет TreeDisplay повинен бути здатним відображати обидва типи ієрархій навіть якщо вони використовують інші інтерфейси. Іншими словами, TreeDisplay повинен мати вбудовану адаптацію інтерфейсу. Ми переглянемо різні способи вбудувати адаптацію інтерфейсу у класи у секції Реалізація.
  3. Використання двосторонніх адаптерів для забезпечення прозорості. Потенційною проблемою адаптерів полягає у тому, що вони не прозорі для усіх клієнтів. Адаптований об'єкт більше не відповідає інтерфейсу Adaptee, отож він не може бути використаним як є, коли об'єкт Adaptee може. Двосторонні адаптери можуть забезпечувати таку прозорість. Особливо, вони корисні коли два різних клієнти потребують об'єкт з різних точок зору. Переглянемо двосторонній адаптер, який інтегрує Unidraw, фреймворк графічного редактора [VL90] і QOCA, інструментарій спеціального призначення [HHMV92]. Обидві системи мають класи, які явно представляють змінні: Unidraw має StateVariable, а QOCA має ConstraintVariable. Для того щоб Unidraw працював з QOCA, ConstraintVariable повинен бути адаптованим до StateVariable; для того, щоб дозволити QOCA пропагувати рішення до Unidraw, StateVariable повинен бути адаптованим до ConstraintVariable. fig.4.fig3_qoca_unidraw_integration Рішення спричиняє двосторонній клас адаптер ConstraintStateVariable, потомок двох класів StateVariable і ConstraintVariable, який адаптує два інтерфейси один для одного. Множинне успадковування являється хорошим вирішенням у цьому випадку через те, що інтерфейси адаптованих класів є сильно відмінними. Двосторонній клас адаптер відповідає обидвом адаптованим класам і може працювати у кожній системі.

Реалізація

Хоча реалізація Adapter зазвичай проста, ось декілька проблем, які необхідно враховувати:
  1. Реалізування класів адаптерів у С++. У С++ реалізація класових адаптерів, Adapter буде успадкований від Target і приватно від Adaptee. Отже Adapter буде підтипом класу Target але не Adaptee.
  2. Змінні адаптери. Давай переглянемо три способи реалізації змінних адаптерів для віджету TreeDisplay описаного раніше, які можуть планувати і відображати ієрархічні структури автоматично.
Першим кроком, який являється поширеним для усіх трьох реалізацій описаних тут, являється знаходження “обмеженого” інтерфейсу для Adaptee, тобто, найменшу підмножину операцій, які дозволяють нам виконати адаптацію. Обмежений інтерфейс, який складається тільки з декількох операцій, легше адаптувати ніж інтерфейс з дюжиною операцій. Для TreeDisaplay, адаптоване являється будь-якою ієрархічною структурою. Мінімалістичний інтерфейс може включати дві операції, одна яка визначає як представляти вузол у ієрархічній структурі графічно, і інша, як отримувати потомки вузла. Обмежений інтерфейс управляє трьома підходами реалізації:
  • Використання абстрактних операцій. Визначення відповідних абстрактних операцій для обмеженого інтерфейсу Adaptee у класі TreeDisplay. Потомки повинні реалізовувати абстрактні операції і адаптувати об'єкти ієрархічних структур. Наприклад, потомок DirectoryTreeDisplay повинен реалізовувати ці операції отримуючи доступ до структур директорій. fig.4.fig4_treedisplay_implementation_structure DirectoryTreeDisplay спеціалізує обмежений інтерфейс так, що він може відображати структуру директорії, яка виконана з об'єктів FileSystemEntity.
  • Використання делегованих об'єктів. У цьому підході, TreeDisplay передає запити для доступу ієрархічних структур до делегованого об'єкту. TreeDisplay може використати різні стратегії адаптацій заміною іншого делегату. Наприклад, припустимо, що існує DirectoryBrowser, який використовує TreeDisplay. DirectoryBrowser може зробити хорошого делегата для адаптування TreeDisplay до ієрархічної структури директорій. У динамічно типізованих мовах на подобі Smalltalk чи Objective C, цей підхід вимагає тільки інтерфейс для реєстрування делегату з адаптером. Тоді TreeDisplay просто передає запити до делегату. NEXTSTEP [Add94] використовує цей підхід в основному для зменшення потомків. Статично типізовані мови на подобі С++ вимагають визначення явного інтерфейсу для делегату. Ми можемо специфікувати такий інтерфейс, вкладанням обмеженого інтерфейсу, який вимагає TreeDisplay у абстрактний клас TreeAccessorDelegate. Після цього ми можемо змішувати цей інтерфейс з делегатом на наш вибір — DirectoryBrowser у цьому випадку, використовуючи успадковування. Ми використовуємо одинарне успадкування якщо DirectoryBrowser не має існуючого батьківського класу, множинне успадковування у іншому випадку. Змішуючи класи разом цим методом являється легшим ніж впровадження нового потомка TreeDisplay і реалізовування його операцій індивідуально.fig.4.fig5_adapter_using_delegate_objects
  • Параметризовані адаптери. Звичайним способом підтримки змінних адаптерів (pluggable adapters) у Smalltalk являється параметризування адаптеру за допомогою одного чи декількох блоків. Блокова структура підтримує адаптацію без створення потомків класів. Блок може адаптувати запит і адаптер може зберегти блок для кожного індивідуального запиту. У нашому прикладі, це означає, що TreeDisplay зберігає один блок для перетворення вузла у GraphicNode і інший блок для отримання доступу до потомків вузла. Наприклад, для створення TreeDisplay у ієрархії директорій ми пишемо:
    directoryDisplay := 
        (TreeDisplay on: treeRoot) 
            getChildrenBlock: 
                [:node | node getSubdirectories] 
            createGraphicNodeBlock: 
                [:node | node createGraphicNode].
    Якщо ви будуєте адаптацію інтерфейсу у клас, цей підхід пропонує зручну альтернативу для створення потомків.

Приклад Коду

Ми дамо резюмуючий ескіз реалізації класового і об'єктного адаптера для прикладу секції Мотивація для класів Shape і TextView.
class Shape 
{ 

  public: 

      Shape(); 
      virtual void BoundingBox ( Point& bottomLeft,
                                Point& topRight ) const; 
      virtual Manipulator* CreateManipulator () const; 
}; 

class TextView 
{ 

  public: 

      TextView(); 
      void GetOrigin (Coord& x, Coord& y) const; 
      void GetExtent (Coord& width, Coord& height) const; 
      virtual bool IsEmpty () const; 
} ;
Клас Shape припускає межуючу прямокутну область, визначену за допомогою її протилежних кутів. На противагу, TextView визначений за допомогою звичайних висоти і ширини. Shape також визначає операцію CreateManipulator для створення об'єкту Manipulator, який знає як анімувати фігуру, коли користувач маніпулює нею.1 TextView немає еквівалентної операції. Клас TextShape являється адаптером між цими різними інтерфейсами. Класовий адаптер використовує множинне успадкування для адаптування інтерфейсів. Ключовим до класових адаптерів являється використання успадковування одного рукава, для успадкування інтерфейсу і іншого рукава для успадкування реалізації. Звичайний спосіб для того, щоб розділити успадковані властивості у С++, полягає у публічному успадковуванні інтерфейсу і приватному успадковуванні реалізації. Ми використаємо цю домовленість для визначення адаптера TextShape.
class TextShape : public Shape, private TextView 
{ 

  public: 

      TextShape (); 
      virtual void BoundingBox( Point& bottomLeft,
                               Point& topRight ) const; 
      virtual bool IsEmpty () const; 
      virtual Manipulator* CreateManipulator () const; 
} ;
Операція BoundingBox перетворює інтерфейс TextView для його відповідності інтерфейсу Shape.
void TextShape::BoundingBox (Point& bottomLeft,
                             Point& topRight ) const 
{ 
      Coord bottom, left, width, height ; 
      GetOrigin(bottom, left) ; 
      GetExtent(width, height) ; 
      bottomLeft = Point(bottom, left) ; 
      topRight = Point(bottom + height, left + width) ; 
}
Порожня операція демонструє пряме передавання запиту поширеним у реалізації адаптеру:
bool TextShape :: IsEmpty () const 
{ 
      return TextView :: IsEmpty() ; 
}
На кінець, ми самостійно визначаємо і реалізовуємо CreatenManipulator (який не підтримує TextView). Припустимо, що ми реалізували клас TextManipulator, який підтримує маніпулювання TextShape.
Manipulator* TextShape :: CreateManipulator () const 
{ 
      return new TextManipulator(this); 
}
Об'єктний адаптер використовує об'єктну композицію для комбінування класів з різними інтерфейсами. У цьому підході, алаптер TextShape підтримує вказівник до TextView.
class TextShape : public Shape 
{ 

  public: 

      TextShape(TextView*); 
      virtual void BoundingBox ( Point& bottomLeft, 
                                Point& topRight ) const; 
      virtual bool IsEmpty () const; 
      virtual Manipulator* CreateManipulator () const; 

  private: 

      TextView* _text;
} ;
TextShape повинен ініціалізувати вказівник на примірник TextView і він робить це у конструкторі. Він також повинен викликати операції з об'єкта TextView кожного разу, коли викликаються його власні операції. У цьому прикладі, припустимо, що клієнт створює об'єкт TextView і передає його до конструктора TextShape:
TextShape :: TextShape (TextView* t) 
{ 
      _text = t; 
} 

void TextShape :: BoundingBox ( Point& bottomLeft,
                               Point& topRight ) const 
{ 
      Coord bottom, left, width, height; 

      _text->GetOrigin(bottom, left); 
      _text->GetExtent(width, height); 

      bottomLeft = Point(bottom, left); 
      topRight = Point(bottom + height, left + width); 
} 

bool TextShape::IsEmpty () const 
{ 
      return _text->IsEmpty(); 
}
Реалізація CreateManipulator не змінюється з-поміж версій класового адаптера, оскільки він реалізований нами і повторно не використовує існуючу функціональність TextView.
Manipulator* TextShape::CreateManipulator () const 
{ 
     return new TextManipulator(this); 
}
Порівняйте цей код з випадком класового адаптера. Об'єктний адаптер вимагає трохи більше зусиль для написання, але він більш гнучкий. Наприклад, версія об'єктного адаптер TextShape працюватиме так само добре з потомками TextView — клієнт просто передає примірники потомка TextView до конструктора TextShape.

Відомі використання

Приклад у секції Мотивація походить від ET++Draw, графічна програма заснована на ET++ [WGM88]. ET++Draw повторно використовує класи ET++ для редагування тексту використовуючи класовий адаптер TextShape. InterViews 2.6 визначає абстрактний клас Ineractor для лементів інтерфейсу користувача на подобі смуг прокручувань, кнопок і меню [VL88]. Він також визначає абстрактний клас Graphic для структурованих графічних об'єктів на подобі ліній, кілець, багатокутників і сплайнів. Обидва класи Interactors і Graphic мають графічне представлення, але вони мають різні інтерфейси і реалізації (вони не поділяють загальний батьківський клас) і тому несумісні — ви не можете вбудувати структурований графічний об'єкт прямо у, скажемо, діалогове вікно. Замість цього, InterViews 2.6 визначає об'єктний адаптер, який називається GraphicBlock, потомок класу Interactor, який містить примірник Graphic. GraphicBlock адаптує інтерфейс класу Graphic до інтерфейсу класу Interactor. GraphicBlock дозволяє примірнику Graphic бути відображеним, прокручуваним і збільшеним (чи зменшеним) в масштабі всередині структури Interactor. Змінні адаптери (pluggable adapters) широковживані у ObjectWorkl\Smalltalk [Par90]. Стандартний Smalltalk визначає клас VolueModel для представлень, які відображають одне значення. VolueModel визначають value i метод value — інтерфейс для отримання доступу до value. Це є абстрактні методи. Автори програм отримують доступ за допомогою більш доменно-залежних імен на подобі wodth і методу width:, але вони не повинні створювати потомків ValueModel для того, щоб адаптувати такі програмно-залежні імена до інтерфейсу ValueModel. Замість цього, ObjectWorks\Smalltalk включають потомки ValueModel, які називаються PluggableAdaptor. Об'єкт PluggableAdaptor адаптує інші об'єкти до інтерфейсу ValueModel (value, value:). Він може бути параметризований за допомогою блоків для отриманні і встановлення бажаного значення. PluggableAdaptor внутрішньо використовує ці блоки для реалізації інтерфейсу value і value:. PluggableAdaptor також дозволяє вам прямо передати селекторні імена (наприклад, width, width:) для синтаксичної зручності. Він перетворює ці селектори у відповідні блоки автоматично. fig.4.fig6_adapter_in_smalltalk Іншим прикладом з ObjectWorks\Smalltalk являється клас TableAdaptor. TableAdaptor може адаптувати послідовність об'єктів у табличне представлення. Таблиця відображає один об'єкт за рядок. Клієнт параметризує TableAdaptor за допомогою множини повідомлень, які таблиця може використати для отримання значення стовпця з об'єкту. Деякі класи у NeXT AppKit [Add94] використовують об'єкт делегат для виконання адаптації інтерфейсу. Прикладом являється клас NXBrowser, який може відображати ієрархічні списки даних. NXBrowser використовує об'єкт делегат для отримання доступу до даних і їх адаптування. “Міраж сумісності” Майєра [Mey88] являється формою класового адаптера. Майєр описує як FixedStack адаптує реалізацію класу Array до інтерфейсу класу Stack. Результатом являється стек, який містить фіксоване число входжень.

Споріднені шаблони

Міст має структуру подібну до об'єктного адаптера, але міст має інший намір: він призначений розділити інтерфейс від його реалізації, отож вони можуть легко і незалежно варіюватися. Адаптер призначений для зміни інтерфейсу існуючого об'єкту. Декоратор збільшує інший об'єкт без зміни його інтерфейсу. Отож декоратор більш прозорий для програми ніж адаптер. Як наслідок, Декоратор підтримує рекурсивну композицію, що є неможливим в чистому адаптері. Проксі визначає представництво або замінник для іншого об'єкту і не змінює його інтерфейс. 1 CreateManipulator являється прикладом Методу Фабрики.