Rails t’a menti : pourquoi tout mettre dans un modèle est une (très) mauvaise idée

Arnaud Lenglet
|
Ruby on Rails
|
23/9/2024

Si je devais résumer le développement logiciel, je dirais qu'il repose sur deux questions : "Comment je nomme ça ?" et "Où je mets ça ?". Deux questions qui, en apparence, semblent innocentes. Sauf qu'en réalité, elles peuvent te plonger dans des heures de doute existentiel. Et là, Ruby on Rails n’est pas toujours ton meilleur allié.

Rails te dit, un peu naïvement : "Si c'est pour l'utilisateur, c'est une vue. Si c'est pour gérer les requêtes ou les réponses, c'est un contrôleur. Et pour tout le reste ? Bah, balance ça dans un modèle, tu verras bien !" Sauf qu'à force de tout balancer dans des modèles et des contrôleurs, tu te retrouves avec des monstres obèses qui font des heures supplémentaires, prennent des vacances quand ils veulent, et génèrent des bugs en série.

Aujourd'hui, on va revenir aux bases du MVC pour voir pourquoi cette organisation fait sens sur le papier, mais a besoin d'un bon coup de frais dans la vraie vie. Tu verras qu'avec un peu d'ordre, on peut éviter de transformer nos applications en labyrinthes où personne ne veut s'aventurer.

1. Redéfinissons les rôles des briques du MVC

Ah, le Single Responsibility Principle (SRP) ou Principe de Responsabilité Unique. Ce petit concept qui te dit qu'une classe ne devrait faire qu'une seule chose, et la faire bien. Plus facile à dire qu'à faire, surtout quand Rails te donne carte blanche pour empiler joyeusement des responsabilités.

Prenons les trois piliers du MVC et voyons comment ce principe devrait, en théorie, s'appliquer :

  • Les modèles : Ils sont censés ne gérer que l'accès aux données. C’est tout. Pas de logique métier lourde, pas de validation excessive. Mais dans la vraie vie, combien de fois ai-je vu des modèles qui ressemblent plus à des centres de contrôle d'aéroport ? Ils gèrent tout, de la validation des données au traitement de la logique métier. Et là, bonjour la maintenance.
  • Les contrôleurs : Leur rôle ? Recevoir les requêtes, les transmettre là où il faut, et renvoyer une réponse. Pas plus. Mais, soyons honnêtes, combien de contrôleurs ont vu leur mission s'étendre à tout gérer, y compris des calculs complexes et de la logique business ? Au point que tu te demandes s'ils ne pourraient pas aussi faire le café pendant qu'ils y sont.
  • Les vues : Là, normalement, on est safe. Une vue est faite pour afficher les données, point barre. Mais même là, on a souvent tendance à y glisser des bouts de logique qui n'ont rien à faire ici. Comme si on ne pouvait pas s’empêcher de tout mélanger, histoire de pimenter un peu la vie des futurs mainteneurs.

Bref, dans un monde idéal, chaque composant du MVC fait son job, et seulement son job. Mais entre les responsabilités floues et les mauvaises habitudes, on finit vite avec un code qui ressemble à un plat de spaghetti. Et c’est là que les ennuis commencent.

2. Une logique métier = Un "service object"

Alors, les services. Si tu n’en as jamais entendu parler, prépare-toi à un changement de paradigme. Quand Rails te dit "tout ce qui ne rentre pas dans le modèle ou le contrôleur, mets-le dans un modèle", ce qu'il sous-entend, c'est que tu vas finir par faire de la plomberie dans un placard à balais. Et c’est là qu'interviennent les service objects.

Un service object, c’est un objet qui fait une seule chose et qui la fait bien (oui, encore le SRP). Contrairement à tes modèles ou contrôleurs surchargés qui gèrent la moitié de la galaxie, un service se concentre uniquement sur une tâche spécifique de ta logique métier. Imagine-le comme un super-héros discret, spécialisé dans une mission ultra-précise : calculer des températures, traiter des paiements, ou synchroniser des utilisateurs avec une API externe.

L'idée, c'est que tout ce qui n'a rien à faire dans tes modèles ou contrôleurs – c'est-à-dire la vraie logique métier – doit être isolé dans des services. Pourquoi ? Parce que c'est plus propre, plus facile à tester, et que ça t’évitera de pleurer des larmes de sang la prochaine fois que tu devras modifier ton code.

Exemple : un CoolingService

Prenons un exemple simple : la gestion thermique d'un système (oui, c’est très glamour). Plutôt que de laisser tes contrôleurs ou modèles faire des acrobaties pour ajuster des températures, tu crées un CoolingService dédié, qui gère cette logique.

class CoolingService
  def initialize(temperature)
    @temperature = temperature
  end

  def adjust
    if @temperature > 70
      activate_cooling_system
    else
      deactivate_cooling_system
    end
  end

  private

  def activate_cooling_system
    # code qui active le système de refroidissement
  end

  def deactivate_cooling_system
    # code qui désactive le système de refroidissement
  end
