Une action de contrôleur de classe par rail avec Aldous

Les contrôleurs sont souvent la source de pollution d’une application Rails. Les actions des contrôleurs sont gonflées malgré nos tentatives pour les garder maigres, et même quand ils ont l’air maigre, c’est souvent une illusion. Nous déplaçons la complexité à divers before_actions, sans réduire ladite complexité. En fait, cela nécessite souvent des recherches approfondies et une compilation mentale pour avoir une idée du flux de contrôle d'une action particulière.. 

Après avoir utilisé des objets de service pendant un certain temps dans l'équipe Tuts + dev, il est devenu évident que nous pourrions peut-être appliquer certains des mêmes principes aux actions du contrôleur. Nous avons finalement trouvé un modèle qui a bien fonctionné et qui a été introduit dans Aldous. Aujourd'hui, je vais examiner les actions des contrôleurs Aldous et les avantages qu'ils peuvent apporter à votre application Rails..

Les arguments en faveur de la décomposition de chaque action de contrôleur en classe

La première chose à laquelle nous avons pensé a été de répartir chaque action dans une classe séparée. Certains des nouveaux frameworks tels que Lotus le font de manière originale, et avec un peu de travail, Rails pourrait également en tirer parti..

Actions du contrôleur qui sont un seul sinon déclaration sont un homme de paille. Même les applications de taille modeste ont beaucoup plus de choses que cela, s'insinuant dans le domaine du contrôleur. Il existe une authentification, une autorisation et diverses règles de gestion au niveau du contrôleur (par exemple, si une personne se rend ici et qu'elle n'est pas connectée, dirigez-la vers la page de connexion). Certaines actions du contrôleur peuvent devenir assez complexes, et toute la complexité est clairement du ressort de la couche contrôleur.

Étant donné à quel point une action de contrôleur peut être responsable, il semble naturel que nous encapsulions tout cela dans une classe. Nous pouvons alors tester la logique beaucoup plus facilement, car nous espérons pouvoir mieux contrôler le cycle de vie de cette classe. Cela nous permettrait également de rendre ces classes d'actions de contrôleurs beaucoup plus cohérentes (des contrôleurs RESTful complexes avec une gamme complète d'actions tendent à perdre de la cohésion assez rapidement).. 

Il y a d'autres problèmes avec les contrôleurs Rails, tels que la prolifération d'état sur les objets de contrôleur via des variables d'instance, la tendance à la formation de hiérarchies d'héritage complexes, etc. Le fait de placer les actions de contrôleur dans leurs propres classes peut également nous aider à en traiter certains.

Que faire avec le contrôleur Rails actuel

Image de Mack Male

Sans beaucoup de piratage complexe du code Rails, nous ne pouvons pas vraiment nous débarrasser des contrôleurs dans leur forme actuelle. Ce que nous pouvons faire, c'est les transformer en standard avec une petite quantité de code à déléguer aux classes d'action du contrôleur. Dans Aldous, les contrôleurs ressemblent à ceci:

classe TodosController < ApplicationController include Aldous::Controller controller_actions :index, :new, :create, :edit, :update, :destroy end

Nous incluons un module pour que nous ayons accès au controller_actions méthode, et nous indiquons ensuite quelles actions le contrôleur devrait avoir. En interne, Aldous mappera ces actions sur les classes nommées en conséquence dans controller_actions / todos_controller dossier. Ce n'est pas encore configurable, mais peut être facilement fait ainsi, et c'est un défaut raisonnable.

Une action basique du contrôleur Aldous

La première chose à faire est de dire à Rails où trouver notre action de contrôleur (comme je l’ai mentionné ci-dessus), afin de modifier notre app / config / application.rb ainsi:

