REST-провайдеры на базе Rails: кошмар с вьюхами

в 17:14, , рубрики: decorator, draper, presenter, ruby, ruby on rails, Блог компании Round Lake

С развитием браузерных MVC-фреймворков, Rails очень часто стали упоминать в контексте удобного фреймворка для REST-провайдеров. Мы тоже используем Rails для этой цели и достаточно долго. Есть, однако, очень большая проблема: представления. Вьюшки, которые описывают структуру JSON для ответа.

На первый взгляд, все просто отлично. Ничего кроме .to_json или RABL, в некоторых сложных случаях, не требуется. Но затем ситуация выходи из под контроля. И идут бесконечные циклы перебора JSON-билдеров в поисках лучшей жизни.

Проблема

Давайте возьмем для примера банковский сервис. Он состоит из 30 моделей. Каждая модель представлена CRUD-реурсом (в каждом по 3-4 расширяющих метода). В каждой модели 10-12 полей и это обычно длинные строки. И, конечно, все они связаны. Вплоть до 4-5 уровней belongs_to.

При этом важно помнить, что в реальной жизни JSON ответа – это не просто прямой дамп структуры модели. В нем постоянно встречаются условия (какой атрибут должен попасть в ответ? Зависит от другого атрибута) и кастомные методы.

Проблема представлений заключается в том, что клиенту REST-сервиса нужен уникальный набор полей модели для каждой такой модели и _для каждого метода_ этого REST-ресурса. И не забудьте про вложенные сущности.

Представьте, что у вас 4-5 наборов полей для каждой модели. И это только начало. После этого модель включает в себя другая. И родитель хочет видеть лишь 3 маленьких поля, которые полностью описывают их отношение. А потом эту же модель включает в себя еще один родитель, которому нужно 2 других поля. Это уже около 10 разных наборов. И в каждом таком наборе могут быть еще свои дополнительные условия для того, что модель вкладывает в себя.

Боль

Решение с которого мы начали – RABL. По началу он выглядит достаточно эффективным, но на практике он совершенно не подходит для сложных представлений. В реальности RABL не так и далеко ушел от .to_json. Мы пробовали очень много разных билдеров и в конце концов остановились на геме Jbuilder, который позволяет писать крайне прямолинейный и простой код избегая синтаксического шума.

Но это не помогло. Что мы делаем в представлениях, чтобы не дубировать код? Используем паршалы, правильно. Очень скоро это привело на к 10-15 паршалам для каждой модели. Умножив это на 30 моделей, мы получим 450 файлов, лешащих в app/views. Эту кучу просто невозможно поддерживать.

Паттерн Presenter

Известным подходом к решению подобных проблем является паттерн Presenter. Так как наши вьюшки – просто Ruby-код, логичным первым шагом было превратить обернуть его в классы.

# example taken from http://quickleft.com/blog/presenters-as-a-solution-to-asjson-woes-in-rails-apis

class Api::V1::ResourcePresenter

  attr_reader :resource

  def initialize( resource )
    @resource = resource
  end

  def as_json( include_root = false )
    data_hash = {
      :attr1 => @resource.attr1,
      :attr2 => @resource.attr2
    }
    data_hash = { :resource => data_hash } if include_root
    data_hash
  end

end

Это позволило уменьшить количество файлов и сгруппировать подобные наборы полей в один метод с входящими параметрами.

Отлично. Мы пришли к соотношению 1 к 1 для декораторов, описывающих наборы полей моделей. Но теперь возникла другая проблема: этот код не похож на то, чего ожидают от Rails.

Лучшего результата позволяет добиться гем Draper. С его помощью мы можем превратить вышеописаный код в это:

# app/decorators/article_decorator.rb
class ArticleDecorator < ApplicationDecorator
  decorates :article

  def the_very_important_fields_set( include_root = false )
    data_hash = {
      :attr1 => att1,
      :attr2 => attr2
    }
    data_hash = { :resource => data_hash } if include_root
    data_hash
  end
end

Но проблемы не кончились, теперь на лицо явное нарушение DRY. При большом количестве полей мы застрянем на огромных хэшах, содержащих одинаковые строки в ключах :foo и значениях self.foo.

Благодаря тому, что Draper поддеривает общий Application-класс это очень легко решить маленьким методом-обвязкой. Однако наша цель – улучшить работу с Jbuilder'ом. И тут стоит заметить, что в Jbuilder уже есть метод, который решает эту проблему. Нам не обязательно работать с хэшами, мы можем собирать ответ из набора строк, используя Jbuilder прямо в наших декораторах.

На момент написания этих строк, Jbuilder не позволяет вставлять сырые JSON-строки при генерации. Однако есть другой подход, который может помочь достичь нужного результата. Существует отличный форк (pull-request уже был частично подтвержден автором и этот функционал очень скоро попадет в сам Jbuilder).

С помощью этого форка мы можем модифицировать наш код следующим образом:

# app/decorators/article_decorator.rb
class ArticleDecorator < ApplicationDecorator
  decorates :article

  def the_very_important_fields_set( include_root = false )
    data = Jbuilder.encode do |j|
      j.(self, :attr1, :attr2)
    end
    data = { :resource => data } if include_root
  end

  def another_set
    Jbuilder.encode do |j|
      j.(self, :attr1, :attr2, :attr3)
      j.cards card.basic_fields(:include_transactions)
    end
  end
end

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

В итоге мы имеем следующую структуру:

REST провайдеры на базе Rails: кошмар с вьюхами

Такая стратегия может выглядеть слегка избыточной. Но это очень хорошо работает для больших слоев данных. Эта стратегия позволяет REST-провайдерам быть точными (отдавать на каждый запрос ровно тот набор полей, который нужен конкретному методу), избежать дублирования и позволить сохранить простоту поддержки.

P.S. Про безопасность

Похоже, что эта стратегия позволяет также решить проблему раздачи разных полей для разных ролей. И это действительно так. Однако обычно этого делать не стоит, – это может привести к дублированию логики. Нам нужно не только авторизовать раздачу данных, но также и изменение.

К моменту, когда мы пришли к этой стратегии, у нас уже был отличный гем Heimdallr, который позволяет решить эту проблему куда лучше. Но это уже тема для совершенно отдельной статьи :).

Автор: inossidabile

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js