Allégez vos modèles en fractionnant la logique, 4 alternatives aux Concerns Rails
Les modèles MVC deviennent très souvent un bazar hétéroclite de méthodes et l’interface publique de ces classes vire vite au chaos désespérant. Tout cela est encouragé par le principe de fat models, skinny controllers qui signifie malheureusement pour beaucoup « ne faites pas de l’orienté objet, vous avez assez de classes avec tous ces modèles qui s’entassent. »
Nous étudierons dans cet article quatre alternatives différentes et complémentaires aux concerns. À utiliser selon le cas.
Le problème se ressent le plus dans les modèles centraux d’une application, comme la classe User pour un réseau social, Message pour un board ou Invoice pour une application de facturation. J’ai déjà pu énumérer 290 méthodes dans une seule classe, dont une quantité de one-liners préfixés. Sans compter les accessors par centaines de la table SGBD associée !
L'équipe de développement de Rails recommande l’utilisation de concerns pour isoler et réutiliser des comportements. J’aime bien l’approche et je l’utilise, mais je lui reproche principalement deux faiblesses :
N’encourager l’isolation de logique que pour les comportements communs à plusieurs modèles. Alors que le problème des fat models vient souvent des sous-comportements spécifiques au même modèle.
Injecter des méthodes dans l’interface publique des modèles, qui viennent encombrer toutes les instances créées du modèle. Ce n'est donc qu'une forme d'organisation de votre code en fichiers. Une organisation sympathique pour le développeur, mais sans impact sur l'organisation et l'exécution réelle du code.
Première approche : utiliser de simples nouvelles classes Ruby !
Si vous avez un calcul ou une logique ponctuelle qui s'étend sur plus d'une méthode. Ou pire encore, que vous restreigniez à une seule méthode anormalement longue ou illisible afin de « ne pas polluer » votre modèle, pensez à l'isoler dans une classe Ruby ordinaire.
Exemple : il vous faut détecter les éventuelles particules nobiliaires des noms de famille des comptes utilisateurs (comme Jean de La Fontaine) :
Avant ; nous avons deux constantes et trois méthodes autour du lastname. La présence d'un préfixe (ici nobiliary_) est un symptôme intéressant pour détecter les comportements à isoler dans une même classe.
CODE: user.rb
Après ; nous nous sommes débarrassés des préfixes et le tout est concentré dans une classe homogène, facile à tester et à réutiliser.
CODE: user.rb CODE: nobiliary.rb
Si votre logique peut être utilisée par d'autres modèles, placez-là dans le dossier /lib. Si elle est spécifique au modèle comme notre exemple, n'hésitez pas à la placer dans le dossier /app/models/{nom-du-modèle}/. Elle sera "namespacée" avec le nom du modèle et se fera ainsi plus discrète !
Deuxième approche : augmenter le modèle (Facet)
Dans le premier exemple, le couplage entre le modèle et la logique isolée était très faible, seul le nom de famille était en jeux. Si votre modèle intègre par contre une logique qui dépend de plusieurs paramètres (typiquement plus de 4), cette deuxième approche sera plus adaptée.
Prenons pour exemple la fonctionnalité de gamification (ludification) du compte utilisateur. Son code ne sera utilisé que de manière marginale, voire jamais si la gamification est désactivée dans les paramètres de votre application. Pourquoi trainer partout cette logique alors que nous pouvons n'y faire appel que quand on en a besoin ?
Avant
Il s'agit de n’injecter dans l’interface du modèle qu’une seule méthode (gamified), sorte de porte d’entrée vers une version augmentée de notre modèle qui dispose de la logique inhérente au sous-comportement de gamification.
Cette méthode renvoie à chaque appel une instance d’une sous classe qui contient ses propres méthodes et accède allègrement à toutes les méthodes de la classe mère, y compris ses méthodes privées. Cela est rendu possible par l'utilisation de SimpleDelegator comme superclass de notre modèle.
Après :
Nous allons maintenant faciliter la création de ce genre de modules avec la méthode de module ModelOnDiet::Facet :
CODE: gamified.rb CODE: model_on_diet.rb
La méthode Facet (avec une majuscule, car c'est une méthode de module, comme Integer() ou Array(), voir ligne 18) crée dynamiquement un module, utilise le bloc qui lui est passé comme définition de la classe qui sera créée dynamiquement, et qui sera instanciée et "mémoisée" dans une méthode créée dynamiquement. Il ne nous restera donc qu'à inclure ce module dans notre modèle principal pour utiliser.
L'un des nombreux avantages de cette technique est qu'elle permet de faire dépendre plusieurs facets entre eux sans le moindre souci d'injection de dépendances. À partir du code de gamified, nous pouvons par exemple accéder au facet `logged` par le simple appel du nom de la méthode éponyme.
Troisième approche : nuancer le modèle (Context)
La deuxième approche peut servir dans énormément de cas, mais nous l'avons limité aux méthodes d'instances. Si votre modèle a besoin de définir —pour des utilisations occasionnelles— méthodes de classe, callbacks, validations ou scopes, optez cette troisième approche.
Il s'agit cette fois de définir un modèle qui héritera de tout ce dont votre modèle principal dispose tout en lui ajoutant les méthodes et les appels de classe nécessaires.
Prenons le cas de l'inscription des utilisateurs. Nous avons besoin d'y valider le login et d'envoyer un email avec mot de passe généré aléatoirement.
CODE : user.rb
L'inscription, aussi fondamentale soit-elle pour une application, demeure un cas spécial d'utilisation du modèle User. La logique qui entre en jeux lors de l'inscription est utilisée une fois et ne sera plus utilisée.
Nous définissons alors User::AtSignUp où nous déplacerons cette logique très contextuelle.
CODE : at_sign_up.rb
Seulement, Rails générera des routes erronées et fera des inflections inexistantes. Il faut y définir model_name mais optons pour une solution plus intelligente, que nous pourrons réutiliser plus facilement.
CODE: model_on_diet.rb CODE : at_sign_up.rb
Nous effectuons un héritage indirect via ModelsOnDiet::Context qui définie une classe transitoire où sera définie et d'où sera héritée la bonne méthode self.model_name.
Quatrième approche : maquiller ses instances avec .extend et after_find
Dans un autre article, je présente une solution qui permet de changer l'état de l'instance d'un modèle en injectant à sa classe singleton des modules dont les méthodes ne seront accessibles qu'à cette instance, sans les autres du même modèle.
CODE: user.rb
Enfin, utiliser des concerns sans concerns
Par la présence des dossiers models/concerns et controllers/concerns, Rails recommande ActiveSupport::Concern pour les modules utilisables par plusieurs modèles. Mais n'hésitez pas à l'utiliser pour tout sorte de modules. ActiveSupport::Concern offre plusieurs avantages, dont l'abstraction de l'intégration des méthodes de classe, et l'évaluation de classe (via included), ainsi que la la simplification de l'injection de dépendances.
Conclusion
Nous avons passé en revue quatre approches différentes pour mettre ses gros modèles au régime ! Ces approches et d'autres que nous n'avons pas évoqués peuvent être utilisés dans un contexte de refactoring, mais le développeur consciencieux doit avoir le réflexe de s'interroger en amont sur l'emplacement idéal de ses ajouts de code.









