Обзор книги "Growing Rails Applications in Practice"
В технических книгах я ценю в первую очередь ясность изложения и практическую ценность материала. Такое сочетание, к сожалению, встречается реже чем хотелось бы. С ясностью изложения в книгах в мире Ruby разработки всё более ли менее окей. Но вот уникальные и полезные практические знания найти можно не так часто. Один пример подобной полезной книжки – Confident Ruby, обзор на которую я писал полгода назад. Еще один пример – герой этого дня Growing Rails Applications in Practice, написанная немецкой консалтинговой фирмой книжка объемом всего в 88 страниц.
Авторы, имеющие большой опыт в разработке крупных приложений на рельсах, оформили свои методы написания приложений в произведение, на чтение которого уйдет максимум два часа. В чем же заключаются эти методы?
Голые рельсы
Авторы не предлагают никаких волшебных гемов и запутанных паттернов проектирования. Вместо этого они сконцентрировались на эффективном использовании тех инструментов, которые уже предоставляют Rails.
Стройные контроллеры
Да-да, идея о thin controllers, fat models не нова. Однако в книжке приведён конкретный паттерн написания стройных контроллеров, а так же несколько техник и фишек, упрощающих их расширение и предотвращающих их утолщение. Например, рекомендуется добавить private метод #{имя_модели}_scope
, и проводить поиск по записям при помощи этого метода:
private
def course_scope
Course.scoped
end
def build_course
@course ||= course_scope
@course.attributes = course_params
end
Теперь мы можем легко добавить ограничения доступа к различным записям, редактируя только один метод course_scope
, во всех остальных местах код менять не придётся:
def course_scope
current_user.admin? ? Course.scoped : current_user.courses
end
Да, не самая новая техника, но авторы собрали несколько вот таких вот очевидных и нужных вещей в одну идею о том, как надо писать контроллеры. Ещё интересно, что в книге не сильно жалуют before_action
, предпочитая вызывать методы напрямую. Поначалу мне казалось это глупостью, но немного поразмыслив я понял, что в этом и правда есть смысл: труднее запутаться в коде, легче редактировать отдельные методы.
Особой строкой идёт нелюбовь авторов книги к гемам вроде Interited Recourses. Я сам терпеть не могу такие библиотеки, которые якобы упрощают написание контроллеров, но на самом деле лишь добавляют лишний уровень сложности.
Сервисные объекты
Очень понравился подход авторов к сервисным объектам. Парни предлагают разбивать контроллеры не только по моделям, но и по различным сервисным объектам. Допустим, у вас есть TransactionsController
, внутри которого, в том числе, есть код для проведения переводов из одного счёта в другой:
def transfer
source_account = Account.find(params[:source_id])
amount = params[:amount]
if (source_account.summary - amount) > 0
transaction = Transaction.new(amount: amount, source_account_id: source_account.id, target_account_id: params[:target_id])
if transaction.save!
head :ok
else
render json: { erros: transaction.errors }
end
else
render json: { erros: “Source account doesn’t have enough money to make this transfer” }
end
end
И вот этот большой (относительно) метод валяется в TransactionsController
, увеличивая размер этого контроллера на порядок и делая его далеко не стройным и не красивым. Решением такой проблемы будет добавить TransfersController
который будет использовать сервисный объект Transfer
. Этот сервисный объект использует ActiveModel для валидаций.
class TransfersController < ApplicationController
def create
build_transfer
if @transfer.create
head :ok
else
render json: { errors: @transfer.errors }
end
end
private
def build_transfer
@transfer = Transfer.new(transfer_params)
end
def transfer_params
params.require(:sign_in).permit(:source_id, :target_id, :amount)
end
end
class Transfer
include ActiveModel::Model
include ActiveSupport::Callbacks
include ActiveModel::Validations::Callbacks
attribute :source_id, :integer
attribute :target_id, :integer
attribute :amount, :integer
validate :validate_source_account_balance
define_callbacks :create
after_create :create_transaction
def save
if valid?
run_callbacks :create do
true
end
else
false
end
end
def source_account
Account.find(source_id)
end
private
def create_transaction
Transaction.create(source_account_id: source_id, target_account_id: target_id, amount: amount)
end
def validate_source_account_balance
if source_account.balance < amount
errors.add(:amount, “Requested amount is not present in source account”)
end
end
end
Тут вы скажете, что кода в итоге получилось в 4 раза больше, чем в изначальном варианте. Но это только потому, что в модели Transfer
много boilerplate кода, который должен быть вынесен в класс, от которого будут наследоваться все подобные сервисные объекты. В книге предлагают использовать гем ActiveType, который является надстройкой над ActiveModel с несколькими дополнительным фичами сверху.
После изменений мы получаем небольшой новый контроллер и лёгкую для восприятия модель, которые легко расширить для различных видов переводов в будущем. Например, мы можем добавить оповещение по почте в after_save
коллбэке этой новой модели.
Признаю, написанный мной пример не идеален. Например, мне лично не нравится, что Transfer
в данном случае прячет в себе код для создания транзакции. Ещё я не добавил вывода сообщений об ошибках при сохранении транзакции. Тем не менее, техника, предлагаемая авторами Growing Rails Applications in Practice, заслуживает внимания. Сам я ещё не имел удовольствия активно её применить, но описанная методика мне определённо нравится своей простотой и элегантностью. Как минимум читаемость кода повышается в разы. В самой книге гораздо больше примеров гораздо больших размеров.
Недостатки книги
Во-первых, четверть книги посвящена организации CSS, и эта часть является пересказом системы BEM. Я сам лично больше люблю SMACSS. Но дело даже не в этом – в книге про рост рельсовых приложений я не ожидал такого количества текста про написание фронтенда. Возможно, сказывается тот факт, что я в целом уже года 3-4 как устал от CSS и мне про него утомительно слушать в целом.
Во-вторых, ребята советуют использовать Cucumber. Не буду долго распинаться, но скажу, что считаю Cucumber потрясающе идиотской штукой, которую никому не советую использовать.
В-третьих, сама техника написания контроллеров не так хороша, как её описывают авторы книги. Я попробовал (и продолжу эксперименты в этом направлении) следовать их рекомендациям в коде mkdev. Читаемость кода и правда улучшилась, вот, например, метод обновления курса:
def update
load_course
build_course
save_course or render :edit
end
Проблемы начались когда я попробовал использовать ту же технику для модели Task
, которая belongs_to :course
. Следуя советам из Growing Rails Applications у меня получился такой набор private-методов:
def load_course
@course = course_scope.find(params[:course_id])
end
def course_scope
Course.where(nil)
end
def load_task
@task = task_scope.find(params[:id])
end
def build_task
@task ||= task_scope.build
@task.attributes = task_params
end
def save_task
if @task.save
redirect_to admin_course_tasks_path(@course)
end
end
def task_scope
load_course
@course.tasks
end
def task_params
task_params = params.fetch(:task, {})
task_params.permit(:title, :text, :position)
end
Это уже 7 методов для простого CRUD, не считая ещё 7 стандартных (index, create, update и т.п.). Переменые @course
и @task
размазаны по этим методам. Создаётся ощущение, что стоит добавить ещё чуть-чуть кода и получится запутанная мешанина. Не уверен, что такой вариант мне нравится больше привычного.
Итого
Growing Rails Applications in Practice – очень клёвая книга, которая стоит своих денег. Я привёл лишь два микроскопических примера из множества техник, описанных в таком небольшим произведении. Абсолютно никакой воды, практически ничего лишнего и точно пригодится как новичкам, не знающим как организовать свой код, так и профи, постоянно ищущим способы улучшить свой код. Методы авторов далеко не идеальны и к их советам, естественно, нужно относиться критически, брать лучшее и откидывать очевидно стрёмное.
Уже прочитавшие, что думаете о книжке?