С развитием браузерных 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-провайдерам быть точными (отдавать на каждый запрос ровно тот набор полей, который нужен конкретному методу), избежать дублирования и позволить сохранить простоту поддержки.
P.S. Про безопасность
Похоже, что эта стратегия позволяет также решить проблему раздачи разных полей для разных ролей. И это действительно так. Однако обычно этого делать не стоит, – это может привести к дублированию логики. Нам нужно не только авторизовать раздачу данных, но также и изменение.
К моменту, когда мы пришли к этой стратегии, у нас уже был отличный гем Heimdallr, который позволяет решить эту проблему куда лучше. Но это уже тема для совершенно отдельной статьи :).
Автор: inossidabile