Создание form objects с помощью ActiveModel и Virtus

Вы возможно слышали о толстых моделях, часто относящихся к ActiveRecord шаблону и превращающих модели в огромные классы, отвечающие за все подряд: от аутентификации пользователей до принятия аттрибутов для других моделей(nested attributes). Это нарушает одно из моих любимых правил SOLID — single responsibility principle(принцип единственной обязанности).

Один из паттернов, который вы можете использовать, чтобы сохранить свою SOLID карму — это form objects. Мы используем form objects в большей степени для того, чтобы управлять нашими формами и инкапсулировать данные, отправляемые пользователем.

Часто мы стоим перед соблазном использования готовой ActiveRecord модели, когда строим формы в приложении потому, что это все отлично работает с форм-хелперами(Form Helpers). Я собираюсь показать вам, как сохранить эти возможности, используя при этом формы с объектами.

Зачем использовать form objects?

Когда мы думаем над рефакторингом нашего приложения, всегда нужно хранить в уме принцип единственной обязанности(single responsibility principle).

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

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

И тут на помощь приходят form objects. Form object ответственнен за представление формы в вашем приложении. Каждое поле формы является аттрибутом в классе и может быть проверено через валидацию, которая отдаст нам чистые данные и они пойдут дальше по цепочке. Это может быть вашей моделью, определяющую таблицу в БД или, например, поисковой формой.

Использование Virtus и ActiveModel для создания объектов форм

Построение form object-а на самом деле просто. Rails 4+ предоставляет интерфейсActiveModel::Model и такой объект будет выглядеть, как полноценная модель ActiveRecord , что в свою очередь позволит использовать хелпер form_forи доступ ко всем возможным валидациям, включая функцию .valid?.

Virtus предоставляет аккуратный DSL для определения аттрибутов объекта с полезными опциями, как например: определение типа данных, значения по умолчанию и т.д. Это то, чем занимается ActiveRecord “за сценой”. Virtus не является чем-то необходимым для form object-ов, но позволяет сократить большой объем шаблонного кода.

В качестве основы для form object-a, добавим ActiveModel::Model и Virtus. Хотя такие объекты технически все же являются моделями, я предпочитаю создавать новую директорию под названием forms в директории app , чтобы хранить form objects отдельно от остальных ActiveRecord моделей.

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

Form object может выглядеть примерно так:

# app/forms/user_expense_form.rb
class UserExpenseForm
include ActiveModel::Model
include Virtus
  # Attributes (DSL provided by Virtus)
attribute :email, String
attribute :amount, Integer
attribute :paid, Boolean, default: false)
  # Access the expense record after it's saved
attr_reader :expense
  # Validations
validates :email, presence: true
validates :amount, numericality: { only_integer: true, greater_than: 0 }
  def save
if valid?
persist!
true
else
false
end
end
  private
  def persist!
user = User.create!(email: email)
@expense = user.expenses.create!(amount: amount, paid: paid)
end
end

Заметьте, мы создали две записи в две разные таблицы из одной формы. Ваши form object-ы не нуждаются в большей усложненности. То же самое верно и для контроллера:

# app/controller/expenses_controller.rb
class ExpensesController < ApplicationController
def new
@user_expense_form = UserExpenseForm.new
end
  def create
@user_expense_form = UserExpenseForm.new(user_expense_form_params)
if @user_expense_form.save
redirect_to dashboard_url, notice: "Expense ID #{@user_expense_form.expense.id} has been created"
else
render :new
end
end
  private
  # Using strong parameters
def user_expense_form_params
params.require(:user_expense_form).permit!(:email, :amount, :paid)
end
end

Это выглядит похоже на то, как мы обычно работаем с ActiveRecord моделями. И наконец, для полноты картины добавим нашу форму на страницу:

<%= form_for @user_expense_form, url: expenses_path do |f| %>
<%= f.label :email %>
<%= f.email_field :email %>
  <%= f.label :amount %>
<%= f.number_field :amount %>
  <%= f.label :paid %>
<%= f.check_box :paid %>
  <%= f.submit %>
<% end %>

Что это значит для ActiveRecord моделей?

Теперь ваша модель напрямую не поддерживает связь с формой и вы можете удалить все валидации из модели.

Нужна поддержка i18n? Она уже тут!

en:
activemodel:
attributes:
user_expense_form:
paid: "Has this been paid?"
errors:
models:
user_expense_form:
attributes:
amount:
greater_than: "must be greater than $%{count}"

Вывод

С form object-ами мы стараемся убрать рельсовую магию. На самом деле, мы работаем с гораздо большими возможностями и гибкостью, создавая наши формы и можем передавать любые данные из формы в любые таблицы, тем самым избегая вложенных аттрибутов и пр. Это также позволяет нам использовать формы, которые не имеют своей таблице в базе, как например, формы поиска и т.д.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.