Бизнес логика в Rails приложениях

Для многих не секрет, что Ruby on Rails - это фрэймворк, основанный на паттерне Model-View-Controller (MVC), где

  • M (Model) - представляет собой данные, их зависимости и бизнес логику
  • V (View) - отвечает за предоставление информации
  • C (Controller) - получает на вход данные и преобразует их для дальнейшего использования вModel илиView.

И не смотря на то, чтоRails позволяет решать огромное количество задач, данный паттерн становится нежизнеспособным в больших проектах, особенно в проектах с быстро меняющейся бизнес логикой. Решением данной проблемы может стать внедрение еще одного слоя, который вынесет бизнес логику из моделей.

Бизнес логика

В нашей команде для выделения слоя бизнес логики мы начали использовать гем active_interaction. По сути он стал для нас обработчиком форм. Гем позволяет объявлять поля с необходимым типом данных, с возможностью задавать значения по-умолчанию, валидировать эти поля, используя знакомый всем механизм валидации изActive Model. Обработка данных формы происходит в методеexecute, который в лучших традицияхUnix, должен выполнять одну единственную задачу и делать это хорошо.

Do One Thing And Do It Well

Поработав какое-то время с этим гемом решили поделиться своим опытом с другими разработчиками.

Плюсы

Начнем с общего перечисления положительных сторон:

  • код стал более красивым и читаемым
  • модели и контроллеры стали "тоньше" за счет удаления из них бизнес логики
  • работа с View не изменилась, так как интеракторы хорошо мимикрируют под модели и рельсовые хелперы с ними прекрасно работают
  • вынос большинства валидаций, в том числе собственных, позволило более гибко менять бизнес логику
  • сама бизнес логика стала легче тестироваться, так как теперь необходимо тестировать один интерактор, а не подстраивать условия для модели для выполнения данной логики

Отдельно я бы хотел остановиться на валидации. В реальном приложении часто случается так что разные пользователи (админ или гражданин) имеют разное поведение при реализации CRUD для одной и той же сущности. Например администратор может устанавливать параметры учетных записей пользователя "на прямую", а гражданину может потребоваться специальный бизнес процесс для наполнения своего профиля. В этом случае очень неудобно держать валидации в модели - они обрастают условными операторами и сильно усложняют понимание. В случае с интеракторами такого рода логика будет реализована в разных классах: для администраторов и для граждан, что очень сильно облегчает тестирование.

Минусы

Не обошлось и без минусов:

  • количество файлов резко увеличилось, так как для каждого действияCRUD нужно писать свой интерактор. И возможно еще под каждую роль пользователя тоже.
  • разделение объявление полей и валидации (сначала идет проверка на наличие данных необходимого типа, потомActive Model валидации) приводит к поэтапным валидациям, что не очень нравится бизнесу

Однако эти минусы неприятны только дляMVP разработки, когда надо очень быстро выкатить рабочее решение. А вот уже начиная со среднесрочной перспективы много файлов уже можно считать плюсом, поскольку объем логики не меняется и в противном случае будет сконцентрирован в одном огромном файле.

Как это выглядит

Мы группируем интеракторы по предметной области и часто выделяем в ней базовые сущности:

Базовый класс интерактора

Это не обязательный подход, но часто это позволяет реализоватьDRY и в результате получать весьма простые сущности:

Интерактор для создания модели

Так выглядит сильно "похудевшая" модель, которая теперь отвечает только за целостность структуры данных:

"Похудевшая" модель

Завершает цепочку примеров контроллер:

Контроллер с использованием интеракторов

Заключение

Вынос бизнес логики из моделей оказалось верным решением, а гем active_interaction очень в этом помог.

Построенный на основе ActiveModel, ActiveInteraction превосходно подходит для выноса бизнес-логики из моделей, поскольку:

  1. Обеспечивает бесшовное использование привычных инструментов ActiveModel (::Dirty, ::Validations, ::Callbacks и пр), а также совместимость с другими компонентами и инструментами Rails и пр.
  2. Повышает гибкость и эффективность использования этих инструментов за счёт обеспечиваемой ActiveInteraction типизации интерфейсов, возможности каскадного использования сервисных объектов (compose) и пр.