Clean architecture: Refac d’un component pour utiliser un MVP (modelView/presenter)

On est donc reparti pour faire notre plus beau projet « todo list » en mode « clean architecture ». Comme vous le savez, les avancées seront mise sur mon github et sera directement visible sur https://naughty-banach-1ccbd2.netlify.app.

Au programme aujourd’hui, passer d’un component « simple » à un component qui utilise un modelView/Presenter. Je dis « simple », mais il est déjà pas si mal! On a des useState, des useMemo, des useEffect, des appels asynchrones, etc…

ps: Prenez votre temps pour lire les étapes de refac 😇

C’est quoi le but dans l’histoire?

En fait, l’idée est simple. Techniquement, il faudrait:

  • Donner à la vue un viewModel qui contient toutes les informations prêtes à être affichées. Si vous avez des dates, ce sont déjà des string bien formatées et pas des Date. Si vous avez des champs à griser, il y a un booléen du genre isButtonDisabled. Les prix ont déjà le bon format. Bref vous voyez l’idée
  • Il faut aussi donner à la vue un Presenter qui contient les méthodes que celle-ci appellera quand on veut effectuer des actions. Par exemple: loadTodos(), toggleTodo(), addTodo(), etc… Le Presenter se chargera de modifier le viewModel et d’en avertir la vue.

Le but étant de simplifier au maximum la vue, car son seul but sera d’afficher les infos du viewModel au bon endroit (comme un placeholder) et d’appeler les méthodes du Presenter au bon moment (via un onClick=>presenter.addTodo()).

Et concrètement?

On va partir en mode « refactoring » pour arriver à une solution vivable.

1. Création du Presenter et du HOC

Commençons par faire notre Presenter qui s’occupera de donner une liste statique à notre vue. Les injections de Presenter et de viewModel se feront via un HOC (Higher-Order Component).

Code complet dispo : https://github.com/Nikoms/clean-todo/pull/6/commits/26e99132790e5e69caa636c26b0e2995e36db4c1?diff=split&w=1

2. Interagir avec notre liste

Avoir une liste, c’est bien. Pouvoir interagir avec celle-ci, c’est mieux. Dans ce commit, nous allons refac deux choses:

  • Les fonctions addEmptyTodo et toggleDone vont appeler le Presenter.
  • La modification du viewModel devra re-renderer la vue avec les nouvelles données. Le Presenter aura désormais un « listener  » (une callback) qu’il appellera à chaque modification. Enfin, le HOC va créer un useState qui, quand il sera set, rafraichira le component. Les plus attentifs verront que le listener/callback du Presenter n’est autre que le setState 😎

Code complet dispo: https://github.com/Nikoms/clean-todo/pull/6/commits/98fb4046dc5ab5d819b2df34c232c02c522453f4

3. Rendre notre Presenter plus explicite

Dans l’exemple précédent, la vue appelle un presenter.setTodoList() à chaque opération de la liste. Le but étant de simplifier la vue, on va créer des méthodes spécifiques dans le Presenter: addEmptyTodo() et toggleDone(index)

Code complet dispo: https://github.com/Nikoms/clean-todo/pull/6/commits/4c9d426e01af0a912c2a9254c4d931d62ec47606

4. Et nos compteurs de ongoing/done ?

Il reste de la logique dans notre vue. Pour le moment, on a ceci:

Ce qui n’est pas bon, car toutes les infos affichées doivent venir du viewModel. C’est à notre Presenter de faire le boulot. Heureusement (il y a findus), tout est déjà centralisé dans la méthode Presenter._setTodoList(list). Il suffit de faire les filter+length à ce moment là:

Code dispo ici: https://github.com/Nikoms/clean-todo/pull/6/commits/396c1061933f9893d87322b525aeb7a3c360c91e

5. Async todos

Pour les besoins du refac, on avait initialiser notre liste de todos en dur dans le code (🤢). Nous allons repasser en mode asynchrone et demander à la vue d’appeler Presenter.loadTodos() quand elle est prête (onMount). On pourrait aussi charger la liste dans le constructeur du Presenter, mais je préfère laisser les constructeurs les plus « pures » possible 😁. Note importante: Nous injectons la méthode (getTodos) qui sera utilisée pour récupérer la liste car le Presenter ne veut pas savoir d’où ça vient (Dependency inversion): Cela pourrait être une méthode qui appelle une API REST ou GraphQL, un mock, etc… « Not our business ».

Code dispo ici: https://github.com/Nikoms/clean-todo/pull/6/commits/41ca449d64ebbb52427ab0424318b9e8202d0218

Et pour les tests

Hé ben pour les tests c’est beaucoup plus simple! En effet, techniquement, vous ne devriez plus avoir besoin de tests se basant sur votre component (ou si peu). Pour info, dans ce commit, vous avez une comparaison des tests écrits en utilisant le component et les tests écrits en utilisant directement le Presenter. Résultat des courses:

  • Les tests sont bien plus rapides: 300ms contre 6ms maintenant
  • Les tests sont robustes car ne se n’utilisent pas la vue (jsx, dom, …)
  • Le code métier n’es pas mélangé à la vue
  • On utilise du javascript pure. C’est tout une partie de code qui ne devra pas changer à chaque mise à jour d’une dépendance (je suppose que vous aussi, vous en avez 2-3…)

