Контроллеры часто являются бельмом на глазу приложения Rails. Действия контроллера раздуты, несмотря на наши попытки сохранить их худыми, и даже когда они выглядят худыми, это часто иллюзия. Мы перемещаем сложность к различным before_actions
, не уменьшая упомянутую сложность. На самом деле, это часто требует значительных размышлений и умственной компиляции, чтобы почувствовать поток управления конкретным действием.
После некоторого использования сервисных объектов в команде разработчиков Tuts + стало очевидно, что мы можем применить некоторые из тех же принципов к действиям контроллера. В конце концов мы придумали шаблон, который работал хорошо и подтолкнул его в Aldous . Сегодня я рассмотрю действия контроллера Aldous и преимущества, которые они могут принести вашему приложению Rails.
Случай для разрыва каждого действия контроллера в классе
Разбить каждое действие на отдельный класс было первым, о чем мы подумали. Некоторые из более новых фреймворков, такие как Lotus, делают это «из коробки», и, приложив немного усилий, Rails также может воспользоваться этим.
Действия контроллера, которые являются единственным оператором if..else
являются соломенными чучелами. Даже в приложениях небольшого размера гораздо больше, чем это, которые проникают в домен контроллера. Существует аутентификация, авторизация и различные бизнес-правила на уровне контроллера (например, если человек заходит сюда и не вошел в систему, перенесите его на страницу входа). Некоторые действия контроллера могут быть довольно сложными, и вся эта сложность находится на уровне контроллера.
Учитывая то, за что может отвечать действие контроллера, кажется естественным, что мы инкапсулируем все это в класс. Затем мы можем намного проще проверить логику, так как мы надеемся, что у нас будет больше контроля над жизненным циклом этого класса. Это также позволило бы нам сделать эти классы действий контроллера гораздо более связными (сложные контроллеры RESTful с полным набором действий имеют тенденцию довольно быстро терять сцепление).
Существуют и другие проблемы с контроллерами Rails, такие как распространение состояния на объектах контроллера через переменные экземпляра, тенденция к формированию сложных иерархий наследования и т. Д. Перенос действий контроллера в их собственные классы также может помочь нам решить некоторые из них.
Что делать с актуальным Rails-контроллером
Без большого количества сложных взломов кода Rails мы не сможем избавиться от контроллеров в их текущем виде. Что мы можем сделать, так это превратить их в шаблон с небольшим количеством кода для делегирования классам действий контроллера. В Aldous контроллеры выглядят так:
1
2
3
4
5
|
class TodosController < ApplicationController
include Aldous::Controller
controller_actions :index, :new, :create, :edit, :update, :destroy
end
|
Мы включаем модуль, чтобы у нас был доступ к методу controller_actions
, и затем мы определяем, какие действия должен выполнять controller_actions
. Внутри Aldous будет сопоставлять эти действия с соответственно именованными классами в папке controller_actions/todos_controller
. Пока это не настраивается, но это легко сделать, и это разумное значение по умолчанию.
Основное действие Aldous Controller
Первое, что нам нужно сделать, это сообщить Rails, где найти действие нашего контроллера (как я уже упоминал выше), поэтому мы изменим наш app/config/application.rb
следующим образом:
1
2
3
4
5
6
7
|
config.autoload_paths += %W(
#{config.root}/app/controller_action
)
config.eager_load_paths += %W(
#{config.root}/app/controller_action
)
|
Теперь мы готовы написать действия контроллера Aldous. Простой может выглядеть так:
1
2
3
4
5
|
class TodosController::Index < BaseAction
def perform
build_view(Todos::IndexView)
end
end
|
Как вы можете видеть, это выглядит несколько похожим на сервисный объект, который разработан. Концептуально действие — это, по сути, услуга, поэтому для них имеет смысл иметь аналогичный интерфейс.
Однако есть две вещи, которые сразу неочевидны:
- откуда
BaseAction
и что в ней - что такое
build_view
Мы расскажем о BaseAction
ближайшее время. Но в этом действии также используются объекты вида Aldous, откуда build_view
. Мы не рассматриваем здесь объекты Aldous, и вам не нужно их использовать (хотя вы должны серьезно рассмотреть это). Ваше действие может выглядеть так:
1
2
3
4
5
|
class TodosController::Index < BaseAction
def perform
controller.render template: ‘todos/index’, locals: {}
end
end
|
Это более знакомо, и мы будем придерживаться этого отныне, чтобы не замуровать воды материалами, связанными с видом. Но откуда берется переменная контроллера?
Как выглядит конструктор для действия
Давайте поговорим о BaseAction
который мы видели выше. Это Aldous эквивалент ApplicationController
, поэтому настоятельно рекомендуется иметь его. BaseAction
является:
1
2
|
class BaseAction < ::Aldous::ControllerAction
end
|
Он наследует от ::Aldous::ControllerAction
и одна из вещей, которые он наследует, — это конструктор. Все действия контроллера Aldous имеют одинаковую сигнатуру конструктора:
1
2
3
4
5
|
attr_reader :controller
def initialize(controller)
@controller = controller
end
|
Какие данные доступны непосредственно из экземпляра контроллера
Будучи тем, чем они являются, мы тесно связали действия Aldous с контроллером, и поэтому они могут делать практически все, что может делать контроллер Rails. Очевидно, у вас есть доступ к экземпляру контроллера и вы можете получать любые данные оттуда. Но вы не хотите вызывать все на экземпляре контроллера — это было бы перетаскиванием для общих вещей, таких как параметры, заголовки и т. Д. Итак, с помощью немного магии Олдоса, прямо в действии доступны следующие вещи:
-
params
-
headers
-
request
-
response
-
cookies
И вы также можете сделать больше вещей доступным таким же образом через config/initializers/aldous.rb
:
1
2
3
|
Aldous.configuration do |aldous|
aldous.controller_methods_exposed_to_action += [:current_user]
end
|
Больше на Aldous Views или нет
Действия контроллера Aldous хорошо работают с объектами вида Aldous, но вы можете отказаться от использования объектов вида, если вы следуете нескольким простым правилам.
Действия контроллера Aldous не являются контроллерами, поэтому вы всегда должны указывать полный путь к представлению. Вы не можете сделать:
1
|
controller.render :index
|
Вместо этого вы должны сделать:
1
|
controller.render template: ‘todos/index’
|
Кроме того, поскольку действия Aldous не являются контроллерами, вы не сможете автоматически использовать переменные экземпляра из этих действий в шаблонах представлений, поэтому вам необходимо предоставить все данные как локальные, например:
1
|
controller.render template: ‘todos/index’, locals: {todos: Todo.all}
|
Отсутствие совместного использования состояния через переменные экземпляра может только улучшить ваш код представления, и более явный рендеринг также не повредит слишком сильно.
Более сложное действие контроллера Aldous
Давайте рассмотрим более сложное действие контроллера Aldous и поговорим о некоторых других вещах, которые дает нам Aldous, а также о некоторых лучших практиках написания действий контроллера Aldous.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class TodosController::Update < BaseAction
def default_view_data
super.merge(todo: todo)
end
def perform
controller.render(template: ‘home/show’, locals: default_view_data) and return unless current user
controller.render(template: ‘defaults/bad_request’, locals: {errors: [todo_params.error_message]}) and return unless todo_params.fetch
controller.render(template: ‘todos/not_found’, locals: default_view_data.merge(todo_id: params[:id])) and return unless todo
controller.render(template: ‘default/forbidden’, locals: default_view_data) and return unless current_ability.can?(:update, todo)
if todo.update_attributes(todo_params.fetch)
controller.redirect_to controller.todos_path
else
controller.render(template: ‘todos/edit’, locals: default_view_data)
end
end
private
def todo
@todo ||= Todo.where(id: params[:id]).first
end
def todo_params
TodosController::TodoParams.build(params)
end
end
|
Ключевым моментом здесь является то, что метод execute содержит всю или большую часть соответствующей логики уровня контроллера. Во-первых, у нас есть несколько строк для обработки локальных предварительных условий (то есть того, что должно быть правдой, чтобы у действия даже был шанс на успех). Все они должны быть одинарными, похожими на то, что вы видите выше. Единственная неприглядная вещь — это «и возвращение», которую мы должны продолжать добавлять. Это не было бы проблемой, если бы мы использовали представления Aldous, но сейчас мы застряли с этим.
Если условная логика для локального предусловия становится слишком сложной, ее следует извлечь в другой объект, который я называю объектом предиката, — таким образом, сложная логика может быть легко разделена и протестирована. Предикатные объекты могут стать концепцией в Aldous в какой-то момент.
После обработки локальных предварительных условий нам нужно выполнить основную логику действия. Есть два способа сделать это. Если ваша логика проста, как указано выше, просто выполните ее прямо здесь. Если это более сложно, вставьте его в объект службы и затем запустите службу.
В большинстве случаев метод perform
нашего действия должен быть аналогичным описанному выше или даже менее сложным в зависимости от того, сколько у вас локальных предварительных условий и вероятность сбоя.
Обработка сильных параметров
Еще одна вещь, которую вы видите в приведенном выше классе действий:
1
|
TodosController::TodoParams.build(params)
|
Это еще один объект, унаследованный от базового класса Aldous, и он здесь для того, чтобы несколько действий могли использовать логику строгих параметров. Это выглядит так:
1
2
3
4
5
6
7
8
9
|
class TodosController::TodoParams < Aldous::Params
def permitted_params
params.require(:todo).permit(:description, :user_id)
end
def error_message
‘Missing param :todo’
end
end
|
Вы предоставляете свою логику params в одном методе и сообщение об ошибке в другом. Затем вы просто создаете экземпляр объекта и вызываете fetch, чтобы получить разрешенные параметры. Он вернет nil
в случае ошибки.
Передача данных в представления
Еще один интересный метод в классе действий выше:
1
2
3
|
def default_view_data
super.merge(todo: todo)
end
|
Когда вы используете объекты представлений Aldous, существует некоторая магия, которая использует этот метод, но мы не используем их, поэтому нам нужно просто передать его как хеш-код локального объекта любому представлению, которое мы отображаем. Базовое действие также отменяет этот метод:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class BaseAction < ::Aldous::ControllerAction
def default_view_data
{
current_user: current_user,
current_ability: current_ability,
}
end
def current_user
@current_user ||= FindCurrentUserService.perform(session).user
end
def current_ability
@current_ability ||= Ability.new(current_user)
end
end
|
Вот почему нам нужно обязательно использовать super
когда мы снова переопределяем его в дочерних действиях.
Обработка перед действиями через объекты предварительных условий
Все вышеперечисленное великолепно, но иногда у вас есть глобальные предварительные условия, которые должны повлиять на все или большинство действий в системе (например, мы хотим сделать что-то с сеансом перед выполнением какого-либо действия и т. Д.). Как мы справимся с этим?
Это хорошая часть причины наличия BaseAction
. У Aldous есть концепция объектов предусловий — это в основном действия контроллера во всем, кроме имени. Вы настраиваете, какие классы действий должны выполняться перед каждым действием в методе BaseAction
, и Aldous автоматически сделает это за вас. Давайте посмотрим:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class BaseAction < ::Aldous::ControllerAction
def preconditions
[Shared::EnsureUserNotDisabledPrecondition]
end
def current_user
@current_user ||= FindCurrentUserService.perform(session).user
end
def current_ability
@current_ability ||= Ability.new(current_user)
end
end
|
Мы переопределяем метод предусловий и предоставляем класс нашего объекта предусловия. Этот объект может быть:
01
02
03
04
05
06
07
08
09
10
11
|
class Shared::EnsureUserNotDisabledPrecondition < BasePrecondition
delegate :current_user, :current_ability, to: :action
def perform
if current_user && current_user.disabled && !current_ability.can?(:manage, :all)
controller.render template: ‘default/forbidden’,
status: :forbidden,
locals: {errors: [‘Your account has been disabled’]}
end
end
end
|
Вышеуказанное предварительное условие наследуется от BasePrecondition
, которое просто:
1
2
|
class BasePrecondition < ::Aldous::Controller::Action::Precondition
end
|
Вам это на самом деле не нужно, если все ваши предварительные условия не потребуют совместного использования некоторого кода. Мы просто создаем его, потому что писать BasePrecondition
проще, чем ::Aldous::Controller::Action::Precondition
.
Вышеуказанное предварительное условие прекращает выполнение действия, поскольку оно отображает представление — Aldous сделает это за вас. Если ваше предварительное условие ничего не отображает и не перенаправляет (например, вы просто устанавливаете переменную в сеансе), тогда код действия будет выполнен после выполнения всех предварительных условий.
Если вы хотите, чтобы конкретное действие не было затронуто определенной предпосылкой, мы используем базовый Ruby для этого. Переопределите метод precondition
в вашем действии и отклоните любые предварительные условия, которые вам нравятся:
1
2
3
|
def preconditions
super.reject{|klass|
end
|
Не так, как в обычном Rails before_actions
, но в красивой оболочке ‘objecty’.
Безошибочные действия
Последнее, что нужно знать, это то, что действия контроллера безошибочны, как и объекты служб. Вам никогда не нужно спасать какой-либо код в методе выполнения действий контроллера — Aldous позаботится об этом за вас. Если возникает ошибка, Aldous спасет ее и использует default_error_handler
для обработки ситуации.
default_error_handler
— это метод, который вы можете переопределить в BaseAction. При использовании объектов вида Aldous это выглядит так:
1
2
3
|
def default_error_handler(error)
Defaults::ServerErrorView
end
|
Но так как мы не, вы можете сделать это вместо этого:
1
2
3
4
5
6
7
|
def default_error_handler(error)
controller.render(
template: ‘defaults/server_error’,
status: :internal_server_error,
locals: {errors: [error]}
)
end
|
Таким образом, вы обрабатываете нефатальные ошибки для своего действия как локальные предварительные условия и позволяете Олдосу беспокоиться о непредвиденных ошибках.
Вывод
Используя Aldous, вы можете заменить свои контроллеры Rails более мелкими, более связными объектами, которые намного меньше черного ящика и их гораздо проще тестировать. В качестве побочного эффекта вы можете уменьшить связывание во всем приложении, улучшить работу с представлениями и стимулировать повторное использование логики на уровне контроллера с помощью композиции.
Более того, действия контроллеров Aldous могут сосуществовать с контроллерами vanilla Rails без чрезмерного дублирования кода, поэтому вы можете начать использовать их в любом существующем приложении, с которым вы работаете. Вы также можете использовать действия контроллера Aldous, не принимая на себя обязательство использовать объекты или сервисы просмотра, если не хотите.
Aldous позволил нам отделить нашу скорость разработки от размера приложения, над которым мы работаем, и в то же время предоставил нам лучшую, более организованную базу кода. Надеюсь, он может сделать то же самое для вас.