1 заметка с тегом

ADR

Обработчики запросов вместо контроллеров

Вашему внимание представляю перевод статьи Goodbye controllers, hello request handlers от Jens Segers, который опубликовал её в своём блоге. Если переводить буквально, то статья в русском переводе должна называться «Прощайте контроллеры, да здравствуют обработчики запросов», но я решил назвать её проще — «Обработчики запросов вместо контроллеров». Вся статья сводится к тому, что все действия контроллеров нужно выносить в отдельные исполняемые классы. Как, зачем и почему читайте ниже.

За последние годы в ландшафте PHP многое изменилось. Мы стали использовать больше паттернов программирования и таких принципов, как DRY и SOLID. Но почему мы всё ещё используем контроллеры?

Если вам доводилось работать над большими приложениями, возможно, вы заметили, что рано или поздно контроллеры становятся толстыми. Даже при внедрении репозиториев или сервисов в контроллеры, со временем количество зависимостей, методов и строк кода неизбежно растёт.

Позвольте представить вам обработчики запросов (request handlers). Концепция очень проста, но малоизвестна среди PHP-разработчиков. Обработчик запроса является контроллером, но ограничен одним действием. Эта концепция очень похожа на шаблон Action-Domain-Responder, предложенная Paul M. Jones, альтернативой шаблону MVC, которая фокусируется на более «чистых» запросах и ответах (request-response) для веб-приложений.

Модель Action-Domain-Responder (ADR), которая является объектно ориентированной надстройкой над MVC. Оперирует понятиями близкими к доменной области приложения.

Хороший способ создания обработчиков запросов — использование вызываемых классов. Вызываемые классы — это классы, которые используют магический метод __invoke() в PHP, превращая их в исполняемые, что позволяет из класса сделать функцию. Вот простой пример вызываемого класса:

<?php

class Greeting
{
    public function __invoke($name)
    {
        echo 'Hello ' . $name;
    }
}

$welcome = new Greeting();

// Hello John Doe
$welcome('John Doe');

На данный момент вы, скорее всего, спрашиваете себя: «Зачем мне это нужно?». Это имеет смысл при ожидании маршрутизатором callback-функций или конкретных зависимостей. Проще говоря, не нужно парсить строку и разбивать её на контроллер и метод. Вы можете сразу указать свою зависимость из http-слоя. Посмотрите наглядный пример, как обработчики запросов уже сейчас можно регистрировать в Laravel или Slim:

<?php

Route::get('/{name}', Greeting::class);

А теперь сравните это с тем, что обычно происходит в ваших проектах:

<?php

Route::get('/{name}', 'SomeController@greeting');

Помимо удобочитаемости, здесь гораздо больше преимуществ. Позвольте мне продолжить и сравнить ключевые особенности обработчиков запросов и контроллеров.

Принцип единственной ответственности

На мой взгляд, контроллеры со многими видами действий нарушают принцип единственной ответственности (SRP) из SOLID. Обработчики запросов, наоборот, решают эту проблему через разделение методов контроллеров на свои собственные независимые классы, что упрощает их обслуживание, рефакторинг и тестирование.

Ниже приведен пример двух обработчиков запросов, которые вынесены из двух методов, вшитых в UserController (редактирование и обновление данных пользователя):

<?php

class EditUserHandler
{
    public function __construct(
        UserRepository $repository,
        Twig $twig
    ) {
        /* ... */
    }

    public function __invoke(Request $request, Response $response)
    {
        /* ... */
    }
}

class UpdateUserHandler
{
    public function __construct(
        UserRepository $repository,
        UpdateUserValidator $validator,
        ImageManager $resizer,
        Filesystem $storage
    ) {
        /* ... */
    }

    public function __invoke(Request $request, Response $response)
    {
        /* ... */
    }
}

Это подводит нас к следующему преимуществу.

Тестируемость

Как давно вы писали модульные тесты для своих контроллеров? Вероятно, вы закончили созданием смешанных зависимостей, которые даже не были связаны с вашим тестом. Поскольку обработчики запросов разделяют различные действия контроллера на отдельные классы, вам нужно только вводить или связывать mock-объекты для конкретного действия.

Недавно Jeffrey Way в своём Твиттере дал совет по тестированию: сделайте свои классы тестов как можно более конкретными, а затем используйте каждый тест для описания важного правила/способности (rule/ability).

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

Рефакторинг

В современных IDE есть мощные варианты рефакторинга. Но если вы привязываете методы контроллеров к маршрутам в конфигурационных файлах (например, Laravel или Slim), то вы сталкнётесь с проблемой замены старых вызовов (строчных) на название объектов. Ничего не поделать. Вот так должно выглядеть:

<?php

Route::get('/{name}', Greeting::class);

И это намного проще, чем такой вариант:

<?php

Route::get('/{name}', 'SomeController@greeting');

Вывод

Обработчики запросов предлагают отличную альтернативу для контроллеров. Действия контроллера разбиваются на отдельные классы обработчика запросов, ответственные за одно действие. Это приводит к созданию кода, который проще в обслуживании, рефакторинге и тестировании.

Должны ли вы пойти и заменить все контроллеры обработчиками запросов? Возможно нет. Для небольших приложений может иметь смысл группировать действия вместе для простоты. Я только недавно открыл для себя обработчики запросов, когда начал работать в Teamleader, и я не чувствую необходимости возвращаться к контроллерам в ближайшее время.

Ссылки по теме
1) Porto (Software Architectural Pattern)
   https://github.com/Mahmoudz/Porto

2) Action-Domain-Responder
   https://github.com/pmjones/adr

3) Request-Driven Development
   http://martinbean.co.uk/blog/2017/01/05/request-driven-development/

4) Classes vs. Namespaces
   http://antonkril.github.io/classes-vs-namespaces
3 октября   ADR   mvc   php   SOLID   SRP   паттерны   перевод