Vous pouvez toujours avoir un ou deux petits tests qui couvre le component, mais ils devraient survoler l’ensemble… Pour le coup, je testerais l’intégration manuellement, car écrire les tests équivaudrait à savoir si on a bien copier/coller la variable au bon endroit (tel un placeholder). Des exceptions sont toujours possibles, mais pour moi, le ratio entre le temps passer à écrire/maintenir le test et le gain apporté, n’est pas assez interessant. Je pense que les tests E2E couvriraient déjà suffisamment le code.

C’est tout?

Oui c’est tout 😊 Bien sûr, j’ai encore effectué 2-3 refac sur le code comme:

Si vous voulez, la merge request est disponible sur Github. Elle contient tous les commits atomiques avec, comme d’habitude, chaque refac effectué est commenté, suivi d’un commit « Remove comment for the next refactoring » pour vous simplifier la lecture du refac suivant 🤓

Encore des questions?

Postez les en commentaires… Sinon j’anticipe déjà quelques-unes :

Tu nous fais des HOC avec des classes plutôt que des fonctions… Tu serais pas un peu has-been? Va faire de l’angular! Ils aiment les classes là bas 🔥

Monsieur M’palèclass

Je dois être has-been oui 🙂. Mais je fais partie de ceux qui pensent qu’on peut utiliser les 2 dans un projets. Par contre Angular, Vue ou React, pour moi c’est le même combat! Après, vous pouvez aller lire l’article sur intitulé Double Your React Coding Speed With This Simple Trick. Il commence par faire un hook où il exporte des fonctions/variables. J’imagine qu’il y a moyen d’écrire tout ce que j’ai décrit ici en mode « composition ». Si ça se trouve, je le ferai pour le challenge. Mais entre temps, je trouve le HOC plus simple pour les tests qu’un hook qu’il faut mocker. En effet, je n’ai qu’à instancier un Presenter, éventuellement appeler quelques méthodes pour me mettre dans une situation particulière (ex: presenter.openModal()), et ensuite le passer à ma vue avec son viewModel. « Et voilà », comme disent les anglais..

Ok, mais ça n’ira pas dans mon projet, j’utilise redux et pleins de hooks, c’est impossible à intégrer dans ton bazar…

Monsieur Nicroipa

Si si! On va en faire un sujet très prochainement (je suis en plein dedans au boulot…). En fait, il faut voir le truc comme un simple HOC qui ajoute 2 props à ton component… La prochaine fois que tu rajoutes un useState, essaye cette technique pour voir.

Et donc tu n’utilises pas la puissance de tout le framework? Les hooks comme useMemo, useEffect ou des trucs plus custom comme useFetch ou useAsync, etc…

Monsieur daypendanse

Effectivement, je ne les utilise que quand il faut et certainement pas dans le Presenter. Mon but est d’avoir un code qui tient dans le temps, pas d’utiliser toutes les technos et les libs en vogue. J’ai tellement eu de gros projets Legacy (des projets avec 15/20 ans de devs) et c’est tellement la galère de maintenir des dépendances que merci mais non merci. Chaque version major est un vrai challenge 🤓! De plus, je me méfie des frameworks/libs qui prennent des décisions pour nous (React qui passe des classes aux fonctions par exemple) sur les paradigmes, nos structures de dossier ou nos nommages de fichier. Ce n’est pas pour ça que je ne les utilise pas bien sûr, mais leur impact sur notre code doit être limité. Intégrer des hooks qui font des calls API dans la vue, je trouve que c’est se tirer une balle dans le pied et franchement pas plus « rapide » à programmer.

On fait ça pour tous les components alors?

Monsieur Bon’Kestillon

Non. Pour le moment, je me concentre sur les gros component qui représente une page. Les « sous components » ont dest props hyper spécifiques. Je ne ferais pas de MVP pour un champ texte par exemple.

La suite?

Si vous voulez me demander d’ajouter une fonctionnalité dans le code ou me lancer un challenge, vous pouvez ouvrir une issue. Au programme:

  • Pouvoir éditer un todo (Mhhh, les formulaires, c’est jamais simple…)
  • Enregistrer ma liste quelque part. On aura probablement un backend simulé qui enregistrera tout en localstorage.

Merci d’avoir eu le courage de tout lire 😘

Rejoindre la conversation

2 commentaires

  1. Comme Cédric, j’avoue qu’il est ultra-stylé cet article !

    Découpler la logique du framework c’est vraiment la base, je l’ai bien compris avec le DDD et la clean architecture.

    Pourtant tout nous incite à faire du code hyper couplé aux frameworks… Merci Nico pour ta sagesse 🙂

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *