Паттерн Action-Domain-Responder

Является усовершенствованием паттерна Model-View-Controller и более удобен для создания веб-приложений.

Большинство фреймворков на данный момент использует MVC. Впервые он был представлен в 70-х годах и предназначался для разработки десктопных приложений. Несмотря на это, в течении долгого времени, паттерн используется и для веб-приложений.

В этой статье мы познакомимся с паттерном Action-Domain-Responder (ADR — Действие-Домен-Ответчик) и научимся его применять в Laravel.

Что такое Action?

Класс Action можно сравнить с контроллером одного действия.

Каждый Action должен обрабатывать только одно действие в приложении.

Представьте, что вы создаете страницу пользователя User. В обычном Laravel-приложении ваше определение маршрута выглядело примерно бы так:

Route::get('/users/{user}', [UsersController::class, 'show']);

В вашем контроллере UsersController есть несколько методов, каждый из которых обрабатывает свой маршрут.

Если бы мы преобразовали этот маршрут в Action, то сделали бы так:

Route::get('/users/{user}', ShowUserAction::class);

Где задаются классы Action?

Обычное приложение хранит классы Controller в файлах директории app/Http/Controllers.

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

В нашем случае я создаю папку app/Users. В ней будут храниться все компоненты, относящиеся к пользователям.

Тогда класс Action можно создать в файле app/Users/Actions/ShowUserAction.php

Что должен иметь Action?

Для того, чтобы использовать класс Action в маршруте, нам необходим метод __invoke

class ShowUserAction
{
  public function __invoke()
  {
    //
  }
}

Этот метод будет вызван при использовании конечной точки /users/{user}

Как Action генерирует Response?

Теперь, когда мы познакомились с классами Action, то можем посмотреть на возвращаемые объекты Response.

Буква R в ADR означает Responders (Ответчик). Как и следует из названия, эти классы отвечают за создание объекта Response.

Где хранятся классы Responder?

Как и для Action-классов мы можем создать для них отдельную папку app/Users/Responders.

Для маршрута /users/{user} создадим новый класс ShowUserResponder.

namespace App\Users\Responders;

class ShowUserResponder
{

}

Что должен иметь Responder?

Обычно я создают метод respond для генерации ответа.

namespace App\Users\Responders;

use Illuminate\Http\Response;

class ShowUserResponder
{
  public function respond()
  {
    
  }
}

Вы можете назвать его send, generate или даже __invoke.

Теперь мы можем получить экземпляр ShowUserResponder в конструкторе ShowUserAction.

namespace App\Users\Actions;

use App\Users\Responders\ShowUserResponder;

class ShowUserAction
{
  public function __construct(
    protected ShowUserResponder $responder
  ) {}

  public function __invoke()
  {
    return $this->responder->respond();
  }
}

На данный момент ShowUserResponder фактически не возвращает Response.

Мы показываем User и поэтому можем использовать привязку модели к маршруту в методе ShowUserAction::__invoke

namespace App\Users\Actions;

use App\Users\Responders\ShowUserResponder;

class ShowUserAction
{
  public function __construct(
    protected ShowUserResponder $responder
  ) {}

  public function __invoke(User $user)
  {
    return $this->responder->respond($user);
  }
}

Также передадим объект User в метод ShowUserResponder::respond, чтобы сгенерировать валидный Response.

namespace App\Users\Responders;

use Illuminate\Http\Response;

class ShowUserResponder
{
  public function respond(User $user)
  {
    return view('users.show', [
      'user' => $user,
    ]);
  }
}

Теперь, когда у нас есть объект User, мы можем использовать хелпер view для создания объекта View, который
автоматически сконвертируется в валидный объект Response.

Что ещё должен делать Responder?

Помимо создания валидного объекта Response (или Responsable), Ответчик несёт ответственность за изменение всего, что связано с ответом.

Это может быть добавление/изменение заголовков, изменение формата ответа (JSON вместо HTML на основе механизма «Согласование контента»).

MVC или ADR?

Вот список того, в чём, на мой взгляд, ADR лучше, чем MVC:

1. Разделение доменов

Паттерн ADR специально был разработан с учётом предметно-ориентированного проектирования (domain-driven design, DDD).

И он лучше подходит для разделения доменов, чем MVC.

Вы можете группировать по цели, а не по типу компонента. У вас больше не будет кучи папок в app/Http/Controllers, но будет много папок с Http/Controllers.

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

2. Разделение ответственности

Для MVC довольно типично — раздувать контроллеры большим количеством бизнес-логики (да я и сам так делаю).

А в ADR сразу понятно, что Action отвечают только за обработку бизнес-логики, а Responder — за генерацию ответов.

Если обнаружите, что используете response() или view() в Action, то вы используете паттерн ADR неправильно.

Вообще этот паттерн поощряет использование мидлваров и перехватчиков запросов (request interceptors).

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

Отсутствие таких проверок в Action позволит вам еще больше разделять ответственность.

Автор: Ryan Chandler
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.