Simplifie ta logique métier avec des services objects sans état (stateless)

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

Quand on parle de service objects, les débats sont souvent animés : quelle est la meilleure manière de les concevoir ? Méthodes d'instance ou de classe ? Singleton ou non ? Chacun a son avis bien tranché. Mais avant de se lancer dans des discussions plus poussées, restons simples : un service object, c'est un bout de code dédié à une tâche bien précise, censé rester indépendant des autres parties de ton application, et surtout stateless. Pas d'attachement émotionnel ici : ce petit objet doit rester neutre, prêt à servir quand on en a besoin, où que ce soit.

Avec cette idée en tête, la question se pose : comment le structurer pour qu'il soit le plus efficace possible ? Pendant longtemps, j'ai utilisé des méthodes d'instance pour mes services. Ça fonctionnait très bien, mais je me suis rapidement retrouvé à écrire des lignes de code un peu redondantes, parfois inutiles. Et puis un jour, je suis passé aux méthodes de classe, et là, tout a changé. Plus concis, plus direct. Alors, qu'est-ce qui marche le mieux ? Méthode d'instance ou de classe ? Spoiler : on va aller droit au but.

1. Méthodes d'instance vs méthodes de classe

Avant de plonger dans les détails du singleton ou d'autres subtilités de conception, concentrons-nous sur un premier choix simple mais structurant : méthode d'instance ou méthode de classe ?

Méthodes d'instance : L'idée est simple. On crée une instance du service, et on appelle la méthode voulue sur cette instance. Cela fonctionne bien si ton service doit garder un état temporaire ou si tu souhaites que chaque appel à un service soit indépendant. Mais soyons honnêtes : pour un service stateless, cela devient vite lourd et inutile. Chaque fois que tu veux utiliser ton service, tu dois instancier une nouvelle classe et appeler ta méthode dessus.

class LedService
  def switch_to(color)
    Rails.logger.debug("💡 Switching LED color to #{color}")
    system("python led_strip/#{color}_led_on.py")
  end
end

# Utilisation :
service = LedService.new
service.switch_to('red')

Jusqu'ici, tout va bien, mais pourquoi multiplier les instances quand ton service n'a pas besoin de stocker de données ? C’est là que les méthodes de classe deviennent intéressantes.

Méthodes de classe : Au lieu d’instancier une classe à chaque fois, tu fais simplement appel à une méthode de classe. Plus besoin de s'embêter avec des new, self et compagnie. Ton code est plus concis, plus direct, et tu n'as plus à te préoccuper de l'état de tes instances. Après tout, si ton service est censé rester stateless, pourquoi perdre du temps à créer des objets ?

class LedService
  def self.switch_to(color)
    Rails.logger.debug("💡 Switching LED color to #{color}")
    system("python led_strip/#{color}_led_on.py")
  end
end

# Utilisation :
LedService.switch_to('red')

La méthode de classe simplifie grandement l’appel de ton service. En une ligne, tu fais le boulot. Mais ce n’est pas tout. En plus d’alléger ton code, ça te donne une base solide pour pousser encore plus loin : pourquoi ne pas aller vers un singleton ? Et c’est là qu’on va monter d’un cran.

2. La beauté du singleton

Maintenant qu'on a vu comment les méthodes de classe permettent de simplifier la gestion des services stateless, parlons du niveau supérieur : le singleton. Si tu te demandes pourquoi s'embêter à passer à cette nouvelle approche après avoir adopté les méthodes de classe, laisse-moi t'expliquer.

Le singleton est un design pattern qui garantit qu'une seule et unique instance d'une classe est créée. Ça veut dire qu'à chaque fois que tu appelles ton service, c'est toujours la même instance qui est utilisée. C’est une manière encore plus propre et contrôlée de gérer un objet stateless, tout en encapsulant parfaitement son comportement.

Et pour cause, si un service n’a pas d’état propre (ce qui est souvent le cas), il n’y a aucune raison de créer plusieurs instances de cet objet. Tu veux une seule et même "boîte à outils" à utiliser partout dans ton application ? Le singleton est exactement ce qu'il te faut.

Pourquoi choisir le singleton ?

Tu pourrais te demander : "Mais pourquoi ne pas continuer avec des méthodes de classe ? Ça marche déjà bien, non ?". Oui, ça fonctionne. Mais en fait, le singleton te permet de centraliser encore mieux ton service. Un service object singleton offre plusieurs avantages pratiques :

  1. Contrôle total sur l'instanciation : Tu n'as plus à te soucier de créer accidentellement plusieurs instances d'un service. Tu sais qu'une seule instance existera à tout moment.
  2. Encapsulation des comportements : Il devient plus facile de regrouper les comportements associés à ce service dans un seul objet, sans polluer l'espace global avec des méthodes de classe.
  3. Lisibilité et testabilité : Puisque le singleton repose sur une unique instance, tu as un point d’entrée centralisé. Cela peut aussi rendre ton service plus facile à tester, car tu sais exactement comment ton service est instancié et utilisé.

Un exemple avec un singleton

Prenons un exemple concret pour voir comment on peut appliquer ce pattern dans une application Rails. Imagine qu'on veuille gérer des LED sur un device via un service object (d'ailleurs, si tu utilises des engines Rails pour organiser ton app, jette un œil à cet [article précédent sur les engines] (<placeholder>)).

class Hardware::LedService
  include Singleton

  class << self
    delegate :switch_to, to: :instance
  end

  def switch_to(color)
    Rails.logger.debug("💡 Switching LED color to #{color}")
    system("python #{Rails.configuration.panda[:panda_os_paths][:root]}/led_strip/#{color}_led_on.py")
  end
end

Voilà notre service object singleton. Grâce au module Singleton de Ruby, nous garantissons qu’il n’y aura qu’une seule instance de LedService. Ensuite, pour l’utiliser, on appelle simplement la méthode switch_to via la classe elle-même, sans avoir à instancier quoi que ce soit.

Hardware::LedService.switch_to('blue')

Pourquoi c'est génial ?

  • Clarté et simplicité : En un coup d'œil, tu sais que ton service n’est pas instancié à chaque fois. Il reste stateless, et c'est sans ambiguïté pour tout développeur qui relit ton code.
  • Réutilisabilité : Le singleton centralise ton service de manière à ce qu’il soit accessible partout dans ton application, toujours avec la même instance.
  • Testabilité : Comme tu sais toujours que tu n'as qu'une seule instance en circulation, tes tests peuvent être plus directs et faciles à gérer.

Conclusion

Les services objects sont un pilier incontournable dans la structuration d’une application Rails, et comprendre les différentes manières de les concevoir est un vrai game changer. Qu'il s'agisse de méthodes de classe ou d'un singleton, l'important est de choisir la solution qui offre le plus de simplicité et de lisibilité pour ton code, tout en restant fidèle à l’idée maîtresse du stateless. En optant pour un singleton, tu assures une gestion claire et précise de tes services, réduisant les risques d'erreur et augmentant la réutilisabilité.

Alors, la prochaine fois que tu as une logique métier à implémenter, pense à ces outils. Le singleton, par exemple, peut faire des merveilles dans bien des situations. Et si tu n'as pas encore franchi le cap, essaie-le sur ton prochain projet, tu ne reviendras probablement plus en arrière.

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