- Суть паттерна
Адаптер - это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.
- Проблема
Представьте, что вы делаете приложение для торговли на бирже. Ваше приложение скачивает биржевые котировки из нескольких источников в XML, а затем рисует красивые графики. В какой-то момент вы решаете улучшить приложение, применив стороннюю библиотеку аналитики. Но вот беда - библиотека поддерживает только формат данных JSON, несовместимый с вашим приложением.
Вы смогли бы переписать библиотеку, чтобы та поддерживала формат XML. Но, во-первых, это может нарушить работу существующего кода, который уже зависит от библиотеки. А во-вторых, у вас может просто не быть доступа к её исходному коду.
- Решение
Вы можете создать адаптер. Это объект-переводчик, который трансформирует интерфейс или данные одного объекта в такой вид, чтобы он стал понятен другому объекту.
При этом адаптер оборачивает один из объектов, так что другой объект даже не знает о наличии первого. Например, вы можете обернуть объект, работающий в метрах, адаптером, который бы конвертировал данные в футы. Адаптеры могут не только переводить данные из одного формата в другой, но и помогать объектам с разными интерфейсами работать сообща. Это работает так:
Иногда возможно создать даже двухсторонний адаптер, который работал бы в обе стороны.
Таким образом, в приложении биржевых котировок вы могли бы создать класс XML_To_JSON_Adapter, который бы оборачивал объект того или иного класса библиотеки аналитики. Ваш код посылал бы адаптеру запросы в формате XML, а адаптер сначала транслировал входящие данные в формат JSON, а затем передавал бы их методам обёрнутого объекта аналитики.
- Структура
Адаптер объектов
Эта реализация использует агрегацию: объект адаптера "оборачивает", то есть содержит ссылку на служебный объект. Такой подход работает во всех языках программирования.
Адаптер классов
Эта реализация базируется на наследовании: адаптер наследует оба интерфейса одновременно. Такой подход возможен только в языках, поддерживающих множественное наследование, например, C++.
- Шаги реализации
- Преимущества
- Недостатки
Adapter - это структурный паттерн, который позволяет подружить несовместимые объекты. Адаптер выступает прослойкой между двумя объектами, превращая вызовы одного в вызовы понятные другому.
Концептуальный пример
/** * Целевой класс объявляет интерфейс, с которым может работать клиентский код. */ class Target { public function request() { return "Target: The default target's behavior."; } } /** * Адаптируемый класс содержит некоторое полезное поведение, но его интерфейс * несовместим с существующим клиентским кодом. Адаптируемый класс нуждается в * некоторой доработке, прежде чем клиентский код сможет его использовать. */ class Adaptee { public function specificRequest() { return ".eetpadA eht fo roivaheb laicepS"; } } /** * Адаптер делает интерфейс Адаптируемого класса совместимым с целевым интерфейсом. */ class Adapter extends Target { private $adaptee; public function __construct($adaptee) { $this->adaptee = $adaptee; } public function request() { return "Adapter: (TRANSLATED) " . strrev($this->adaptee->specificRequest()); } } /** * Клиентский код поддерживает все классы, использующие целевой интерфейс. */ function clientCode($target) { echo $target->request(); } echo "Client: I can work just fine with the Target objects:\n"; $target = new Target(); clientCode($target); echo "\n\n"; $adaptee = new Adaptee(); echo "Client: The Adaptee class has a weird interface. See, I don't understand it:\n"; echo "Adaptee: " . $adaptee->specificRequest(); echo "\n\n"; echo "Client: But I can work with it via the Adapter:\n"; $adapter = new Adapter($adaptee); clientCode($adapter);
Результат выполнения:
Client: I can work just fine with the Target objects: Target: The default target's behavior. Client: The Adaptee class has a weird interface. See, I don't understand it: Adaptee: .eetpadA eht fo roivaheb laicepS Client: But I can work with it via the Adapter: Adapter: (TRANSLATED) Special behavior of the Adaptee.
Пример из реальной жизни
Паттерн Адаптер позволяет использовать сторонние или устаревшие классы, даже если они несовместимы с основной частью кода. Например, вместо того, чтобы переписывать интерфейс уведомлений вашего приложения для поддержки каждого стороннего сервиса вроде Slack, Facebook, SMS и прочих, вы создаёте под эти сервисы набор специальных обёрток, которые приводят вызовы из приложения к требуемым сторонними классами интерфейсу и формату.
/** * Целевой интерфейс предоставляет интерфейс, которому следуют классы вашего приложения. */ interface Notification { public function send($title, $message); } /** * Вот пример существующего класса, который следует за целевым интерфейсом. * * Дело в том, что у большинства приложений нет чётко определённого интерфейса. * В этом случае лучше было бы расширить Адаптер за счёт существующего класса * приложения. Если это неудобно (например, SlackNotification не похож на * подкласс EmailNotification), тогда первым шагом должно быть извлечение интерфейса. */ class EmailNotification implements Notification { private $adminEmail; public function __construct($adminEmail) { $this->adminEmail = $adminEmail; } public function send($title, $message) { mail($this->adminEmail, $title, $message); echo "Sent email with title '$title' to '{$this->adminEmail}' that says '$message'."; } } /** * Адаптируемый класс – некий полезный класс, несовместимый с целевым * интерфейсом. Нельзя просто войти и изменить код класса так, чтобы следовать * целевому интерфейсу, так как код может предоставляться сторонней библиотекой. */ class SlackApi { private $login; private $apiKey; public function __construct($login, $apiKey) { $this->login = $login; $this->apiKey = $apiKey; } public function logIn() { // Send authentication request to Slack web service. echo "Logged in to a slack account '{$this->login}'.\n"; } public function sendMessage($chatId, $message) { // Send message post request to Slack web service. echo "Posted following message into the '$chatId' chat: '$message'.\n"; } } /** * Адаптер – класс, который связывает Целевой интерфейс и Адаптируемый класс. * Это позволяет приложению использовать Slack API для отправки уведомлений. */ class SlackNotification implements Notification { private $slack; private $chatId; public function __construct($slack, $chatId) { $this->slack = $slack; $this->chatId = $chatId; } /** * Адаптер способен адаптировать интерфейсы и преобразовывать входные данные * в формат, необходимый Адаптируемому классу. */ public function send($title, $message) { $slackMessage = "#" . $title . "# " . strip_tags($message); $this->slack->logIn(); $this->slack->sendMessage($this->chatId, $slackMessage); } } /** * Клиентский код работает с классами, которые следуют Целевому интерфейсу. */ function clientCode($notification) { // ... echo $notification->send("Website is down!", "<strong style='color:red;font-size: 50px;'>Alert!</strong> " . "Our website is not responding. Call admins and bring it up!"); // ... } echo "Client code is designed correctly and works with email notifications:\n"; $notification = new EmailNotification("developers@example.com"); clientCode($notification); echo "\n\n"; echo "The same client code can work with other classes via adapter:\n"; $slackApi = new SlackApi("example.com", "XXXXXXXX"); $notification = new SlackNotification($slackApi, "Example.com Developers"); clientCode($notification);
Результат выполнения:
Client code is designed correctly and works with email notifications: Sent email with title 'Website is down!' to 'developers@example.com' that says '<strong style='color:red;font-size: 50px;'>Alert!</strong> Our website is not responding. Call admins and bring it up!'. The same client code can work with other classes via adapter: Logged in to a slack account 'example.com'. Posted following message into the 'Example.com Developers' chat: '#Website is down!# Alert! Our website is not responding. Call admins and bring it up!'.