Los controladores son a menudo la monstruosidad de una aplicación de Rails. Las acciones del controlador están infladas a pesar de nuestros intentos de mantenerlas delgadas, e incluso cuando se ven delgadas, a menudo es una ilusión. Movemos la complejidad a varios. before_actions
, sin reducir dicha complejidad. De hecho, a menudo requiere excavaciones significativas y compilación mental para tener una idea del flujo de control de una acción en particular..
Después de usar los objetos de servicio durante un tiempo en el equipo de Tuts + dev, se hizo evidente que podemos aplicar algunos de los mismos principios a las acciones del controlador. Finalmente se nos ocurrió un patrón que funcionó bien y lo introdujimos en Aldous. Hoy veré las acciones del controlador Aldous y los beneficios que pueden aportar a su aplicación Rails.
Lo primero que pensamos fue romper con cada acción en una clase separada. Algunos de los marcos más nuevos, como Lotus, hacen esto de manera inmediata, y con un poco de trabajo, Rails también podría aprovechar esto..
Controlador de acciones que son un solo. si ... otra cosa
declaración es un hombre de paja. Incluso las aplicaciones de tamaño modesto tienen muchas más cosas que eso, introduciéndose en el dominio del controlador. Hay autenticación, autorización y varias reglas de negocios a nivel de controlador (por ejemplo, si una persona va aquí y no ha iniciado sesión, llévela a la página de inicio de sesión). Algunas acciones del controlador pueden ser bastante complejas, y toda la complejidad está firmemente en el ámbito de la capa del controlador.
Dado que la acción de un controlador puede ser responsable, parece natural que encapsulemos todo eso en una clase. Luego podemos probar la lógica mucho más fácilmente, ya que esperamos que tengamos más control del ciclo de vida de esa clase. También nos permitiría hacer que estas clases de acción de controlador sean mucho más cohesivas (los controladores RESTful complejos con un complemento completo de acciones tienden a perder cohesión bastante rápidamente).
Existen otros problemas con los controladores de Rails, como la proliferación de estados en los objetos de controlador a través de variables de instancia, la tendencia a que se formen jerarquías de herencia complejas, etc..
Sin una gran cantidad de piratería compleja en el código de Rails, realmente no podemos deshacernos de los controladores en su forma actual. Lo que podemos hacer es convertirlos en repetitivo con una pequeña cantidad de código para delegar en las clases de acción del controlador. En Aldous, los controladores se ven así:
clase TodosController < ApplicationController include Aldous::Controller controller_actions :index, :new, :create, :edit, :update, :destroy end
Incluimos un módulo para que tengamos acceso a la controlador_acciones
Método, y luego indicamos qué acciones debe tener el controlador. Internamente, Aldous asignará estas acciones a las clases con nombres correspondientes en el controller_actions / todos_controller
carpeta. Esto no es configurable todavía, pero se puede hacer fácilmente, y es un valor predeterminado razonable.
Lo primero que debemos hacer es decirle a Rails dónde encontrar la acción de nuestro controlador (como mencioné anteriormente), por lo que modificamos nuestra app / config / application.rb
al igual que:
config.autoload_paths + =% W (# config.root / app / controller_action) config.eager_load_paths + =% W (# config.root / app / controller_action)
Ahora estamos listos para escribir las acciones del controlador Aldous. Una simple podría tener este aspecto:
clase TodosController :: Index < BaseAction def perform build_view(Todos::IndexView) end end
Como puede ver, se ve algo similar a un objeto de servicio, que es por diseño. Conceptualmente, una acción es básicamente un servicio, por lo que tiene sentido que tengan una interfaz similar.
Hay, sin embargo, dos cosas que no son obvias de inmediato:
BaseAction
viene de y lo que hay en ellaconstruir_vista
esNosotros cubriremos BaseAction
dentro de poco. Pero esta acción también está utilizando objetos de vista de Aldous, que es donde construir_vista
viene de. No estamos cubriendo los objetos de vista de Aldous aquí y no tienes que usarlos (aunque deberías considerarlo seriamente). Tu acción puede fácilmente verse así en su lugar:
clase TodosController :: Index < BaseAction def perform controller.render template: 'todos/index', locals: end end
Esto es más familiar y lo seguiremos haciendo a partir de ahora, para no enturbiar las aguas con cosas relacionadas con la vista. Pero de donde viene la variable del controlador?
Hablemos de la BaseAction
que vimos arriba Es el equivalente de Aldous de Controlador de aplicaciones
, por lo que es muy recomendable tener uno. Un esqueleto BaseAction
es:
clase BaseAction < ::Aldous::ControllerAction end
Hereda de :: Aldous :: ControllerAction
y una de las cosas que hereda es un constructor. Todas las acciones del controlador Aldous tienen la misma firma de constructor:
attr_reader: el controlador def initialize (controller) @controller = controller end
Siendo lo que son, hemos unido las acciones de Aldous a un controlador para que puedan hacer casi todo lo que un controlador Rails puede hacer. Obviamente, usted tiene acceso a la instancia del controlador y puede extraer los datos que desee desde allí. Pero no quiere estar llamando a todo en la instancia del controlador, eso sería un lastre para cosas comunes como parámetros, encabezados, etc. Entonces, a través de un poco de magia de Aldous, las siguientes cosas están disponibles en la acción directamente:
params
encabezados
solicitud
respuesta
galletas
Y también puede hacer que haya más cosas disponibles de la misma manera a través de un inicializador. config / initializers / aldous.rb
:
Aldous.configuration do | aldous | aldous.controller_methods_exposed_to_action + = [: current_user] end
Las acciones del controlador de Aldous están diseñadas para funcionar bien con los objetos de vista de Aldous, pero puede optar por no usar los objetos de vista si sigue unas pocas reglas simples.
Las acciones de controlador de Aldous no son controladores, por lo que siempre debe proporcionar la ruta completa a una vista. No puedes hacer
controller.render: index
En su lugar tienes que hacer:
Plantilla controller.render: 'todos / index'
Además, dado que las acciones de Aldous no son controladores, no podrá tener variables de instancia de estas acciones automáticamente disponibles en las plantillas de vista, por lo que debe proporcionar todos los datos como locales, por ejemplo:
plantilla controller.render: 'todos / index', locals: todos: Todo.all
No compartir el estado a través de las variables de instancia solo puede mejorar su código de vista, y una representación más explícita tampoco afectará demasiado.
Veamos una acción de controlador Aldous más compleja y hablemos sobre algunas de las otras cosas que Aldous nos brinda, así como algunas de las mejores prácticas para escribir acciones de controlador Aldous..
clase TodosController :: Actualizar < 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
La clave aquí es para el realizar
Método para contener toda o la mayor parte de la lógica relevante a nivel de controlador. Primero, tenemos unas pocas líneas para manejar las condiciones previas locales (es decir, las cosas que deben ser ciertas para que la acción tenga la oportunidad de tener éxito). Todos estos deben ser de una sola línea similar a lo que ves arriba. Lo único desagradable es el 'y retorno' que debemos seguir agregando. Esto no sería un problema si tuviéramos que usar las vistas de Aldous, pero por ahora estamos atascados con eso.
Si la lógica condicional para la condición previa local se vuelve demasiado compleja, debería extraerse en otro objeto, al que llamo un objeto de predicado; de esta manera, la lógica compleja se puede compartir y probar fácilmente. Los objetos predicados pueden convertirse en un concepto dentro de Aldous en algún momento.
Una vez que se manejan las condiciones previas locales, debemos realizar la lógica central de la acción. Hay dos maneras de hacer esto. Si su lógica es simple, como está arriba, simplemente ejecútela allí. Si es más complejo, empújelo en un objeto de servicio y luego ejecute el servicio.
La mayor parte del tiempo nuestra acción es realizar
El método debe ser similar al de arriba, o incluso menos complejo dependiendo de cuántas condiciones previas locales tenga y la posibilidad de falla..
Otra cosa que ves en la clase de acción anterior es:
TodosController :: TodoParams.build (params)
Este es otro objeto que se hereda de una clase base de Aldous, y está aquí para que múltiples acciones puedan compartir una lógica de parámetros fuerte. Parece que sí
clase TodosController :: TodoParams < Aldous::Params def permitted_params params.require(:todo).permit(:description, :user_id) end def error_message 'Missing param :todo' end end
Suministra su lógica de parámetros en un método y un mensaje de error en otro. Luego simplemente crea una instancia del objeto y llama a fetch para obtener los parámetros permitidos. Volverá nulo
en caso de error.
Otro método interesante en la clase de acción anterior es:
def default_view_data super.merge (todo: todo) final
Cuando usas los objetos de vista de Aldous, hay algo de magia que usa este método, pero no los estamos usando, por lo que necesitamos simplemente pasarlo como un hash local a cualquier vista que procesemos. La acción base también anula este método:
clase 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
Por eso necesitamos asegurarnos de usar súper
Cuando lo volvemos a anular de nuevo en acciones infantiles..
Todo lo anterior es excelente, pero a veces tiene condiciones previas globales que deben afectar todas o la mayoría de las acciones en el sistema (por ejemplo, queremos hacer algo con la sesión antes de ejecutar cualquier acción, etc.). Como manejamos eso?
Esta es una buena parte de la razón para tener un BaseAction
. Aldous tiene un concepto de objetos de condición previa: son básicamente acciones de controlador en todo menos en el nombre. Configura qué clases de acción deben ejecutarse antes de cada acción en un método en el BaseAction
, y Aldous hará esto automáticamente por ti. Echemos un vistazo:
clase 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
Anulamos el método de condiciones previas y suministramos la clase de nuestro objeto de condición previa. Este objeto podría ser:
Clase compartida :: Garantía de usuarioNotDisabledPrecondition < 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
La condición previa anterior hereda de BasePrecondición
, que es simplemente:
clase BasePrecondition < ::Aldous::Controller::Action::Precondition end
Realmente no necesita esto a menos que todas sus condiciones previas necesiten compartir algún código. Simplemente lo creamos porque la escritura. BasePrecondición
es más fácil que :: Aldous :: Controller :: Action :: Precondition
.
La condición previa anterior finaliza la ejecución de la acción, ya que representa un view-Aldous hará esto por usted. Si su condición previa no representa ni redirige nada (por ejemplo, simplemente establece una variable en la sesión), el código de acción se ejecutará después de que se hayan completado todas las condiciones previas..
Si quieres que una acción en particular no se vea afectada por una condición previa en particular, usamos Ruby básico para lograr esto. Anular el condición previa
Método en tu acción y rechazar las condiciones previas que te gustan:
condiciones previas super.reject | klass | klass == Compartido :: AsegurarUsuarioNotDisechaPrecondition fin
No es tan diferente a los rieles regulares before_actions
, pero envuelto en una bonita concha 'objetiva'.
Lo último a tener en cuenta es que las acciones del controlador están libres de errores, al igual que los objetos de servicio. Nunca necesita rescatar ningún código en el método de ejecución de la acción del controlador: Aldous manejará esto por usted. Si ocurre un error, Aldous lo rescatará y utilizará el default_error_handler
para manejar la situación.
los default_error_handler
es un método que puede anular en su BaseAction. Cuando se usan objetos de vista de Aldous, se ve así:
def default_error_handler (error) Predeterminados :: ServerErrorView end
Pero como no lo somos, puedes hacer esto en su lugar:
def default_error_handler (error) controller.render (plantilla: 'defaults / server_error', status:: internal_server_error, locals: errors: [error]) end
Así que maneja los errores no fatales de su acción como condiciones previas locales y deja que Aldous se preocupe por los errores inesperados..
Usando Aldous, puede reemplazar sus controladores Rails por objetos más pequeños y más cohesivos que son mucho menos de una caja negra y son mucho más fáciles de probar. Como efecto secundario, puede reducir el acoplamiento en toda la aplicación, mejorar la forma de trabajar con las vistas y promover la reutilización de la lógica en la capa del controlador a través de la composición.
Mejor aún, las acciones del controlador Aldous pueden coexistir con los controladores de Rails vanilla sin demasiada duplicación de código, por lo que puede comenzar a usarlos en cualquier aplicación existente con la que esté trabajando. También puede usar las acciones del controlador Aldous sin comprometerse a usar objetos de vista o servicios a menos que desee.
Aldous nos ha permitido desacoplar nuestra velocidad de desarrollo del tamaño de la aplicación en la que estamos trabajando, al tiempo que nos brinda un código base mejor y más organizado a largo plazo. Esperemos que pueda hacer lo mismo por ti..