Sommaire
Cet article fait suite au précédent, sur le thème de la migration d’une API REST vers GraphQL.
Dans l’article précédent, nous avons survolé le plan d’action mis en place pour faire cette migration.
Nous allons voir dans ce billet la phase de préparation plus en détails. Tout d’abord, un petit rappel :
Travail préparatoire
- Répertorier les controllers & actions qui seront présent sur la version GraphQL de l’API.
- Extraire les actions des controllers et les transformer en objet (design pattern command).
- Optionnel (mais vivement conseillé !) : mettre à jour sa couverture de test.
Répertorier les controllers & actions qui seront présent sur la version GraphQL de l’API
Pour cette phase, vous seul êtes à même de répertorier quelles actions sont utilisées ou non par les consommateurs de votre API.
Pour la méthodologie, une passe exhaustive sur l’ensemble des controllers est nécessaire.
Vous pouvez également en profiter pour prendre des notes sur les travaux de refactors à prévoir, les tests unitaires à écrire... Ceci vous permettra de commencer à chiffrer plus précisément le temps à passer pour les futures étapes.
Extraire les actions des controllers et les transformer en objet
Je vous conseille tout d’abord de prendre connaissance du design pattern Command
(ou Service Object
)
Le but de cette étape est d’extraire chaque action de vos controllers en objet. En procédant de cette manière, plusieurs avantages se dévoilent :
- Vous pourrez identifier les dépendances de votre code ;
- Vous pourrez tester la logique du code plus facilement, indépendamment du contexte du controller ;
- Vous pourrez ré-utiliser votre code à plusieurs endroits ;
Sur l’exemple suivant, on va extraire la logique de l’action create dans le controller posts_controller.rb.
Avant refactor :
## app/api/v1/posts_controller.rbmodule Api module V1 class PostsController < ::Api::ApplicationController def create ## Logique métier ## ... ## ... if post_repository.save(post) redirect_to post_path(post) else render "new" end end def post_repository @post_repository ||= ::Repositories::Post.new end end endend
Après refactor :
## lib/command/post/create.rbmodule Command module Post class Create def self.exec({ attrs, callbacks, repositories }) new(callbacks, repositories).exec(attrs) end def exec(attrs) ## Logique métier ## ... ## ... if @repositories[:post].save(post) @callbacks[:success].call(post: post) else @callbacks[:failure].call(post: post) end end private def initialize(callbacks, repositories) @callbacks = callbacks @repositories = repositories end end endend
## app/api/v1/posts_controller.rbmodule Api module V1 class PostsController < ::Api::ApplicationController def create ::Command::Post::Create.exec( { attrs: params, callbacks: { success: ->(args) { @post = args[:post] redirect_to post_path(@post) }, failure: ->(args) { @post = args[:post] render "new" }, }, repositories: { post: ::Repositories::Post.new, } } ) end end endend
Voici quelques liens qui pourront vous êtes utile pour aller plus loin :
- https://www.codeproject.com/Articles/12263/The-Command-Pattern-and-MVC-Architecture
- https://blog.slava.dev/thin-controllers-fat-models-one-of-the-base-principles-of-mvc-in-ruby-on-rails-but-time-goes-by-a5a044124207
- https://www.honeybadger.io/blog/refactor-ruby-rails-service-object/
- https://www.cloudbees.com/blog/refactoring-legacy-rails-controllers
Mettre à jour sa couverture de test
Avant toute modification ou refactor, je vous conseille d’écrire & de mettre à jour vos tests. Ceci vous permettra de “tout casser” sereinement !
Je ne vais pas m’attarder sur la mise en place des tests unitaires, mais je vais vous présenter un exemple adapté aux changements.
Si vous utilisez rspec, l’exemple ci-dessous pourra vous servir de template afin de tester de vos Objets command.
require 'rails_helper'RSpec.describe ::Command::Post::Create do let (:callbacks) { { success: -> (*args) { :success }, failure: -> (*args) { :failure } } } before do post_repository = double('::Repositories::Post') allow(post_repository).to receive(:save).with(an_instance_of(::Post)).and_return(true) @repositories = { post: post_repository, } end let (:command_post_create) { ::Command::Post::Create } describe "create" do context "the record is created" do let (:attrs) { { title: "Test" } } it "calls success callback" do result = command_post_create.exec( attrs: attrs, callbacks: callbacks, repositories: @repositories ) expect(result).to eq :success end end endend
Voici quelques liens qui pourront vous êtes utile pour aller plus loin :
Conclusion
Après ce travail, vous devriez avoir séparé la logique métier des actions des controllers dans des objets spécifique. Ceux-ci, ont leurs dépendances bien identifiées, elles vont pouvoir être testés correctement.
Vous pouvez désormais utiliser ces objets n’importe où dans votre projet, ce qui évitera de dupliquer le code entre vos deux versions d’API.
Vous avez donc dès à présent une bonne valeur ajoutée en termes de clarté et de structuration !