Шагаем вперед с MVC: CQRS

Забываете ли вы иногда, что моделирует ваша модель? Не выходят ли из под контроля ваши контроллеры? Современные MVC фреймворки, такие как Ruby on Rails и Laravel, дают возможность чрезвычайно легко получить полноценные веб-приложения, готовые к продакшену с невероятно высокой скоростью. С помощью находчивых контроллеров в CRUD стиле небольшая команда, иногда состоящая из одного человека, может запустить RESTful веб-приложение в комплекте с пользователями, сообщениями в блоге, комментариями и административными возможностями в течении нескольких часов. Когда это облегчает массу объёма работы, это прекрасно. Но все мы знаем, что проекты могут быстро превратиться в базу кода, которая станет ночными кошмарами по развёртыванию и поддержке без прочной поддержки. Распределение ответственности на команды и запросы (CQRS) является одним из паттернов, которые мы использовали в Grok, когда наши приложения, основанные на MVC, стали развиваться в более сложные части программного обеспечения.

CQRS является простым, но всё же мощным паттерном проектирования, который вы можете использовать для хранения моделей и контроллеров (и представления, если вам нравится злоупотреблять каждой частью акронима MVC) сухо (ориг. dry. Вероятно, имеется ввиду принцип разработки «не повторяйся». Прим. перев.). Основная идея заключается в том, что все действия, которые делаются в тех сотнях (тысячах?) строк кода в контроллерах и моделей, должны быть разделены на команды — действия, которые пишут данные, и запросы — действия, считывающие данные. (Если в вашем контроллере несколько сот строк кода, это уже повод задуматься об уровне ответственности контроллера, его разделения, а также убедиться, что бизнес-логика вынесена в службы, прим. перев.) Никогда не смешивайте оба эти действия, НИКОГДА. Это приведёт к нарушению одного и единственного правила CQRS. Команды всегда должны иметь пустой тип возврата, т. е. изменяя данные, ничего не возвращать. А запросы всегда должны возвращать некоторый тип данных без внесения каких-либо изменений в систему.

cqrs

Кажется, существуют некоторые холивары (внезапно) между сторонниками CQRS по поводу того, требует ли он использовать различные модели для записи и чтения данных или же нет.  Отдельные модели в этом слое вашего приложения могут помочь начать прокладывать путь в другие паттерны проектирования для дальнейшей универсальности вашей программы, такие как, Task-based UI, Event Sourcing и Domain Objects.

cqrs_different_read_write

Каждый слой программы увеличивает сложность разработки и содержания, хотя, использование тех же моделей для чтения/записи, чтобы управлять командами и запросами, может оказаться наиболее эффективным решением для отдельных проектов:

cqrs_same_read_write

Рассмотрим следующий код, который вы можете увидеть в 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, и вы существенно улучшите свой код.


Примечание

Это авторский перевод статьи «Moving on from MVC: CQRS».

Комментарии

  1. Сергей пишет:

    «Команды всегда должны иметь пустой тип возврата, т. е. изменяя данные, ничего не возвращать.»

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

    • Oleg пишет:

      Исключения?

    • bogomya пишет:

      Ошибки должны быть выброшены исключением еще на моменте валидации модели/команды. Идея как раз и в том, чтобы сделать клиентский код независимым от результата работы команды. Запросы к базе мы вообще можем делать в фоне, после отправки ответа пользователю.

    • betterweb пишет:

      200 — OK
      4xx — ошибки запроса, там и можно описать ошибку, хоть прямо в теле

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *