Шаблон программирования «Текучий интерфейс» в PHP. Свежий взгляд

от автора


При разработке программного обеспечения одной из важных составляющих является высокая читабельность исходного кода программы. Существуют специальные методики и рекомендации, которые позволяют добиться улучшения читабельности исходного кода. Одной из методик улучшения читабельности исходного кода является применение «текучих интерфейсов» (англ. Fluent Interface). О нем мы и поговорим в данной статье.

Эволюция. От простого к сложному.

Могу предположить, что каждый программист начинает свой путь PHP-программиста с написания банального приложения «Hello, world!». После которого будут идти долгие года изучения языка и неуклюжие попытки сделать что-то особенное: ORM/CMS/Framework (нужное подчеркнуть). Думаю, у всех есть тот код, который лучше никому не показывать. Но это абсолютно нормальный процесс развития, потому что без понимания простых вещей нельзя разобраться в сложных! Поэтому, давайте, повторим этот путь — начнем с простых примеров и дойдем до реализации «текучего» интерфейса в виде отдельного класса с помощью АОП. Те, кто знает этот шаблон программирования в ООП — могут смело переходить к последней части статьи, там можно получить отличную пищу для размышлений.

Приступим-с

Чтобы нам не пришлось ходить далеко за примером, давайте возьмем некоторую сущность пользователя, у которого есть свойства имени, фамилии, а также пароля:

class User {     public $name;     public $surname;     public $password; } 

Превосходный класс, который можно легко и изящно использовать:

$user = new User;  $user->name     = 'John'; $user->surname  = 'Doe'; $user->password = 'root'; 

Однако легко заметить, что у нас нет никакой валидации и можно сделать пароль пустым, что не очень хорошо. Помимо этого, было бы неплохо знать, что значения полей не изменяются без нашего ведома (Immutable). Эти несколько соображений приводят нас к мысли о том, что свойства должны быть защищенными или приватными, а доступ к ним осуществлять через пару геттер/сеттер. (примечание: этот подход как раз и лежит в основе прокси-классов Doctrine)

Сказано-сделано:

class User {     protected $name;     protected $surname;     protected $password;      public function setName($name)     {         $this->name = $name;     }      public function setSurname($surname)     {         $this->surname = $surname;     }      public function setPassword($password)     {         if (!$password) {             throw new InvalidArgumentException("Password shouldn't be empty");         }         $this->password = $password;     } } 

Для нового класса конфигурация немного изменилась и теперь использует вызов методов-сеттеров:

$user = new User;  $user->setName('John'); $user->setSurname('Doe'); $user->setPassword('root');  

Вроде, ничего сложного, ведь так? А что если нам надо настроить 20 свойств? 30 свойств? Этот код будет засыпан вызовами сеттеров и постоянным появлением $user-> Если же имя переменной будет $superImportantUser, то читаемость кода ухудшится еще больше. Что же можно предпринять, чтобы избавиться от копирования этого кода?

Текучий интерфейс

Итак, мы подошли к шаблону программирования Fluent Interface, который был придуман Эриком Эвансом и Мартином Фаулером для повышения читабельности исходного кода программы за счет упрощения множественных вызовов методов одного объекта. Реализуется это с помощью цепочки методов (Method Chaining), передающих контекст вызова следующему методу в цепочке. Контекстом является значение, возвращаемое методом и этим значением может быть любой объект, включая текущий.

Если быть проще, то для реализации текучего интерфейса нам нужно во всех методах-сеттерах возвращать текущий объект:

class User {     protected $name;     protected $surname;     protected $password;      public function setName($name)     {         $this->name = $name;         return $this;     }      public function setSurname($surname)     {         $this->surname = $surname;         return $this;     }      public function setPassword($password)     {         if (!$password) {             throw new InvalidArgumentException("Password shouldn't be empty");         }         $this->password = $password;         return $this;     } } 

Такой подход позволит нам сделать цепочку вызовов, избегая множественного указания имени переменной:

$user = new User; $user->setName('John')->setSurname('Doe')->setPassword('root'); 

