Адаптер (шаблон проєктування)
Адаптер, Adapter — структурний шаблон проєктування, призначений для організації використання функцій об'єкту, недоступного для модифікації, через спеціально створений інтерфейс.
Призначення
Адаптує інтерфейс одного класу в інший, очікуваний клієнтом. Адаптер забезпечує роботу класів з несумісними інтерфейсами, та найчастіше застосовується тоді, коли система підтримує необхідні дані і поведінку, але має невідповідний інтерфейс.
Застосування
Адаптер передбачає створення класу-оболонки з необхідним інтерфейсом.
Структура
Учасники
Клас Adapter
приводить інтерфейс класу Adaptee
у відповідність з інтерфейсом класу Target
(спадкоємцем якого є Adapter
). Це дозволяє об'єктові Client
використовувати об'єкт Adaptee
так, немов він є екземпляром класу Target
.
Наслідки
Шаблон Адаптер дозволяє включати вже існуючі об'єкти в нові об'єктні структури, незалежно від відмінностей в їхніх інтерфейсах.
Переваги та недоліки
Переваги
- Допомагає досягти багаторазового використання та гнучкості.
- Клас клієнта не ускладнюється необхідністю використовувати інший інтерфейс і може використовувати поліморфізм для обміну між різними реалізаціями адаптерів.
Недоліки
- Всі запити пересилаються, тому спостерігається незначне збільшення накладних витрат.
- Іноді багато адаптацій потрібні по ланцюгу адаптера, щоб досягти потрібного типу.
Зв'язок з іншими патернами
- Фасад створює новий інтерфейс доступу, адаптер — використовує старий
Реалізація
Інтерфейс класу Adaptee
, тобто того, який адаптується, приводиться у відповідність з новими вимогами класу Target
, а виклики його методів перетворяться у виклики методів класу Target
.
Шаблон Адаптер для адаптації інтерфейсу методу/ів Adaptee
до інтерфейсу Target
в Adapter
можна реалізувати як мінімум двома способами: використовуючи композицію+успадкування (Адаптер об'єкта), або використовуючи множинне успадкування (Aдаптер класу).
- Адаптер об'єкта:
Adapter
наслідує інтерфейс відTarget
(успадкування) та містить примірник (здебільшого як вказівник) класуAdaptee
(композиція) і делегує виклики своїх методів (які збігаються з інтерфейсомTarget
) доAdaptee
- Адаптер класу:
Adapter
наслідує інтерфейси обох класівTarget
таAdaptee
(множинне успадкування). В ООП-мовах, які не підтримують множинне успадкування, реалізація цього варіанту адаптера дещо складніша (наприклад в Java за допомогою інтерфейсів).
C++
#include <iostream>
using namespace std;
// Target Ціль
struct CShape
{
virtual ~CShape() {};
virtual void Draw() { cout << "Rectangle" << endl; };
};
// Adapter Будемо адаптувати
struct CTextViewer
{
CTextViewer() {};
virtual ~CTextViewer() {};
void DrawText() { cout << "Text" << endl; };
};
// Adapter
// Адаптер класу
struct CTextShape : public CShape, protected CTextViewer
{
CTextShape() {};
virtual ~CTextShape() {};
virtual void Draw()
{
DrawText(); // Виклик метода класу, що адаптуємо
};
};
// Адаптер об’єкта
struct CTextShapeOBJ // : public CShape
{
CTextViewer shape;
virtual void Draw()
{
shape.DrawText(); // Виклик метода класу, що адаптуємо
};
};
// Client
void DrawObject(CShape* p)
{
p->Draw();
}
void main()
{
// Створюємо адаптер і малюємо об'єкт
CShape* obj = new CTextShape();
DrawObject(obj);
delete obj;
CTextShapeOBJ adapt;
CTextViewer obj2;
adapt.shape = obj2;
adapt.Draw();
}
Java
interface LightningPhone {
void recharge();
void useLightning();
}
interface MicroUsbPhone {
void recharge();
void useMicroUsb();
}
class Iphone implements LightningPhone {
private boolean connector;
@Override
public void useLightning() {
connector = true;
System.out.println("Lightning connected");
}
@Override
public void recharge() {
if (connector) {
System.out.println("Recharge started");
System.out.println("Recharge finished");
} else {
System.out.println("Connect Lightning first");
}
}
}
class Android implements MicroUsbPhone {
private boolean connector;
@Override
public void useMicroUsb() {
connector = true;
System.out.println("MicroUsb connected");
}
@Override
public void recharge() {
if (connector) {
System.out.println("Recharge started");
System.out.println("Recharge finished");
} else {
System.out.println("Connect MicroUsb first");
}
}
}
/* exposing the target interface while wrapping source object */
class LightningToMicroUsbAdapter implements MicroUsbPhone {
private final LightningPhone lightningPhone;
public LightningToMicroUsbAdapter(LightningPhone lightningPhone) {
this.lightningPhone = lightningPhone;
}
@Override
public void useMicroUsb() {
System.out.println("MicroUsb connected");
lightningPhone.useLightning();
}
@Override
public void recharge() {
lightningPhone.recharge();
}
}
public class AdapterDemo {
static void rechargeMicroUsbPhone(MicroUsbPhone phone) {
phone.useMicroUsb();
phone.recharge();
}
static void rechargeLightningPhone(LightningPhone phone) {
phone.useLightning();
phone.recharge();
}
public static void main(String[] args) {
Android android = new Android();
Iphone iPhone = new Iphone();
System.out.println("Recharging android with MicroUsb");
rechargeMicroUsbPhone(android);
System.out.println("Recharging iPhone with Lightning");
rechargeLightningPhone(iPhone);
System.out.println("Recharging iPhone with MicroUsb");
rechargeMicroUsbPhone(new LightningToMicroUsbAdapter (iPhone));
}
}
Output
Recharging android with MicroUsb MicroUsb connected Recharge started Recharge finished Recharging iPhone with Lightning Lightning connected Recharge started Recharge finished Recharging iPhone with MicroUsb MicroUsb connected Lightning connected Recharge started Recharge finished
Зауваження і коментарі
Шаблон Адаптер дозволяє в процесі проєктування не приймати до уваги можливі відмінності в інтерфейсах вже існуючих класів. Якщо є клас, що володіє необхідними методами і властивостями (принаймні, концептуально), то при необхідності завжди можна скористатися шаблоном Адаптер для приведення його інтерфейсу до потрібного вигляду.
Більш прийнятним є адаптер об'єкта, в якому використовується композиція+успадкування, оскільки це більш відповідає правилу "надавайте перевагу композиції, а не успадкуванню". Цей адаптер можна використовувати тільки односторонньо - як заміну для Target. Однак, у випадку, коли ми створюємо двосторонній адаптер, або ж адаптер, який адаптує одночасно кілька Adaptee класів, слід надавати перевагу шаблону адаптера класу. Також адаптер класу дозволяє більш ефективно використовувати вже реалізований код з Target та Adaptee. Однак недоліки адаптера класу випливають з множинного успадкування, коли зміни в деяких базових класах викликають непередбачливі зміни в похідних, а особливо, коли це відбувається одночасно в кількох успадкованих адаптером класах.
Близьким Адаптеру є шаблон Фасад, не завжди можна відрізнити один від другого. Різниця полягає в тому, що шаблон Фасад призначений для спрощення інтерфейсу і створює новий інтерфейс, тоді як шаблон Адаптер використовує з обох сторін інтерфейси, які є в наявності, і забезпечує їх функціювання.
Відомі застосування
Типовим прикладом використання шаблону Адаптер можна назвати створення класів, що приводять до єдиного інтерфейсу функції мови PHP що забезпечують доступ до різних СУБД[1].
Примітки
- В мові PHP доступ до СУБД реалізований у вигляді набору функцій, для кожної СУБД вони мають різні найменування і, іноді, різний набір використовуваних параметрів, що приводить до значних проблем при переході з однією СУБД на іншу, якщо такий перехід наперед не забезпечений використанням шаблону Адаптер.
Джерела
Література
Алан Шаллоуей, Джеймс Р. Тротт. Шаблоны проектирования. Новый подход к объектно-ориентированному анализу и проектированию = Design Patterns Explained: A New Perspective on Object-Oriented Design. — М. : «Вильямс», 2002. — 288 с. — ISBN 0-201-71594-5.