То, что контроллеры должны быть «худыми» знают все, но по мере наращивания функционала поддерживать чистоту контроллеров становится все сложнее и сложнее. Мы хотим предложить несколько рекомендаций как содержать свои контроллеры максимально чистыми без ущерба для качества кода.
Все контроллеры строим на базе inherited_resources, что позволяет избегать банального CRUD кода. Всего две строчки объявления контроллера, унаследованного от InheritedResources::Base, и он умеет выполнять все базовые операции (создание/отображение/обновление/удаление) с ресурсом! Все бы хорошо, но часто возникают проблемы:
Для решения первой проблемы можно перекрыть соответствующий метод контроллера (index) и внести необходимые изменения в выборку, но есть способ намного более элегантный, который описан в документации inherited_resources, но почему-то им мало кто пользуется.
Этот gem позволяет вклинивать в выборку ресурса/колекции ресурсов любые скоупы, описанные в модели.
Как это работает? Например, нам нужно вывести все поста блога, т.е. отфильтровать посты по блогу:
Теперь при запросе /posts?by_blog=1 коллекция ресурсов будет автоматически отфильтрована по блогу с id=1. Но выглядит это не очень красиво, поэтому пропишем в роутах следующее:
И тот же самый результат можно будет получить по URLу /blogs/1.
Если необходимо постоянно сортировать коллекции, то можно использовать параметр default — тогда скоуп будет применяться всегда с указанным дефолтным значением:
Аналогичным образом можно истреблять проблему N+1 запросов:
Подробнее ознакомиться с has_scope можно на странице проекта. Эффект от него как от HAMLа — стоит потратить немного времени на привыкание к особенностям, зато потом экономится много времени.
Эти гемы очень популярны и только ленивый их не использовал. Как они интегрируются с inherited_resources? С kaminari вообще нет никаких проблем — его можно применить в контроллере как обычный скоуп :page точно также как в предыдущих примерах. А вот с will_paginate — придется немного повозиться, т.к. метод paginate, который он предоставляет, не является скоупом модели. Но и здесь найдется вполне элегантное решение — необходимо перекрыть метод collection в контроллере следущим образом:
Поскольку данный трюк будет использоваться очень часто, то его можно вынести в модуль и подключать по мере необходимости в других контроллерах, но об этом подробнее в следующий раз.
При использовании этой связки можно полностью обойтись без отдельных контроллеров для админпанели. Они позволяют полностью вынести логику разделения доступа к ресурсам из контроллера, оставив там только декларации и заслуживают отдельной статьи.
Все дополнительные проверки, нормализацию параметров и прочие действия для стандартных операций можно вынести в before_filter:
Достаточно запросить коллекцию в нужном формате: /posts.rss и /posts.json, при этом в контроллере достаточно прописать:
Кроме того, для формирования RSS нужно прописать во шаблонах posts/index.rss.builder:
Обработать JSON коллекцию можно следующим кодом на jQuery:
Таким образом можно добиться минимального объема кода в контроллерах с сохранением гибкой функциональности, REST подхода и прозрачности кода. Все данные принципы реализованы в реальных проектах и неплохо показали себя на практике. Немного статистики: спустя 10 месяцев разработки, самый большой контроллер в проекте СмартСорсинг занимает 86 строк, самый маленький — 2 строки, но большинство котроллеров — не более 20 LOC.
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.