При разработке программного обеспечения одной из важных составляющих является высокая читабельность исходного кода программы. Существуют специальные методики и рекомендации, которые позволяют добиться улучшения читабельности исходного кода. Одной из методик улучшения читабельности исходного кода является применение «текучих интерфейсов» (англ. 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 GoAopAspect;
use GoAopInterceptMethodInvocation;
use GoLangAnnotationAround;
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. Надеюсь, вам было интересно узнать о реализации этого шаблона программирования.
Ссылки:
Автор: NightTiger