Забываете ли вы иногда, что моделирует ваша модель? Не выходят ли из под контроля ваши контроллеры? Современные MVC фреймворки, такие как Ruby on Rails и Laravel, дают возможность чрезвычайно легко получить полноценные веб-приложения, готовые к продакшену с невероятно высокой скоростью. С помощью находчивых контроллеров в CRUD стиле небольшая команда, иногда состоящая из одного человека, может запустить RESTful веб-приложение в комплекте с пользователями, сообщениями в блоге, комментариями и административными возможностями в течении нескольких часов. Когда это облегчает массу объёма работы, это прекрасно. Но все мы знаем, что проекты могут быстро превратиться в базу кода, которая станет ночными кошмарами по развёртыванию и поддержке без прочной поддержки. Распределение ответственности на команды и запросы (CQRS) является одним из паттернов, которые мы использовали в Grok, когда наши приложения, основанные на MVC, стали развиваться в более сложные части программного обеспечения.
CQRS является простым, но всё же мощным паттерном проектирования, который вы можете использовать для хранения моделей и контроллеров (и представления, если вам нравится злоупотреблять каждой частью акронима MVC) сухо (ориг. dry. Вероятно, имеется ввиду принцип разработки «не повторяйся». Прим. перев.). Основная идея заключается в том, что все действия, которые делаются в тех сотнях (тысячах?) строк кода в контроллерах и моделей, должны быть разделены на команды — действия, которые пишут данные, и запросы— действия, считывающие данные. (Если в вашем контроллере несколько сот строк кода, это уже повод задуматься об уровне ответственности контроллера, его разделения, а также убедиться, что бизнес-логика вынесена в службы, прим. перев.) Никогда не смешивайте оба эти действия, НИКОГДА. Это приведёт к нарушению одного и единственного правила CQRS. Команды всегда должны иметь пустой тип возврата, т. е. изменяя данные, ничего не возвращать. А запросы всегда должны возвращать некоторый тип данных без внесения каких-либо изменений в систему.
Кажется, существуют некоторые холивары (внезапно) между сторонниками CQRS по поводу того, требует ли он использовать различные модели для записи и чтения данных или же нет. Отдельные модели в этом слое вашего приложения могут помочь начать прокладывать путь в другие паттерны проектирования для дальнейшей универсальности вашей программы, такие как, Task-based UI, Event Sourcing и Domain Objects.
Каждый слой программы увеличивает сложность разработки и содержания, хотя, использование тех же моделей для чтения/записи, чтобы управлять командами и запросами, может оказаться наиболее эффективным решением для отдельных проектов:
Рассмотрим следующий код, который вы можете увидеть в Laravel контроллере:
<?php
class UserController {
public function index()
{
if ($user_preferences = Auth::user()->preferences) {
$users = User::where('region', $user_preferences->region)
->where('group', $user_preferences->group)
->where('role', $user_preferences->role)
->paginate(20);
} else {
$users = User::all()->paginate(20);
}
return View::make('users.index')->with('users', $users);
}
public function store()
{
$input = Input::all();
$user = User::where('email', $input['email'])->first();
// Check to see if email exists
if ($user) {
$message = 'Email already exists';
return Redirect::back()->with('message', $message);
}
// Validate input
$user_attributes = array_intersect($input, User::$rules);
$validator = Validator::make($user, $user_attributes);
if ($validator->fails()) {
return Redirect::back()->withErrors($validator);
}
// Store User
$user = new User;
$user->first_name = $input['first_name'];
$user->last_name = $input['last_name'];
$user->email = $input['email'];
$user->password = Hash::make($input['password']);
$user->region = $input['region'];
if (!$user->save()) {
$message = 'Unknown Error Occurred';
return Redirect::back()->with('message', $message);
}
return Redirect::action('Controller@index');
}
}
Здесь не выполняется много логики. Индексный метод запрашивает User модели для списка пользователей с пагинацией и опциональными поисковыми предпочтениями, если реляционная модель для поисковых предпочтений найдена. Метод хранилища выполняет несколько основных шагов проверки, после чего пытается создать нового пользователя с набором атрибутов в запросе POST.
Но контроллер имеет более 50 строк кода! Для вас это может показаться не большой проблемой, но здесь также ничего не делается для ограничения потенциального роста представленных методов в течении долгого времени. Что происходит, когда объект пользователя удваивается в размере?
Давайте теперь взглянем на тот же контроллер с использованием служб отдельных команд и запросов в виде внедрённых зависимостей:
<?php
use app\Users\UserCommandService;
use app\Users\UserQueryService;
class UserController {
public function construct(UserCommandService $user_command_service, UserQueryService $user_query_service)
{
$this->user_command_service = $user_command_service;
$this->user_query_service = $user_query_service;
}
public function index()
{
$users = $this->user_query_service->getAllUsersWithSearchPreferences();
return View::make('users.index')->with('users', $users);
}
public function store()
{
$this->user_command_service->createUserCommand();
return Redirect::action('Controller@index');
}
}
Просто переместив всю логику из контроллера в классы служб с разделением чтения и записи в нашем хранилище, код сократился более чем на половину и, что ещё лучше, это помогло определить единственное действие, принимаемое в каждом из действий нашего контроллера.
Если будете гуглить CQRS, несомненно, вы столкнётесь с большими концепциями проектирования ПО, такими как Domain Driven Design или Event Sourcing, и вскоре испытаете соблазн прыгнуть в червоточину информации, охватывающую их (или спрыгнуть с ближайшего выступа). Не делайте так! По крайней мере пока. Придерживайтесь понимания простого строительного блока типа CQRS, и вы существенно улучшите свой код.