При разработке программного обеспечения одной из важных составляющих является высокая читабельность исходного кода программы. Существуют специальные методики и рекомендации, которые позволяют добиться улучшения читабельности исходного кода. Одной из методик улучшения читабельности исходного кода является применение «текучих интерфейсов» (англ. 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. Надеюсь, вам было интересно узнать о реализации этого шаблона программирования.
Ссылки:
ссылка на оригинал статьи http://habrahabr.ru/post/170019/
Добавить комментарий