config.autoload_paths + =% W (# config.root / app / controller_action) config.eager_load_paths + =% W (# config.root / app / controller_action)

Nous sommes maintenant prêts à écrire les actions du contrôleur Aldous. Un simple pourrait ressembler à ceci:

classe TodosController :: Index < BaseAction def perform build_view(Todos::IndexView) end end

Comme vous pouvez le constater, il ressemble un peu à un objet de service, ce qui est inhérent au projet. Conceptuellement, une action est essentiellement un service, il est donc logique qu’ils aient une interface similaire..

Il y a cependant deux choses qui ne sont pas évidentes immédiatement:

  • où Action de base vient de et ce qu'il y a dedans
  • quoi build_view est

Nous couvrirons Action de base prochainement. Mais cette action utilise également des objets de vue Aldous, qui est où build_view vient de. Nous ne couvrons pas les objets de vue Aldous ici et vous n'avez pas à les utiliser (bien que vous deviez y réfléchir sérieusement). Votre action peut facilement ressembler à ceci à la place:

classe TodosController :: Index < BaseAction def perform controller.render template: 'todos/index', locals:  end end

Ceci est plus familier et nous y tiendrons désormais, afin de ne pas brouiller les cartes avec des éléments liés à la vue. Mais d'où vient la variable contrôleur??

À quoi ressemble le constructeur d'une action

Parlons de la Action de base que nous avons vu ci-dessus. C'est l'équivalent Aldous de ApplicationController, il est donc fortement recommandé d’en avoir un. Un nu-os Action de base est:

classe BaseAction < ::Aldous::ControllerAction end

Il hérite de :: Aldous :: ControllerAction et l'une des choses dont il hérite est un constructeur. Toutes les actions du contrôleur Aldous ont la même signature de constructeur:

attr_reader: contrôleur def initialize (contrôleur) @controller = fin du contrôleur

Quelles données sont directement disponibles à partir de l'instance de contrôleur

Etant ce qu’ils sont, nous avons étroitement couplé les actions Aldous à un contrôleur afin qu’il puisse faire à peu près tout ce que peut faire un contrôleur Rails. De toute évidence, vous avez accès à l'instance du contrôleur et pouvez extraire toutes les données que vous souhaitez. Mais vous ne voulez pas tout appeler sur l'instance du contrôleur, ce qui constituerait un frein pour des choses courantes comme les paramètres, les en-têtes, etc. Ainsi, grâce à un peu de magie Aldous, les éléments suivants sont directement disponibles sur l'action:

  • params
  • en-têtes
  • demande
  • réponse
  • biscuits

Et vous pouvez également rendre plus de choses disponibles de la même manière via un initialiseur config / initializers / aldous.rb:

Aldous.configuration do | aldous | aldous.controller_methods_exposed_to_action + = [[current_user] end

Plus sur Aldous Views or Not

Les actions du contrôleur Aldous sont conçues pour fonctionner correctement avec les objets vue Aldous, mais vous pouvez choisir de ne pas utiliser les objets vue si vous suivez quelques règles simples..

Les actions des contrôleurs Aldous ne sont pas des contrôleurs. Vous devez donc toujours indiquer le chemin complet vers une vue. Tu ne peux pas faire:

controller.render: index

Au lieu de cela, vous devez faire:

modèle controller.render: 'todos / index'

De plus, les actions Aldous n'étant pas des contrôleurs, les variables d'instance de ces actions ne seront pas automatiquement disponibles dans les modèles de vue. Vous devez donc fournir toutes les données sous forme de variables locales, par exemple:

modèle controller.render: 'todos / index', locals: todos: Todo.all

Ne pas partager l'état via des variables d'instance ne peut qu'améliorer votre code d'affichage, et un rendu plus explicite ne fera pas trop de mal non plus..

Une action de contrôleur aldous plus complexe

Image de Howard Lake

Examinons une action plus complexe du contrôleur Aldous et discutons de certaines des autres choses que Aldous nous donne, ainsi que des meilleures pratiques d'écriture des actions du contrôleur Aldous..

classe 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

La clé ici est pour le effectuer méthode pour contenir tout ou la plupart de la logique de niveau de contrôleur pertinente. Nous avons d’abord quelques lignes pour gérer les conditions préalables locales (c’est-à-dire ce qui doit être vrai pour que l’action ait même une chance de réussir). Ceux-ci devraient tous être des one-liners semblables à ce que vous voyez ci-dessus. Le seul inconvénient est le «retour» que nous devons continuer à ajouter. Ce ne serait pas un problème si nous utilisions les vues d'Aldous, mais pour l'instant nous sommes coincés avec elle.. 

Si la logique conditionnelle pour la précondition locale devient trop complexe, elle doit être extraite dans un autre objet, que j'appelle un objet de prédicat. Ainsi, la logique complexe peut facilement être partagée et testée. Les objets de prédicats peuvent devenir un concept chez Aldous à un moment donné.

Une fois que les conditions préalables locales sont gérées, nous devons exécuter la logique de base de l’action. Il y a deux façons d'aborder cela. Si votre logique est simple, comme ci-dessus, exécutez-la ici. S'il est plus complexe, insérez-le dans un objet de service, puis exécutez le service.. 

La plupart du temps nos actions sont effectuer la méthode doit être similaire à celle ci-dessus, ou même moins complexe, en fonction du nombre de conditions préalables locales que vous avez et de la possibilité d'échec.

Gestion des paramètres puissants

Une autre chose que vous voyez dans la classe d'action ci-dessus est:

TodosController :: TodoParams.build (paramètres)

Ceci est un autre objet qui hérite d'une classe de base Aldous, et ceux-ci sont là pour permettre à plusieurs actions de partager une logique de paramètres forte. Cela ressemble à ceci:

classe TodosController :: TodoParams < Aldous::Params def permitted_params params.require(:todo).permit(:description, :user_id) end def error_message 'Missing param :todo' end end

Vous fournissez votre logique params dans une méthode et un message d'erreur dans une autre. Il vous suffit ensuite d’instancier l’objet et d’appeler l’extraction pour obtenir les paramètres autorisés. Il reviendra néant en cas d'erreur.

Passer des données aux vues

Une autre méthode intéressante dans la classe d'action ci-dessus est la suivante:

def default_view_data super.merge (todo: todo) end

Lorsque vous utilisez les objets de vue Aldous, cette méthode utilise une certaine magie, mais nous ne les utilisons pas. Nous devons donc simplement la transmettre sous forme de hachage de paramètres locaux à toutes les vues que nous rendons. L'action de base remplace également cette méthode:

classe 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

C’est pourquoi nous devons nous assurer d’utiliser super quand on le redéfinit dans les actions enfants.

Gestion des actions avant via des objets de précondition

Tout ce qui précède est excellent, mais vous avez parfois des conditions préalables globales, qui doivent affecter toutes ou la plupart des actions du système (par exemple, nous voulons utiliser quelque chose avec la session avant d'exécuter une action, etc.). Comment traitons-nous cela?

C’est une bonne partie de la raison d’avoir un Action de base. Aldous a un concept d’objets de précondition. C’est essentiellement des actions de contrôleur dans tout sauf le nom. Vous configurez les classes d’actions à exécuter avant chaque action d’une méthode sur le Action de base, et Aldous le fera automatiquement pour vous. Regardons:

classe 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

Nous substituons la méthode de préconditions et fournissons la classe de notre objet de précondition. Cet objet pourrait être:

classe 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

La condition précitée hérite de BasePrecondition, qui est simplement:

classe BasePrecondition < ::Aldous::Controller::Action::Precondition end

Vous n’avez vraiment pas besoin de cela sauf si toutes vos conditions préalables doivent partager du code. Nous le créons simplement parce que nous écrivons BasePrecondition est plus facile que :: Aldous :: Controller :: Action :: Conditions préalables.

La condition ci-dessus met fin à l'exécution de l'action car elle rend une vue, Aldous le fera pour vous. Si votre condition préalable ne restitue ni ne redirige quoi que ce soit (par exemple, vous définissez simplement une variable dans la session), le code d'action sera exécuté une fois toutes les conditions préalables remplies.. 

Si vous souhaitez qu'une action particulière ne soit pas affectée par une condition préalable particulière, nous utilisons Ruby de base pour accomplir cela. Remplacer le condition préalable méthode dans votre action et rejeter toutes les conditions préalables que vous aimez:

def preconditions super.reject | klass | klass == Shared :: EnsureUserNotDisabledPrecondition end

Pas si différent des rails ordinaires before_actions, mais enveloppé dans une belle coquille 'objecty'.

Actions sans erreur

Image de Duncan Hull

La dernière chose à prendre en compte est que les actions du contrôleur sont sans erreur, tout comme les objets de service. Vous n'avez jamais besoin de récupérer de code dans l'action du contrôleur. La méthode est exécutée par Aldous. Si une erreur se produit, Aldous la sauvera et utilisera le default_error_handler gérer la situation.

le default_error_handler est une méthode que vous pouvez remplacer sur votre BaseAction. Lorsque vous utilisez les objets de vue Aldous, cela ressemble à ceci:

def default_error_handler (error) Defaults :: ServerErrorView end

Mais puisque nous ne le sommes pas, vous pouvez le faire à la place:

def default_error_handler (error) controller.render (template: 'defaults / server_error', statut:: internal_server_error, variables locales: errors: [error]) end

Ainsi, vous gérez les erreurs non fatales de votre action en tant que conditions préalables locales et laissez Aldous s’inquiéter des erreurs inattendues..

Conclusion

En utilisant Aldous, vous pouvez remplacer vos contrôleurs Rails par des objets plus petits, plus cohérents, beaucoup moins propres à une boîte noire et beaucoup plus faciles à tester. En tant qu'effet secondaire, vous pouvez réduire le couplage dans l'ensemble de votre application, améliorer votre travail avec les vues et favoriser la réutilisation de la logique dans la couche de votre contrôleur via la composition..

Mieux encore, les actions des contrôleurs Aldous peuvent coexister avec les contrôleurs vanilla Rails sans trop de duplication de code. Vous pouvez donc commencer à les utiliser dans n’importe quelle application existante avec laquelle vous travaillez. Vous pouvez également utiliser les actions du contrôleur Aldous sans vous engager à utiliser des objets de vue ou des services, sauf si vous souhaitez. 

Aldous nous a permis de dissocier notre vitesse de développement de la taille de l'application sur laquelle nous travaillons, tout en nous offrant une base de code meilleure et mieux organisée à long terme. Espérons que cela puisse faire la même chose pour vous.