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

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

Subscribe to our Newsletter

Let us send you the best of what we've discovered in DevOps, Cloud and Kubernetes, as well us occasional event announcements.

We are also preparing some ways to learn together: weekly challenges, free courses and more. Subscribe now to be the first to get those.