end

Maintenant, tout ce qui concerne la gestion des températures est contenu dans un service bien propre. Tes contrôleurs peuvent continuer à gérer les requêtes, tes modèles à jouer avec les données, et ton service fait son boulot, sans bavure.

Pourquoi c'est génial ? Parce que tu peux tester ce service de manière isolée, tu sais exactement où se trouve ta logique métier, et, surtout, tu évites de gonfler tes modèles et contrôleurs avec du code qui n’a rien à y faire.

3. La beauté des PORO

Avant de poursuivre, mettons une chose au clair : tes service objects ne sont pas une invention magique venue des contrées lointaines du développement logiciel. Non, ce sont simplement des PORO, ou Plain Old Ruby Objects. Oui, aussi simple que ça. Pas de méta-programmation tordue, pas de magie Rails, juste des classes Ruby pures et dures.

Et là, tu te demandes sûrement : "Pourquoi je devrais m'embêter avec ça ?" Eh bien, laisse-moi te dire que les PORO, c’est l’arme secrète que beaucoup sous-estiment. D’abord, ils te permettent de sortir de la bulle Rails, et ça, c’est un game-changer. Tu n’es plus prisonnier des conventions du framework. Tu as un objet Ruby indépendant qui ne dépend d’aucune gem ni d'aucun composant Rails spécifique. En gros, tu crées du code agnostique, que tu peux réutiliser partout.

1. Testabilité au top

Puisque tes services ne sont que des objets Ruby tout simples, ils deviennent beaucoup plus faciles à tester. Oublie les trucs compliqués à base de mocks ou de stubs pour simuler la moitié de ton application Rails. Avec un PORO, c'est simple : tu l’instancies, tu appelles la méthode que tu veux tester, et boum, tu obtiens le résultat. Pas besoin de charger tout l’écosystème Rails derrière juste pour voir si ta logique métier marche.

Tu veux tester un service ? Un petit CoolingService.new(75).adjust, et tu peux vérifier si le système de refroidissement a bien été activé. Ça rend les tests plus rapides, plus fiables, et tu ne perds pas trois heures à configurer des mocks pour des trucs qui devraient être simples.

2. Réutilisation à gogo

Autre avantage des PORO : leur réutilisation. Puisque ces objets ne dépendent pas de Rails, tu peux facilement les sortir de ton application web. Imagine que ton service de gestion thermique soit utilisé dans une application mobile, un script d'automatisation ou même un système embarqué. Grâce à la simplicité des PORO, tu n’as aucune dépendance lourde à transporter.

Rails reste une excellente brique pour assembler une application web, mais il ne devrait pas devenir une prison dorée. En séparant ta logique métier via des PORO, tu rends ton code portable et adaptable à différents contextes, sans l'encombrement des contraintes du framework.

3. Adieu les surcharges de modèles

Si tu en as marre de voir tes modèles devenir des monstres Frankenstein remplis de méthodes qu’ils ne devraient pas avoir, les PORO sont ta bouée de sauvetage. Au lieu d’enfermer toute ta logique métier dans un modèle sous prétexte que Rails t’y encourage, tu la places dans un service distinct. Résultat : des modèles plus légers, plus simples à lire, et qui se concentrent uniquement sur leur boulot.

En fin de compte, tes service objects ne sont que des classes Ruby classiques, et c’est exactement ça qui les rend si puissants. Simples à écrire, simples à tester, simples à réutiliser. La simplicité, c’est parfois le meilleur atout pour éviter de transformer ton code en un casse-tête chinois.

Conclusion

En fin de compte, Rails, avec son MVC tout mignon, peut vite te pousser à créer des monstres de complexité sans même t’en rendre compte. Le fameux "balance ça dans un modèle" n’est pas toujours la meilleure solution, et quand tu te retrouves avec des contrôleurs obèses et des modèles sous stéroïdes, c’est que quelque chose a mal tourné.

C’est là que les service objects et les PORO entrent en jeu, comme des héros discrets de ta codebase. Ils ne font pas de miracles, mais ils te permettent de garder une application propre, organisée et surtout, maintenable. Ils te facilitent la vie pour les tests, la réutilisation et, soyons honnêtes, ils t’évitent bien des migraines.

Alors la prochaine fois que tu es tenté de jeter un peu de logique métier dans ton modèle ou ton contrôleur, prends une seconde pour respirer et pense à un service object. Rails n'est pas une prison, et avec un peu de méthode, tu peux écrire du code qui fait vraiment ce qu'il est censé faire : marcher, sans te rendre fou.

Arnaud Lenglet
Développeur. Concepteur. Créateur.
Je partage mes expériences autour de la création de produit.

Sur la même thématique

Une idée ? Un projet ?
Travaillons ensemble