Build Dumb, Refactor Smart : comment éliminer les problèmes du code Ruby on Rails
Publié: 2022-03-11Parfois, les clients nous soumettent des demandes de fonctionnalités que nous n'aimons vraiment pas. Ce n'est pas que nous n'aimons pas nos clients, nous aimons nos clients. Ce n'est pas que nous n'aimons pas la fonctionnalité - la plupart des fonctionnalités demandées par les clients correspondent parfaitement à leurs objectifs commerciaux et à leurs revenus.
Parfois, nous n'aimons pas une demande de fonctionnalité parce que la façon la plus simple de la résoudre est d'écrire du mauvais code, et nous n'avons pas de solution élégante en tête. Cela enverra beaucoup d'entre nous, développeurs Rails, dans des recherches infructueuses via RubyToolbox, GitHub, les blogs de développeurs et StackOverflow à la recherche d'un joyau, d'un plugin ou d'un exemple de code qui nous fera nous sentir mieux dans notre peau.
Refactoriser le code Ruby on Rails
Eh bien, je suis ici pour vous dire : c'est normal d'écrire du mauvais code. Parfois, un mauvais code Rails est plus facile à refactoriser en un beau code qu'une solution mal pensée implémentée en un temps record.
C'est le processus de refactorisation de Rails que j'aime suivre lorsque j'élimine les problèmes de mes horribles solutions de pansement :
Pour une perspective alternative, voici le journal de validation Git pour une fonctionnalité qui a été refactorisée étape par étape :
Et voici un autre article intéressant sur la refactorisation à grande échelle d'un collègue du réseau Toptal.
Voyons comment c'est fait.
Les vues
Étape 1. Commencez dans les vues
Disons que nous commençons un ticket pour une nouvelle fonctionnalité. Le client nous dit : "Les visiteurs doivent pouvoir voir une liste des projets actifs sur la page d'accueil."
Ce ticket nécessite un changement visible, donc un endroit raisonnable pour commencer le travail serait dans les Vues. Le problème est simple et nous avons tous été entraînés à le résoudre plusieurs fois. Je vais le résoudre The Wrong Way et montrer comment refactoriser ma solution dans ses domaines appropriés. Résoudre un problème The Wrong Way peut nous aider à surmonter l'obstacle de ne pas connaître la bonne solution.
Pour commencer, supposons que nous ayons un modèle appelé Project
avec un attribut booléen appelé active
. Nous voulons obtenir une liste de tous les Projects
où active
est égal à true
, nous pouvons donc utiliser Project.where(active: true)
et boucler dessus avec each
bloc.
app/views/pages/welcome.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name
Je sais ce que vous dites : "Cela ne passerait jamais une révision de code" ou "Mon client me virerait sûrement pour ça." Oui, cette solution rompt la séparation des problèmes Modèle-Vue-Contrôleur, elle peut entraîner des appels de base de données parasites difficiles à tracer et elle peut devenir difficile à maintenir à l'avenir. Mais considérez la valeur de le faire dans le mauvais sens :
- Vous pouvez obtenir ce changement sur la mise en scène en moins de 15 minutes.
- S'il est laissé, ce bloc est facile à mettre en cache.
- La résolution de ce problème Rails est simple (peut être confiée à un développeur junior).
Étape 2. Partiels
Après l'avoir fait The Wrong Way , je me sens mal dans ma peau et je veux isoler mon mauvais code. Si ce changement ne concernait clairement que la couche View, je pourrais refactoriser ma honte en un partiel.
app/views/pages/welcome.haml: = render :partial => 'shared/projects_list' app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name
C'est un peu mieux. De toute évidence, nous commettons toujours l'erreur d'une requête de modèle dans une vue, mais au moins, lorsqu'un responsable viendra plus tard et verra mon horrible partiel, il aura un moyen simple de résoudre ce problème de code Rails en particulier. Réparer quelque chose d'évidemment stupide est toujours plus facile que de réparer une abstraction mal implémentée et boguée.
Étape 3. Aides
Les Helpers dans Rails sont un moyen de créer un DSL (Domain Specific Language) pour une section de vos Vues. Vous devez réécrire votre code en utilisant content_tag au lieu de haml ou HTML, mais vous avez l'avantage d'être autorisé à manipuler des structures de données sans que d'autres développeurs ne vous regardent pendant 15 lignes de code View non imprimable.
Si je devais utiliser des aides ici, je refactoriserais probablement la balise li
.
app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| = project_list_item(project) app/helpers/projects_helper.rb: def project_list_item(project) content_tag(:li, :class => 'project') do link_to project_path(project), project.name end end
Les contrôleurs
Étape 4. Déplacez-le vers les contrôleurs
Peut-être que votre horrible code n'est pas seulement un problème de View. Si votre code sent toujours mauvais, recherchez les requêtes que vous pouvez faire passer des vues aux contrôleurs.

