Обзор книги "Growing Rails Applications in Practice"

Illustration of a person sitting on the floor with their back against the wall, looking down at a tablet in their hands, with a spotlight effect on the background. Illustration of a person sitting on the floor with their back against the wall, looking down at a tablet in their hands, with a spotlight effect on the background.

В технических книгах я ценю в первую очередь ясность изложения и практическую ценность материала. Такое сочетание, к сожалению, встречается реже чем хотелось бы. С ясностью изложения в книгах в мире 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 doesnt 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 – очень клёвая книга, которая стоит своих денег. Я привёл лишь два микроскопических примера из множества техник, описанных в таком небольшим произведении. Абсолютно никакой воды, практически ничего лишнего и точно пригодится как новичкам, не знающим как организовать свой код, так и профи, постоянно ищущим способы улучшить свой код. Методы авторов далеко не идеальны и к их советам, естественно, нужно относиться критически, брать лучшее и откидывать очевидно стрёмное.

Уже прочитавшие, что думаете о книжке?