Шаблоны проектирования в PHP : Адаптер

Шаблон проектирования — Адаптер (Adapter)

Название шаблона говорит само за себя. Он помогает адаптировать ваш код к новым требованиям, не существовавшим ранее без изменения исходных классов или интерфейсов. Допустим, что есть проект, веб-сайт, при помощи которого пользователи могут отправлять сообщения в твиттер компании. Например:

  • Post.php — класс для отправки сообщений. Этот объект содержит в себе текст и URL сообщения.
  • Twitter.php — Твиттер класс. Это самописный класс, или позаимствованный, например с packagist.org

Отлично. Вот пример кода, для отправки сообщения:

<?php
// создание сообщения и отправка в Twitter
$post = new Post();
$post->description = 'Моё первое сообщение Twitter!';
$post->url = 'http://devacademy.ru';
$post->send();
class Post
{
   // ...
   public function send()
   {
       $text = $this->description . ' ' . $this->url;
       // любой Twitter класс
       $twitter = new Twitter();
       // аутентификация и т.д.
       $twitter->tweet($text);
   }
}

Здорово. Работает! Прекрасно справляется с поставленной задачей. Но, как обычно бывает, через некоторое время вам говорят, что теперь у пользователей должен быть выбор, между отправкой сообщений в Twitter или Instagram. Не проблема, легко!

<?php
// изменяем класс Post
class Post
{
   public function send($service = 'twitter')
   {
       if ($service == 'twitter') {
           $twitter = new Twitter();
           // ...
           $twitter->tweet();
       } elseif ($service == 'instagram') {
           $instagram = new Instagram();
           // ...
           $instagram->postToInstagram();
       }
   }
}

Вот она, ситуация для использования шаблона Адаптер (Adapter). У нас есть два разных класса, но с похожим функционалом, а для определения куда отправлять сообщение используется условная конструкция if, а это плохо. От неё надо избавиться т.к. если добавится ещё один класс Facebook, то придётся переписывать метод send.

В итоге нам нужно сделать, чтобы класс Post вообще не знал, в какой социальный сервис он отправляет сообщения. Он просто дожен создавать текст сообщения и вызывать определённые методы для их отправки. Проблема в том, что методы для отправки сообщений у классов Twitter и Instagram разные и изменять мы их не можем.

Для решения этой проблемы нужно создать дополнительные обёртки для этих классов. По сути эти обёртки и есть шаблон проектирования Адаптер, он просто преобразует разные интерфейсы одних классов к нужному интерфейсу для других классов (понятнее станет в примере ниже).

Создаем интерфейс службы

Итак, в нашем распоряжении две службы: Twitter и Instagram. Первая использует для отправки сообщений метод tweet(), а вторая — postToInstagram(). Для начала надо создать интерфейс сервиса, включающий в себя методы аутентификации и отправки сообщения в социальный сервис (неважно какой, т.к. у всех таких сервисов, есть авторизация и отправка сообщений). Вот пример такого интерфейса:

<?php
interface ServiceInterface
{
   public function authenticate(array $options);
   public function post($text, $url);
}

В этом интерфейсе присутствует два метода: один для аутентификации, а второй для отправки текста сообщения. Теперь создадим два класса (обратите внимание они реализуют наш интерфейс).

<?php
class TwitterService implements ServiceInterface
{
   protected $service;
   public function __construct()
   {
       $this->service = new Twitter();
   }
   public function authenticate(array $options)
   {
       $apiKey = $options['api_key'];
       // ...
       $this->service->authenticateUsingSomeMethod($apiKey);
       // ...
   }
   public function post($text, $url)
   {
       // ...
       $this->service->tweet($text . ' ' . $url);
       // ...
   }
}
<?php
class InstagramService implements ServiceInterface
{
   protected $service;
   public function __construct()
   {
       $this->service = new Instagram();
       $this->service->someAnotherMethodYouHaveToCall();
   }
   public function authenticate(array $options)
   {
       // ...
       $this->service->authenticateWithInstagramClass();
       // ...
   }
   // Делегериуем работу по отправке сообщения, исходному классу
   public function post($text, $url)
   {
       // ... 
       $this->service->postToInstagram($text);
       // ...
   }
}

Классы TwitterService и InstagramService являются адаптерами для исходных классов Twitter и Instagram. И хотя они по разному реализуют отправку сообщений, для нашего приложения это неважно, т.к. в нём используются классы адаптеры, которые заменяют старую логику на новую. Теперь, чтобы посмотреть как это работает, внесём изменения в класс Post.php.

<?php
class Post
{
   protected $service;
   // Здесь мы говорим, что для отправки нужно использовать адаптер
   public function setServiceAdapter(ServiceInterface $service)
   {
       $this->service = $service;
   }
   public function send()
   {
       $this->service->post($this->description, $this->url);
   }
}

Чтобы использовать класс Post, его сначала надо инициализировать, задав значение параметрам description и url, установить объект сервиса с типом ServiceInterface и вызвать метод send():

<?php
$post = new Post();
// Если нужно отправить сообщение в Twitter
$post->setServiceAdapter(new TwitterService()); // ИЛИ
// Если нужно отправить сообщение в Instagram
$post->setServiceAdapter(new InstagramService());  // ИЛИ
// Если нужно отправить сообщение в Facebook
$post->setServiceAdapter(new FacebookService());
$post->description = 'Моё первое сообщение!';
$post->url = 'http://devacademy.ru';
$post->send();

Таким образом вы сможете создавать новые адаптеры без изменения кода класса Post. Этот класс использует интерфейс и только классы реализующие этот интерфейс будут иметь эти методы, а именно методы для отправки сообщений.

Исходный код шаблона доступен на github: https://github.com/padp/php_modern_design_patterns/tree/master/Adapter

Вот и все. Я надеюсь, что помог вам понять шаблон проектирования — Адаптер. Если у вас есть какие-то замечания и дополнения, я с удовольствием их выслушаю и внесу необходимые изменения в статью. Спасибо что дочитали до конца!