app/views/shared/projects_list.haml: %ul.projects - @projects_list.each do |project| = project_list_item(project) app/controllers/pages_controller.rb: def welcome @projects = Project.where(active: true) end
Étape 5. Filtres du contrôleur
La raison la plus évidente de déplacer du code dans un Controller before_filter
ou after_filter
concerne le code que vous avez dupliqué dans plusieurs actions Controller. Vous pouvez également déplacer du code dans un filtre Controller si vous souhaitez séparer l'objectif de l'action Controller des exigences de vos vues.
app/controllers/pages_controller.rb: before_filter :projects_list def welcome end def projects_list @projects = Project.where(active:true) end
Étape 6. Contrôleur d'application
Supposons que vous ayez besoin que votre code apparaisse sur chaque page, ou que vous souhaitiez rendre les fonctions d'assistance du contrôleur disponibles pour tous les contrôleurs, vous pouvez déplacer votre fonction dans ApplicationController. Si les modifications sont globales, vous pouvez également modifier la présentation de votre application.
app/controllers/pages_controller.rb: def welcome end app/views/layouts/application.haml: %ul.projects - projects_list.each do |project| = project_list_item(project) app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.where(active: true) end
Les modèles
Étape 7. Refactorisation vers le modèle
Comme le dit la devise de MVC : Fat Model, Skinny Controllers et Dumb Views . Nous sommes censés refactoriser tout ce que nous pouvons dans le modèle, et il est vrai que les fonctionnalités les plus complexes finiront par devenir des associations de modèles et des méthodes de modèles. Nous devons toujours éviter de formater/afficher les choses dans le modèle, mais la transformation de données en d'autres types de données est autorisée. Dans ce cas, la meilleure chose à refactoriser dans le modèle serait la clause where(active: true)
, que nous pouvons transformer en portée. L'utilisation d'une portée est utile non seulement parce qu'elle rend l'appel plus joli, mais si jamais nous décidions d'ajouter un attribut comme delete
ou outdated
, nous pouvons modifier cette portée au lieu de rechercher toutes nos clauses where
.
app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.active end app/models/project.rb: scope :active, where(active: true)
Étape 8. Filtres de modèle
Nous n'avons pas d'utilisation particulière pour les after_save filters
before_save
ou after_save d'un modèle dans ce cas, mais l'étape suivante que je prends habituellement consiste à déplacer les fonctionnalités des contrôleurs et des méthodes de modèle vers les filtres de modèle.
Supposons que nous ayons un autre attribut, num_views
. Si num_views > 50
, le projet devient inactif. Nous pourrions résoudre ce problème dans la vue, mais apporter des modifications à la base de données dans une vue est inapproprié. Nous pourrions le résoudre dans le contrôleur, mais nos contrôleurs doivent être aussi fins que possible ! Nous pouvons le résoudre facilement dans le modèle.
app/models/project.rb: before_save :deactivate_if_over_num_views def deactivate_if_over_num_views if num_views > 50 self.active = false fi end
Remarque : vous devez éviter d'appeler self.save
dans un filtre de modèle, car cela provoque des événements d'enregistrement récursifs, et la couche de manipulation de base de données de votre application doit de toute façon être le contrôleur.
Étape 9. Bibliothèques
Parfois, votre fonctionnalité est suffisamment grande pour justifier sa propre bibliothèque. Vous voudrez peut-être le déplacer dans un fichier de bibliothèque car il est réutilisé dans de nombreux endroits ou s'il est suffisamment volumineux pour que vous souhaitiez le développer séparément.
C'est bien de stocker des fichiers de bibliothèque dans le répertoire lib/ , mais au fur et à mesure qu'ils grandissent, vous pouvez les transférer dans un vrai RubyGem ! L'un des principaux avantages du déplacement de votre code dans une bibliothèque est que vous pouvez tester la bibliothèque séparément de votre modèle.
Quoi qu'il en soit, dans le cas d'une liste de projets, nous pourrions justifier de déplacer l'appel scope :active
du modèle de Project
vers un fichier de bibliothèque, et de le ramener dans Ruby :
app/models/project.rb: class Project < ActiveRecord::Base include Activeable before_filter :deactivate_if_over_num_views end lib/activeable.rb: module Activeable def self.included(k) k.scope :active, k.where(active: true) end def deactivate_if_over_num_views if num_views > 50 self.active = false end end end
Remarque : la méthode self.included
est appelée lorsqu'une classe Rails Model est chargée et passe dans la portée de la classe en tant que variable k
.
Conclusion
Dans ce didacticiel de refactorisation Ruby on Rails, nous avons pris moins de 15 minutes et implémenté une solution et l'avons mise en place pour les tests utilisateurs, prête à être acceptée dans l'ensemble de fonctionnalités ou supprimée. À la fin du processus de refactorisation, nous avons un morceau de code qui établit un cadre pour la mise en œuvre d'éléments listables et activables sur plusieurs modèles qui passeraient même le processus de révision le plus strict.
Dans votre propre processus de refactorisation Rails, n'hésitez pas à sauter quelques étapes dans le pipeline si vous êtes sûr de le faire (par exemple, passez de la vue au contrôleur ou du contrôleur au modèle). Gardez simplement à l'esprit que le flux de code va de la vue au modèle.
N'ayez pas peur d'avoir l'air stupide. Ce qui sépare les langages modernes des anciennes applications de rendu de modèles CGI, ce n'est pas que nous faisons tout de la bonne manière à chaque fois, c'est que nous prenons le temps de refactoriser, de réutiliser et de partager nos efforts.