Большинство фреймворков для веб-приложений используют паттерн Model-View-Controller (MVC). Этот паттерн был впервые представлен в 70-х годах прошлого века и выдержал испытание временем.
Исходя из даты введения, очевидно, что он никогда не был разработан или спроектирован для веб-разработки. Веб стал реальностью только в конце 80-х — начале 90-х годов.
Несмотря на это, он является де-факто способом создания серверных веб-приложений на протяжении многих лет и, без сомнения, будет им еще долгие годы.
За пределами мира MVC существует множество других паттернов проектирования, но они часто не используются или редко упоминаются во вселенной Laravel.
Я хотел бы показать вам, как использовать паттерн Action-Domain-Responder (ADR) в приложении Laravel и чем он отличается от традиционного паттерна MVC.
Что такое Action?
Когда речь идет об ADR, класс Action
можно сравнить с контроллером одного действия.
Каждый Action
должен обрабатывать только одно действие в вашем приложении.
Представьте, что вы создаете страницу «show» для User
. В типичном приложении Laravel определение маршрута может выглядеть следующим образом:
Route::get('/users/{user}', [UsersController::class, 'show']);
У вас есть контроллер UsersController
, который имеет несколько методов, каждый из которых обрабатывает различные маршруты.
Вы также можете иметь отдельный контроллер ShowUserController
(или аналогичный).
Если бы мы преобразовали этот маршрут в Action
, мы бы сделали примерно следующее:
Route::get('/users/{user}', ShowUserAction::class);
Где размещать и определять Action — классы.
Обычное приложение Laravel будет хранить классы контроллеров в app/Http/Controllers
.
Поскольку мы используем ADR
, мы можем сгруппировать нашу бизнес-логику более плотно и использовать более доменно-ориентированную (D в ADR
) структуру папок.
В данном случае я создам папку app/Users
. В этой папке Users
будут храниться все компоненты, связанные с компонентами моего приложения, ориентированными на пользователя.
Затем Action
класс создаем по такому пути app/Users/Actions/ShowUserAction.php
.
Что должно быть в Action?
Чтобы использовать класс Action
для пути(route), необходимо определить метод __invoke
для этого класса.
namespace App\Users\Actions; class ShowUserAction { public function __invoke() { // } }
Когда вы дерните этот endpoint /users/{user}
, будет вызван этот метод.
Как Action генерирует Response?
Теперь, когда мы знаем, что такое классы Action
, мы можем посмотреть на возвращение объекта Response
.
Давайте создадим папку 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
.
Теперь, когда мы создали наш класс Responder, мы можем получить экземпляр 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
, поэтому мы можем использовать привязку route-model для получения экземпляра 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
, мы можем использовать Laravel’s view helper для создания объекта View
, который Laravel автоматически преобразует в правильный объект Response
.
Что еще должен делать Responder?
Помимо генерации корректного Response
(или объекта Responsable
), Responder
должен отвечать за модификацию всего, что связано с ответом.
Это может быть добавление/изменение заголовков или изменение формата ответа (например, использование JSON
вместо HTML
через content-negotiation).
Итак, MVC или ADR?
Вот список вещей, которые, на мой взгляд, ADR делает лучше, чем MVC:
1. Разделение доменов (Domain separation)
Шаблон ADR был специально разработан для проектирования, ориентированного на домен. Буква D буквально означает домен.
Я думаю, что этот паттерн лучше подходит для разделения доменов, чем MVC.
Вы можете выбрать группировку на основе цели вместо типа компонента. У вас больше не будет 6 или 7 папок внутри app/Http/Controllers
, вместо этого у вас будет 6 или 7 папок с папкой Http/Controllers
.
Однако в зависимости от размера и размеров вашего приложения этот уровень абстракции может оказаться излишним. В итоге вы будете тратить больше времени на выяснение того, где что должно находиться, вместо того, что это что-либо должно делать.
2. Разделение проблем (Separation of concerns)
Одна вещь, которая часто встречается в MVC — раздувание контроллеров большим количеством бизнес-логики (иногда грешу этим и я).
В ADR ясно, что ваши классы Action
отвечают только за обработку бизнес-логики, а классы Responder
должны обрабатывать генерацию ответов.
Если вы используете response()
или view()
внутри Action
, вы применяете ADR неправильно.
Я думаю, что этот паттерн также поощряет использование промежуточного программного обеспечения и фактических перехватчиков запросов.
Вы можете избежать проверок авторизации внутри вашего Action
, используя middleware
Не допуская таких проверок в Action
, вы сможете еще больше разделить логику и ответы.