Mettez vos contrôleurs et vos modèles au régime
Ou comment découpler le code métier et le code du framework grâce aux Interactors.
Votre projet grossit. Petit à petit les contrôleur et les modèles se chargent de réaliser des opérations métier. Ils s’encrassent. Le code métier se mélange au code du framework.
C’est un phénomène appelé “fat controllers / fat models” et c’est un indicateur clair que le projet va rapidement devenir difficile à maintenir et à faire évoluer.
Pourtant, on sait que :
- le contrôleur devrait uniquement traiter les requêtes du client
- le modèle devrait uniquement se charger de la persistance des données
On trouve un code similaire dans de nombreux projets. Dans l’exemple ci-dessus, l’action create
du contrôleur accumule les responsabilités :
- instancier une Company
- faire appel à un service tiers
- enrichir la Company
- gérer les erreurs en cas de défaillance du service tiers ou de la validation des données
- notifier un manager par email en cas de succès
- répondre à l’utilisateur
Aouch.
Cette problématique, qu’on peut résumer par “je-vois-bien-le-problème-mais-où-est-ce-que-je-dois-mettre-mon-code-alors ?”, est adressée par plusieurs design patterns. Lorsqu’on parle d’opérations métier (et une opération métier est généralement constituée d’une suite d’instructions plus ou moins complexes), le pattern le plus simple et le plus efficace est probablement l’Interactor. Vous le connaissez peut être déjà sous le nom de Business Objects ou Service Objects.
Ce pattern propose d’isoler une opération métier dans une classe Ruby pure, au nom équivoque (c’est-à-dire qui exprime bien l’opération réalisée). Son rôle est d’exécuter cette opération métier, et rien d’autre.
Ici par exemple, on peut créer un interactor nommé CreateCompany
, dont le rôle est donc de créer une société. Pour cela, elle va réaliser les étapes listées précédemment et gérer les cas aux limites.
Cela permet de simplifier le contrôleur, qui devient le client de cette classe :
Ouf, on respire déjà mieux.
On pourrait créer la classe CreateCompany
de toutes pièces. Il faudrait définir un cadre commun pour tous nos interactors, c’est-à-dire une API commune, de sorte que tous les interactors du projet s’utilisent et se comportent de la même manière.
Plutôt que de mettre les mains dans le cambouis, adoptons la gem interactor, car c’est exactement le service qu’elle rend.
Il n’y a qu’une notion à comprendre pour utiliser un interactor avec cette gem : le contexte. Le contexte est une plateforme d’échange entre l’intérieur et l’extérieur de l’interactor. Il est retourné par un appel à l’interactor :
context = CreateCompany.call(…)
C’est lui qui permet de savoir si l’exécution s’est bien déroulée :
context.success?
Au sein de l’interactor, pour indiquer une erreur et interrompre l’exécution, il suffit d’utiliser :
context.fail!
Enfin, il permet de récupérer les données produites par l’interactor, le cas échéant :
context.company
context.error
D’autre part, on voit bien que la méthode call
est le cœur de l’interactor. C’est à elle de dérouler les instructions qui constituent l’opération métier.
Toutefois elle peut être encadrée par des hooks :
before
naturellement appelé avant lecall
after
appelé après lecall
around
appelé autour ducall
Cette structure nous permet de construire un interactor extrêmement simple et lisible :
Lorsque vous définissez de la sorte vos opérations métier, de manière atomique et réutilisable :
- vous garantissez un meilleur respect du principe de responsabilité unique
- vous évitez la duplication de code
- vous rendez de la lisibilité aux modèles et aux contrôleurs
- vous facilitez les tests du code métier, indépendamment du framework
En clair, vous améliorez la maintenabilité du code.
Et si vous souhaitez aller plus loin, la gem interactor vous propose aussi un interactor d’un type particulier : l’Organizer. Comme son nom l’indique, il permet d’organiser des interactors de base, en créant un enchaînement :
class PlaceOrder
include Interactor::Organizer
organize CreateOrder, ChargeCard, SendThankYou
end
Chaque interactor bénéficie du contexte de celui qui le précède, et la suite est interrompue en cas de fail. De quoi réduire encore le risque de duplication de code.
Découvrez la vidéo de mise en œuvre de ce design pattern :
Apprenez Ruby on Rails FACILEMENT et RAPIDEMENT grâce à des ressources en français sur Dev on Rails. Suivez-vous :
Promotion spéciale pour notre formation “Apprendre Ruby on Rails RAPIDEMENT” sur Udemy à 9,99 € au lieu de 49,99 € (80% de réduction) : https://www.udemy.com/apprendre-ruby-on-rails-rapidement/?couponCode=M3DIUM
Avec cette formation, investissez UNE JOURNÉE de votre temps pour apprendre Ruby on Rails.
Pour s’ancrer dans le cadre d’un projet réel, on développe dans ce cours une vraie application web. Vous disposez du code source pour vous y référer en cas de besoin (publié sur GitHub, avec une branche par session du cours).
Avec cette expérience, vous serez prêt·e à développer votre propre application web et à passer très vite de l’idée au produit.