PHP
Website development
Programming
March 2013 23

Шаблон программирования «Текучий интерфейс» в 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
Ваше мнение относительно реализации текучего интерфейса с помощью АОП
22.1% Ничего себе! Не знал, что так возможно сделать, попробую на досуге! 177
34.2% Статья хорошая, интересно, что же еще можно сделать с помощью АОП? 274
31.7% Статья понравилась, возьму на заметку, но пока не время для АОП, остановлюсь на ООП. 254
11.8% Статья не понравилась, все слишком непрозрачно и непонятно, буду делать ручками по-старинке. 95
Voted 800 users. Passed 242 users.
+29
33.3k 293
Comments 67