Как вы уже заметили, конфигурация объекта теперь занимает меньше места и читается значительно легче. Мы достигли поставленной цели! В этом месте у многих разработчиков должен возникнуть вопрос: «И что? Я это и так знаю…» Тогда попробуйте ответить на вопрос: «Чем плох текучий интерфейс в данном виде?» перед чтением следующего блока статьи.

Так чем же он плох?

Наверное, вы не нашли ответа и решили его прочитать? ) Ну тогда вперед! Спешу вас успокоить: на текущем уровне ООП с текучим интерфейсом все хорошо. Однако если подумать, то можно заметить, что его нельзя реализовать в виде отдельного класса и подключить к нужному объекту. Эта особенность выражается в том, что приходится монотонно проставлять return $this в конце каждого метода. Если же у нас пара десятков классов с парой десятков методов, которые мы желаем сделать «текучими», то приходится вручную заниматься этой неприятной операцией. Это и есть классическая сквозная функциональность.

Давайте наконец-то сделаем его с помощью отдельного класса

Так как у нас сквозная функциональность, то нужно подняться на уровень выше ООП и описать этот паттерн формально. Описание получается весьма простое — при вызове публичных методов в некотором классе необходимо возвращать в качестве результата метода сам объект. Чтобы не получить неожиданных эффектов — давайте сделаем уточнения: публичные методы должны быть сеттерами (начинаются на set) и классы будем брать только те, которые реализуют интерфейс-маркер FluentInterface.
Конечное описание «текучего» интерфейса в нашей реализации на PHP будет звучать так: при вызове публичных методов-сеттеров, начинающихся на set, и находящихся в классе, реализующем интерфейс FluentInterface — необходимо возвращать в качестве результата вызова метода сам объект, для которого осуществляется вызов. Вот как! Теперь осталось дело за малым — опишем это с помощью кода АОП и библиотеки Go! AOP:

Первым делом, опишем интерфейс-маркер «текучего» интерфейса:

/**  * Fluent interface marker  */ interface FluentInterface {  } 

А дальше сама логика «текучего» интерфейса в виде совета внутри аспекта:

use Go\Aop\Aspect; use Go\Aop\Intercept\MethodInvocation; use Go\Lang\Annotation\Around;  class FluentInterfaceAspect implements Aspect {     /**      * Fluent interface advice      *      * @Around("within(FluentInterface+) && execution(public **->set*(*))")      *      * @param MethodInvocation $invocation      * @return mixed|null|object      */     protected function aroundMethodExecution(MethodInvocation $invocation)     {         $result = $invocation->proceed();         return $result!==null ? $result : $invocation->getThis();     } } 

Сделаю небольшое пояснение — совет Around задает хук «вокруг» оригинального метода класса, полностью отвечая за то, будет ли он вызван и какой результат будет возвращен. Это будет со стороны выглядеть так, как будто мы взяли код метода и немного изменили его код, добавив туда наш совет. В самом коде совета мы сперва вызываем оригинальный метод сеттера и если он ничего не вернул нам, то возвращаем в качестве результата вызова оригинального метода сам объект $invocation->getThis(). Вот такая вот незатейливая реализация этого полезного шаблона программирования всего в пару строчек.

После всего этого, подключение «текучего» интерфейса в каждый конкретный класс приложения — простая и приятная работа:

class User implements FluentInterface {     //...     public function setName($name)     {         $this->name = $name;     }     } 

Все, что нам нужно, чтобы использовать теперь текучий интерфейс в конкретном классе — просто добавить интерфейс — implements FluentInterface. Никакого копирования return $this по сотням методов, только чистый исходный код, понятный маркер интерфейса и сама реализация «текучего» интерфейса в виде простого класса аспекта. Всю работу возьмет на себя АОП.

Данная статья носит ознакомительный характер и предназначена исключительно для размышления о тех возможностях, которые могут быть доступны с помощью АОП в PHP. Надеюсь, вам было интересно узнать о реализации этого шаблона программирования.

Ссылки:

  1. Go! Aspect-Oriented Framework for PHP
  2. Wikipedia — Fluent Interface
  3. Github project
Ваше мнение относительно реализации текучего интерфейса с помощью АОП

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/170019/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *