Как стать автором
Обновить

Архитектура контроллеров: простые советы на каждый день

Ruby on Rails
То, что контроллеры должны быть «худыми» знают все, но по мере наращивания функционала поддерживать чистоту контроллеров становится все сложнее и сложнее. Мы хотим предложить несколько рекомендаций как содержать свои контроллеры максимально чистыми без ущерба для качества кода.

1. Использовать inherited_resources


Все контроллеры строим на базе inherited_resources, что позволяет избегать банального CRUD кода. Всего две строчки объявления контроллера, унаследованного от InheritedResources::Base, и он умеет выполнять все базовые операции (создание/отображение/обновление/удаление) с ресурсом! Все бы хорошо, но часто возникают проблемы:
  • отфильтровать/отсортировать список
  • постраничная разбивка коллекций
  • разделение доступа между пользователями/группами пользователей

Для решения первой проблемы можно перекрыть соответствующий метод контроллера (index) и внести необходимые изменения в выборку, но есть способ намного более элегантный, который описан в документации inherited_resources, но почему-то им мало кто пользуется.

2. Использовать расширение has_scope


Этот gem позволяет вклинивать в выборку ресурса/колекции ресурсов любые скоупы, описанные в модели.
Как это работает? Например, нам нужно вывести все поста блога, т.е. отфильтровать посты по блогу:

class PostsController < InheritedResources::Base
has_scope :by_blog, :only => :index
end

class Post < ActiveRecord::Base
belongs_to :blog
scope :by_blog, lambda{|blog_id| where(:blog_id => blog_id)}
end

* This source code was highlighted with Source Code Highlighter.

Теперь при запросе /posts?by_blog=1 коллекция ресурсов будет автоматически отфильтрована по блогу с id=1. Но выглядит это не очень красиво, поэтому пропишем в роутах следующее:

get "blogs/:blog_id(/page/:page)(.:format)" => "posts#index", :constraints => { :page => /\d+/ }, :defaults => { :page => 1 }
resources :posts


* This source code was highlighted with Source Code Highlighter.

И тот же самый результат можно будет получить по URLу /blogs/1.

Если необходимо постоянно сортировать коллекции, то можно использовать параметр default — тогда скоуп будет применяться всегда с указанным дефолтным значением:

class PostsController < InheritedResources::Base
has_scope :ordered, :default => 'created_at DESC'
end

class Post < ActiveRecord::Base
scope :ordered, lambda{|field| order(field)} # это может быть не безопастно
end


* This source code was highlighted with Source Code Highlighter.

Аналогичным образом можно истреблять проблему N+1 запросов:

class PostsController < InheritedResources::Base
has_scope :eager_loading, :default => 'true', :only => :index
end

class Post < ActiveRecord::Base
scope :eager_loading, preload(:blog, :user, :tags)
scope :eager_loading2, includes(:blog, :user)
end


* This source code was highlighted with Source Code Highlighter.
Если нужно указать несколько скоупов сразу, проверить какие-то дополнительные условия или просто нет желания плодить скоупы в модели, то можно указать их прямо при задании параметров has_scope в блоке:

class PostsController > InheritedResources::Base
has_scope :blog do |controller, scope, value|
value != "all" ? scope.where(:blog_id => value) : scope
end
end


* This source code was highlighted with Source Code Highlighter.

Подробнее ознакомиться с has_scope можно на странице проекта. Эффект от него как от HAMLа — стоит потратить немного времени на привыкание к особенностям, зато потом экономится много времени.

3. Для постраничной разбивки использовать kaminari/will_paginate


Эти гемы очень популярны и только ленивый их не использовал. Как они интегрируются с inherited_resources? С kaminari вообще нет никаких проблем — его можно применить в контроллере как обычный скоуп :page точно также как в предыдущих примерах. А вот с will_paginate — придется немного повозиться, т.к. метод paginate, который он предоставляет, не является скоупом модели. Но и здесь найдется вполне элегантное решение — необходимо перекрыть метод collection в контроллере следущим образом:

class PostsController < InheritedResources::Base
protected
def collection
@posts ||= end_of_association_chain.paginate(:page => params[:page])
end
end


* This source code was highlighted with Source Code Highlighter.

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

4. Для аутентификации и авторизации использовать devise и cancan соответственно


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

5. По возможности избегайте перекрытия стандартных методов контролера


Все дополнительные проверки, нормализацию параметров и прочие действия для стандартных операций можно вынести в before_filter:

class PostsController < InheritedResources::Base
before_filter lambda{ resource.user = current_user }, :only => :create
before_filter lambda { resource.thumb = nil if params[:thumb_delete] }, :only => :update
end


* This source code was highlighted with Source Code Highlighter.


6. Для формирования RSS лент, подгрузки AJAX коллекций и т.п. не нужно плодить отдельные методы


Достаточно запросить коллекцию в нужном формате: /posts.rss и /posts.json, при этом в контроллере достаточно прописать:
class PostsController < InheritedResources::Base
respond_to :html
respond_to :rss, :json, :only => :index
end


* This source code was highlighted with Source Code Highlighter.

Кроме того, для формирования RSS нужно прописать во шаблонах posts/index.rss.builder:
xml.instruct! :xml, :version => "1.0"
xml.rss :version => "2.0" do
xml.channel do
xml.title "Заголовок"
xml.description "Описание"
xml.link collection_url(:format => :rss)

for resource in collection
xml.item do
xml.title resource.title
xml.description "#{resource.annotation}\n#{link_to 'Читать дальше...', resource_url(resource)}"
xml.pubDate resource.published_at.to_s(:rfc822)
xml.link resource_url(resource)
xml.guid resource_url(resource)
end
end
end
end


* This source code was highlighted with Source Code Highlighter.


Обработать JSON коллекцию можно следующим кодом на jQuery:
$('#blog_id').live('change', function() {
$.ajax({
url: '/posts.json',
dataType: 'json',
data: { blog_id: $(this).val() },
success: function(json) {
var options = '';
for (var i = 0; i < json.length; i++) {
options += '<option value="' + json[i].id + '">' + json[i].title + '</option>';
}
$('#destination').html(options);
}
});
});


* This source code was highlighted with Source Code Highlighter.

Таким образом можно добиться минимального объема кода в контроллерах с сохранением гибкой функциональности, REST подхода и прозрачности кода. Все данные принципы реализованы в реальных проектах и неплохо показали себя на практике. Немного статистики: спустя 10 месяцев разработки, самый большой контроллер в проекте СмартСорсинг занимает 86 строк, самый маленький — 2 строки, но большинство котроллеров — не более 20 LOC.
Теги:rubyruby on railskissdry
Хабы: Ruby on Rails
Всего голосов 51: ↑47 и ↓4 +43
Просмотры3.4K

Похожие публикации

Senior back-end разработчик (Ruby on Rails)
от 200 000 до 320 000 ₽HoodiesМожно удаленно
Backend Ruby on Rails (Middle / Senior)
от 180 000 до 250 000 ₽Lifetime+МоскваМожно удаленно
Программист Ruby on Rails (mid, mid+)
от 120 000 до 160 000 ₽TIQUMМожно удаленно
Ruby on Rails разработчик (Fintесh inhouse)
от 100 000 до 300 000 ₽DevmasterzСанкт-ПетербургМожно удаленно
Team Lead Ruby on Rails
от 300 000 ₽ВГТМосква

Лучшие публикации за сутки