Недавно мы опубликовали гем RailsStuff. Это коллекция небольших модулей и утилит для выполнения самых разных частых задач: от организации контроллеров и генерации уникальных случайных значений до парсера параметров и хэлперов переводов. В этом посте я расскажу про некоторые из них:
- ResourcesController — облегчённая и современная версия InheritedResources;
- Трекер типов;
- Генератор уникальных случайных значений;
- Хэлперы переводов и основных ссылок.
Установка
Большинство модулей слишком маленькие, чтобы быть отдельным гемом, и для удобства было решено объединить их в один гем. При этом используется ленивая загрузка: если вы не используете какой-то модуль, он и загружен не будет. Часть модулей используются напрямую через include/extend, другие добавляют методы класса для активации и настройки. Для вторых в геме есть Rails::Engine, который автоматически загрузит их в базовые классы моделей и контроллеров (ActiveRecord::Base и ActionController::Base). Выбрать отдельные модули или отключить автоматическую загрузку полностью можно, добавив initializer:
# Не использовать автоматическую загрузку:
RailsStuff.load_modules = []
# Загрузить только определенные модули:
RailsStuff.load_modules = %i(sort_scope statusable)
Гем не имеет зависимостей, но для некоторых модулей (или даже отдельных методов) надо будет установить что-нибудь ещё. Для таких модулей зависимости указаны в документации.
ResourcesController
Это один из основных модулей в RailsStuff. Фактически, он является упрощенной и обновлённой версией InheritedResources.
InheritedResources показал мне отличный подход к написанию контроллеров. Он реализовал многие функции, которые я искал или делал сам, например:
- Доступ к ресурсам в разных контроллерах по одному имени. Хорошо, когда у тебя переменная
@user
, но уже хуже, если@payment_transaction
. Ещё хуже, если@recurring_payment_transaction
. Кто-то в таком случае начинает сокращать имена, но от этого обычно становится ещё хуже. - Отсутствие повторяющихся
before_action :set_user
/before_action :find_manager
. С длинными спискамиonly
илиexcept
, которые нужно не забывать поддерживать в актуальном состоянии. - И, конечно, меньше дублирования кода экшенов и файндеров. Да, рельсы генерируют его за вас, но он всё равно остаётся кодом в вашем репозитории, и он повторяется. При редактировании приходится читать весь код: сразу сложно сказать, что перед тобой — оригинальный код из генератора, или он уже был редактирован.
InheritedResources позволяет избежать этих проблем и сосредоточиться на бизнес-логике приложения. Ho библиотека слишком перегружена, и достаточно часто приходится искать подходящие комбинации настроек или лезть в исходные коды за названием метода, который надо переопределить, или смотреть, как именно что-то реализовано. К тому же библиотека находится в статусе deprecated уже около года. Поэтому новые проекты было решено начинать без использования InheritedResources.
Так, после использования своих модулей в нескольких новых приложениях, мы выделили их в ResourcesController, который является основной частью RailsStuff. Это облегчённая версия InheritedResources, предоставляющая базовые возможности, и в ней отсутствуют некоторые сложные функции. Но это позволяет сделать код проще и яснее как в библиотеке, так и в приложениях. Все методы в библиотеке максимально просто выполняют отдельные функции. Основной принцип — не предоставить множество настроек на все возможные случаи, а обеспечить простым, расширяемым базовым функционалом. Если требуется особенное поведение, просто напишите метод так, как вы делали бы это без ResourcesController.
Вот основные отличия:
- Одинаковые названия переменных для всех контроллеров:
@_resource
и@_collection
. Нет необходимости в громоздких конструкциях изset_collection_ivar
иget_resource_ivar
. - Отсутствуют
belongs_to
и цепочки отношений. Но естьresource_helper
и быстрый способ установитьsource_relation
:
resources_controller source_relation: -> { manager.projects } # Это добавит метод #manager, который находит менеджера по params[:manager_id]: resource_helper :manager
- Улучшенная интеграция со StrongParameters (хэлпер вместо странного
def permited_params
):
permit_attrs :name, project_attributes: [:id, :_destroy, :name]
respond_with
используется только в create, update, destroy. В целом, это не должно быть проблемой: переопределение.to_json
— не очень хорошая идея для форматирования ответов для API Но если это действительно необходимо, то можно допатчить модуль и добавить нужные экшены.- Хэлперы для STI-моделей: белый список типов, доступных для создания, автоматическое определение класса по названию из параметров и отдельные разрешенные параметры для каждого типа.
module Site
class UsersController < SiteController
resources_controller
permit_attrs :name, :email
end
class ProjectsController < SiteController
resources_controller sti: true
resource_helper :user
permit_attrs :name
permit_attrs_for Project::External, :company
permit_attrs_for Project::Internal, :department
def create
super(action: :index)
end
protected
def after_save_url
url_for action: :index, user_id: resource.user_id
end
def source_relation
params.key?(:user_id) ? user.projects : self.class.resource_class
end
end
end
Трекер типов
Часто при использовании STI-моделей в интерфейсе надо вывести список всех возможных типов. В таких ситуациях ленивая загрузка мешает, и нужно явно вызвать
require_dependency/eager_load
для всех наследующих моделей. Да и DescendantsTracker для получения списка всех унаследованных классов в этом случае — не самое лучшее решение. Для таких задач мы используем TypesTracker. Он содержит хэлпер для загрузки всех типов для модели и хранит отдельный массив со всеми унаследованными классами: список готов и доступен в любой момент. При этом выборочно можно убрать некоторые классы из этого списка.class Project
extend RailsStuff::TypesTracker
# ...
eager_load_types! # загрузит все .rb в app/models/project
# или указать директорию явно:
eager_load_types! 'lib/path/to/projects'
end
class Project::Big < Project
unregister_type # Убираем этот класс из списка
end
class Project::Internal < Project::Big; end
class Project::External < Project::Big; end
class Project::Small < Project; end
Project.types_list # [Internal, External, Small]
Если массив заменить кастомным хранилищем, то можно задавать флаги для каждого класса и фильтровать/сортировать по ним при выводе:
class Project
extend RailsStuff::TypesTracker
# MyTaggedArray должен предоставлять #add(klass, *args).
# В примере *args - фактически *tags.
self.types_list_class = MyTaggedArray
end
class Project::Internal < Project::Big
# Вызывает types_list.add Project::Internal, :tag_1
register_type :tag_1
end
class Project::External < Project::Big
register_type :tag_2
end
Project.types_list.with(:tag_1)
Генератор случайных уникальных значений
Иногда требуется для создаваемой записи генерировать уникальное значение. Теоретически, UUID предоставляет такую возможность, но иногда значение надо генерировать в соответствии с шаблоном, и возможны коллизии. RandomUniqAttr использует ограничения БД и автоматически перегенерирует значение, если запись не удалась. Это позволяет использовать такой генератор даже тогда, когда коллизии довольно вероятны. Вот как он работает:
- Запись сохраняется как обычно.
- Если поле уже имеет значение, то ничего не происходит.
- Генерирует значение и пробует сохранить запись.
- Если произошла ошибка
RecordNotUnique
, повторяет предыдущий шаг.
У такого подхода есть условие: поле должно быть объявлено nullable. Несмотря на это, поле не будет иметь NULL после завершения транзакции.
# По-умолчанию используется SecureRandom.hex(32)
random_uniq_attr :token
# С кастомным генератором:
random_uniq_attr(:code) { |instance| my_random(instance) }
Хэлперы
Для мультиязычных интерфейсов в RailsStuff есть хэлперы для переводов названий действий и подтверждений. Эти методы кэшируют переводы на время запроса, и для больших списков и таблиц поиск перевода будет выполнен только один раз. Перед использованием нужно добавить в файлы переводов секции
helpers.actions
и/или helpers.confirmations
:ru:
helpers:
actions:
edit: Редактировать
delete: Удалить
confirm: Точно?
confirmations:
delete: Не пожалеете?
Теперь во всех шаблонах, можно начать переводить действия одинаково:
# в хэлпер добавить модуль:
# include RailsStuff::Helpers::Translation
= translate_action(:edit) or translate_action(:delete)
- collection.each do |resource|
tr
td= resource.name
td= link_to 'x', url_for(resource),
method: :delete, data: {confirm: translate_confirmation(:delete)}
= translate_confirmation(:purge_all) # Фолбэк: 'Точно?'
Для того, чтобы в приложении все ссылки на основные действия были оформлены одинаково, в RailsStuff есть хэлперы для них:
# Настройки в хэлпере.
# Подробности можно посмотреть в модуле rails_stuff/helpers/links
include RailsStuff::Helpers::Links
ICONS = {
destroy: '<span class="glyphicon glyphicon-trash"></span>'.html_safe,
edit: 'Редактировать',
new: -> { translate_action(:new) },
}
def basic_link_icons
ICONS
end
# Использование:
link_to_edit([:edit, :scope, resource]) or link_to_edit(edit_path)
link_to_edit('url', class: 'text-info')
# Если url не указан, используется url_for(action: edit).
# Так на странице /users/1
link_to_edit # будет вести на '/users/1/edit'
# По такому же принципу работают:
link_to_destroy or link_to_new
По умолчанию в качестве текста для ссылок используются переводы экшенов. Для пользователей Bootstrap или Glyphicon есть хэлпер RailsStuff::Helpers::Bootstrap, который использует иконки для этих ссылок.
Что ещё можно найти?
- «nullify blanks»;
- модуль для работы со статусами/перечислениями (как AR::Enum, только лучше);
- обработку параметров для сортировки;
- хэлпер
response.json_body
для тестирования API; - даже хэлпер для дебага
Net::HTTP
; - и другое.
Попробовать гем можно, добавив
gem 'rails_stuff', '~> 0.4.0'
в Gemfile. Больше подробностей, документация на английском и исходные коды — в репозитории на гитхабе.
Комментарии (10)
phoenixweiss
08.10.2015 01:11+2Начинание неплохое, но по-факту сборная солянка. У нас нечто подобное по гистам на гитхабе раскидано. Нет, безусловно есть неплохие удобные вещи, но я соглашусь с первым комментатором что это разнобой. В качестве статьи на хабре — интересно посмотреть как что использует ваша команда, но в реальной жизни и проектах использовать все же мало кто будет.
P.S. вы бы идею ResourceController развили в отдельный гем. Я думаю это было бы значительно полезнее для сообщества, тем более в качестве альтернативы полумертвого InheritedResources. А остальное не нужно, имхо.printercu
08.10.2015 10:20Попробую на статью набрать чего-нибудь интересного. Но сейчас всё кажется очень банальным.
Если ResourcesController будет быстро развиваться или окажется, что его лучше вынести отдельно — так и сделаем. То же самое и про все остальные части. Пока что, по-моему, это было бы не рационально — несколько гемов поддерживать требует больше времени, чем один. Гисты сложнее поддерживать в актуальном состоянии, и тесты к ним писать.
Скажите, что вас останавливало бы от использования одного только ResourcesController, установив весь набор? Все сделано на autoload, остальные файлы даже прочитаны не будут.phoenixweiss
08.10.2015 11:54+1Узкопрофильные гемы как правило стабильнее, они лучше развиваются и у них значительно меньше мусора. Текущий подход приведет к тому что придется следить по change-логу о тех изменениях которые не нужны, а это информационный шум, да и понимание концепции развития тоже немаловажно — пока это некоторый toolbox отдельно взятой команды, в нем завтра может появиться еще mp3-плеер и генератор QR-кодов, а также хэлперы со ссылками на амиго или еще что, и даже если это все будет на автолоаде, тащить гору мусора тоже не хочется. По предпочтениям, я даже стараюсь ставить гемы с --no-ri --no-rdoc дабы не тащить лишнего.
printercu
08.10.2015 13:01Про ченджлог согласен. Но зачем так про амиго сразу :) Выше я писал:
При этом в RailsStuff собраны, конечно, не все модули, которые мы когда-либо использовали. Только те, которые мы использовали во многих проектах, которые часто добавлялись в первых комитах.
Всё, что вы перечислили, не вписывается в имеющийся набор, т.к. очень специализированное.
> тащить гору мусора тоже не хочется
Какие объективные причины для такой точки зрения? Я уверен, что и в рельсах и во многих других гемах мало кто использует абсолютно все функции/файлы, которые едут в геме.phoenixweiss
08.10.2015 13:10+1Степень специализированности определяется той же командой которая использует этот тулсет. То есть если например завтра вы будете во всех проектах использовать feature_X, то велика вероятность что в тулсете это появится вне зависимости от того, нужно это или нет остальному сообществу. Подозреваю что в перспективе это может разрастись еще больше. Нет, я ни в коем случае не против такого подхода. Я просто говорю что наработки конкретной команды в их сыром виде не всегда удобно использовать остальному миру.
Что касается объективных причин такой точки зрения — её нет, чистый субъективизм. Но программисты — народ капризный и весьма принципиальный. Я частенько вычищаю из проектов лишние не используемые гемы и код. Иначе какое-то чувство дискомфорта остается. Это как спать с горой грязной посуды в раковине — кому-то всё равно, а кого-то это раздражает и он не может спокойно уснуть просто зная что всё не на своих местах. По этой же причине если мне нужно пройти 100 метров, я иду пешком а не вызываю такси. Возможно исходя из подобных суъбективных предубеждений, я не буду ставить большой мультифункциональный гем если мне из него нужен один единственный хэлпер.
Metus
08.10.2015 13:54+1Не имею ничего против предоставленных инструментов. Некоторые из них действительно занятны.
Но любой разработчик или команда смогут точно также как вы вытащить кучу кода из своих проектов и сделать подобные гемы.
И такие гемы, в среднем, будут не хуже и не лучше вашего, а будут просто описывать общее видение проекта и набор субъективно удобных утилит, правил наименований, структуры, хелперов и прочего.
Именно поэтому лучше не дампить в гем всё что тянется из проекта в проект, а выделить некоторые действительно полезные компоненты, подумать о нестандартных сценариях, улучшить их и добавить кастомизацию (если нужно) и только потом сделать гем. Но на это, конечно, нужно намного больше дополнительного времени.
Metus
Здравствуйте.
Скажите пожалуйста, а зачем разработчику ставить этот гем и писать
вместо того, чтобы сделать так:
или вообще создать свой хелпер или расширить i18n?
Просто это выглядит как набор никак не связанных друг с другом небольших решений, принятых в Вашей команде разработчиков, но объединённых в гем для удобства этой же команды.
printercu
Здравствуйте!
Компоненты действительно не связаны друг с другом. Почему они оказались вместе, я упомянул в начале поста. При этом в RailsStuff собраны, конечно, не все модули, которые мы когда-либо использовали. Только те, которые мы использовали во многих проектах, которые часто добавлялись в первых комитах. Чтобы не копить дубликаты файлов, вынесли их все в гем.
Относительно хэлпера перевода, я вижу несколько плюсов:
— Скорость работы вкупе с удобством использования. Для многих производительность в приложениях на рельсах — спорный вопрос, а жаль. Если это не аргумент, просто пропустите, пожалуйста. I18n.t — довольно сложный вызов и не самый быстрый. Время работы его растет, если используются фолбэки. Хранить перевод в локальных переменных, чтобы не переводить в циклах одно и то же — эффективнее, конечно, но больше писать надо, да и не все хотят.
— Один общий вариант использования. Не надо вспоминать, как надо переводить, нет длинных ключей, в которых можно ошибиться.
— Один общий способ организации переводов. Нет выбора, куда бы ещё положить перевод. Позволяет избежать повторений.
Свой хэлпер — это тоже хорошо. Просто тут он уже готовый. Только патчить I18n для этого не надо, имо :)