Appliquer le modèle des cas d'utilisation avec Rails
Nicolas Nisoria
Un problème courant lorsque l'on travaille avec Rails est de décider où placer la logique de nos fonctionnalités.
La logique est souvent placée dans les contrôleurs, les modèles ou, si nous avons de la chance, dans un objet de service. Si nous disposons d'objets de service, pourquoi avons-nous besoin de cas d'utilisation ?
Suivez-moi dans cet article pour découvrir les avantages de ce modèle.
Cas d'utilisation
Définition
Un cas d'utilisation est une liste d'actions ou d'étapes définissant généralement les interactions entre un rôle et un système pour atteindre un objectif.
Il convient de mentionner que ce modèle est appliqué de différentes manières et qu'il a d'autres noms. On peut le trouver sous la forme Interacteurs, Opérateurs ou Commandesmais dans le Rubis la communauté avec laquelle nous restons Cas d'utilisation. Chaque mise en œuvre est différente mais a le même objectif : servir le cas d'utilisation du système par l'utilisateur.
Même si dans notre projet nous ne définissons pas les exigences à l'aide de Cas d'utilisationet UML, ce modèle reste utile pour structurer la logique d'entreprise d'une manière pratique.
Règles
Notre Cas d'utilisation doit être :
Agnostique par rapport au cadre de travail
Agnostique en matière de bases de données
Responsable d'une seule chose (définir les étapes pour atteindre l'objectif de l'utilisateur)
Avantages
Lisibilité : Facile à lire et à comprendre car les étapes sont clairement définies.
Découplage : Déplacer la logique des contrôleurs et des modèles et créer un nouveau niveau d'abstraction.
Visibilité : La base de code révèle les fonctionnalités disponibles dans le système.
En pratique
Prenons l'exemple d'un utilisateur qui souhaite acheter quelque chose dans notre système.
module UseCases
module Buyer
classe Achat
def initialize(buyer :, cart :)
@buyer = acheteur
@cart = panier
end
def call
return unless check_stock
return unless create_purchase
notify end
private
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart : cart)
end
def create_purchase
Services::CreatePurchase.call(buyer : buyer, cart : cart).call
end
def notify
Services::NotifyBuyer.call(buyer : buyer)
end
end
end
fin
Comme vous pouvez le voir dans cette code nous avons créé un nouveau Cas d'utilisation appelée Achat. Nous n'avons défini qu'une seule méthode publique appel. Dans la méthode d'appel, nous trouvons des étapes assez basiques pour effectuer un achat, et toutes les étapes sont définies comme des méthodes privées. Chaque étape appelle un objet de service, ce qui permet à notre méthode Cas d'utilisation ne fait que définir les étapes pour effectuer un achat et non la logique elle-même. Cela nous donne une image claire de ce qui peut être fait dans notre système (effectuer un achat) et des étapes pour y parvenir.
Nous sommes maintenant prêts à appeler notre premier Cas d'utilisation d'un contrôleur.
classe Contrôleur
def purchase
UseCases::Buyer::Purchase.new(
buyer : purchase_params[:buyer],
cart : purchase_params[:cart]
).call
...
fin
...
fin
De ce point de vue, le Cas d'utilisation ressemble beaucoup à un objet de service, mais son objectif est différent. Un objet de service accomplit une tâche de bas niveau et interagit avec différentes parties du système, comme la base de données, tandis que l'objet de service est un objet de service. Le cas d'utilisation crée une nouvelle abstraction de haut niveau et définit les étapes logiques.
Améliorations
Notre première Cas d'utilisation fonctionne mais pourrait être amélioré. Comment l'améliorer ? Utilisons l'outil sec gemmes. Dans ce cas, nous allons utiliser transaction à sec.
Commençons par définir notre classe de base.
classe UseCase
include Dry::Transaction
classe << self
def call(**args)
new.call(**args)
end
end
fin
Cela nous aidera à passer des attributs à la transaction du cas d'utilisation et à les utiliser. Nous sommes alors prêts à redéfinir notre cas d'utilisation d'achat.
module UseCases
module Buyer
classe Achat
def initialize(buyer :, cart :)
@buyer = acheteur
@cart = panier
end
def call
return unless check_stock
return unless create_purchase
notifier
fin
privé
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart : cart)
end
def create_purchase
Services::CreatePurchase.call(buyer : buyer, cart : cart).call
end
def notify
Services::NotifyBuyer.call(buyer : buyer)
end
end
end
end
Avec les nouveaux changements, nous pouvons voir clairement comment nos étapes sont définies et nous pouvons gérer le résultat de chaque étape avec Success() et Failure().
Nous sommes prêts à appeler notre nouveau cas d'utilisation dans le contrôleur et à préparer notre réponse en fonction du résultat final.
classe Contrôleur
def achat
UseCases::Buyer::Purchase.new.call(
buyer : purchase_params[:buyer],
cart : purchase_params[:cart]
) do |result|
result.success do
...
end
result.failure do
...
end
fin
...
fin
...
fin
Cet exemple pourrait être encore amélioré par des validations, mais il suffit de montrer la puissance de ce modèle.
Conclusions
Soyons honnêtes, le Modèle de cas d'utilisation est assez simple et ressemble beaucoup à un objet de service, mais ce niveau d'abstraction peut apporter un grand changement dans votre application.
Imaginez un nouveau développeur rejoignant le projet et ouvrant le dossier use_cases, il aura comme première impression une liste de toutes les fonctionnalités disponibles dans le système et après avoir ouvert un cas d'utilisation, il verra toutes les étapes nécessaires pour cette fonctionnalité sans avoir à aller au fond de la logique. Ce sentiment d'ordre et de contrôle est le principal avantage de ce modèle.
Mettez cela dans votre boîte à outils et peut-être en ferez-vous bon usage à l'avenir.