10) "Avoid Primitives" - Commands

Nous avons encore du travail quant aux entrants de notre système. Celui-ci ne prend en paramètre que des types primitifs avec des signatures de méthodes :

  • difficiles à faire évoluer

    • listes de Tuples...

  • pouvant être cryptiques et donc avec un fort impact cognitif

public Guid Handle((string nom, int nbGalinettes) terrainDeChasse, List<(string nom, int nbBalles)> chasseurs)
public void Handle(Guid id, string chasseur)
Step 10 : "Avoid Primitives" - Commands

Il existe un moyen d'éviter ce phénomène : les "objets". En l'occurence nous allons encapsuler ce qui est handlé par nos Use Cases dans des objets de type Command :

  • Prendre du temps pour comprendre ce qu'est le principe Avoid Primitive Types

  • Commencer par refactorer le code du Use Case DemarrerPartieDeChasse

Si nous avions une couche d'exposition au-dessus, nous devrions mapper les DTOs en entrée vers nos commandes afin de préserver l'encapsulation et pouvoir faire évoluer notre Domain sans impacter les couches supérieures.

DemarrerPartieDeChasse

  • On peut commencer par transformer les paramètres entrants en class

Transform parameters
  • Puis on configure l'extraction

Configure Transform Parameters
  • Voici l'impact de ce refactoring automatisé sur notre code :

    • Notre IDE est capable de faire ces changements lui-même

  • On peut maintenant renommer et bouger cette classe dans notre Domain

  • Nous pouvons continuer à travailler sur le record afin de lui donner plus de sens métier

    • Malheureusement, à l'heure où j'écris ces lignes mon IDE n'est pas en capacité de faire ce refactoring automatiquement

  • Nous allons donc adapter par nous-mêmes les appelants en nous laissant guider par les erreurs de compilation

    • On commence par "fixer" les tests

  • On adapte notre Builder

Refactoring du code de production

  • On se focalise maintenant sur le code de production

Failure
  • On utiliser le Strangler Pattern pour refactorer notre code

    • On génère un nouvel overload

    • 🔴 5 tests sont maintenant rouges : ils vont nous servir de driver

  • 🟢 On commence par utiliser l'ancienne méthode dans à partir de la nouvelle pour faire passer les tests

  • 🔵 On peut maintenant "copier / coller" le code de l'ancienne méthode dans la nouvelle

  • On regarde qui appelait l'ancienne méthode de création

Usages
  • On "plug" les appelants sur la nouvelle méthode

  • 🔵 On peut supprimer de manière totalement safe la méthode "étranglée"

Reproduire ces étapes pour les autres Commands

Après avoir extrait des Command pour chaque Use Case nous pouvons remarquer les patterns suivants :

  • 1 Use Case a pour définition : Command -> Result

  • Chaque Use Case implémente la même logique

    • Charge la PartieDeChasse à partir du repository

    • Vérifie que celle-ci existe

    • Exécute l'action sur le Domain

    • Save le nouvel état

1 seule exception à celà : DemarrerPartieDeChasse

Nous allons faire en sorte de factoriser cette logique grâce à notre nouveau typage d'entrée.

Factoriser le code des Use Cases

On commence par traduire le texte ci-dessus sous forme de "contrat".

  • On crée une nouvelle interface pour nos Use Cases

    • On crée également une interface pour contraindre qu'en entrée d'un Use Case seule 1 Command puisse être passée

  • On l'utilise pour ConsulterStatus

    • Le gain n'est pas flagrant...

    • On utilise uniquement les interfaces pour contraindre la méthode Handle

  • On continue avec un autre Use Case : PrendreLapéro

    • On tombe sur un problème

    • Le type de retour ici est void

  • On crée un type de retour pour ce besoin

  • On profite d'avoir 2 usages pour essayer de mutualiser du code

    • On extrait 1 squelette de classe

    • On extrait également 1 record contenant le Guid de la partie

  • On utilise cette classe dans 1 premier Use Case

  • On implémente la méthode Handle

  • On "fixe" la classe ConsulterStatus

Alternativement, nous pourrions utiliser 1 Higher Order Function plutôt qu'une méthode Abstract ici

  • On refactor les autres Use Cases

    • Et apporte quelques améliorations

    • Finalement, voici le code de nos Use Cases (sans avoir introduit aucune régression)

Confinement des Commands

Nous pouvons ajouter 1 nouvelle règle d'architecture :

Voici le code que l'on peut écrire à l'aide d'ArchUnit :

Nouveau rapport SonarCloud disponible ici.

Reflect

  • Où avez-vous placé les classes de type Command dans votre solution ?

  • Quel impact ce refactoring a eu ?

Last updated

Was this helpful?