11) "Avoid Exceptions"

Step 11 : "Avoid Exceptions"

Quel est le problème avec ce code ?

tirerUseCase.Handle(new Domain.Commands.Tirer(id, Data.Bernard));

public TResponse Handle(TRequest command)
{
    var partieDeChasse = _repository.GetById(command.PartieDeChasseId);

    if (partieDeChasse == null)
    {
        throw new LaPartieDeChasseNexistePas();
    }

    var response = _handler(partieDeChasse, command);
    _repository.Save(partieDeChasse);

    return response;
}

Si on regarde la signature de la méthode Handle :

  • TRequest -> TResponse

    • Que l'on peut traduire par : Pour tout TRequest je te retourne 1 TResponse

    • Ce qui est faux puisque cette méthode et la méthode d'handling peuvent lancer des exceptions

  • La signature de cette méthode ne représente pas de manière explicite les sorties possibles de cette dernière

Souvent notre code contient ce genre de mensonges...

Friends Don't Lie

Pour aller plus loin sur ce sujet je t'invite à regarder la super conférence de Scott Wlaschin sur le sujet : Functional Design Patterns :

Nous allons chercher à rendre ce code plus explicite en :

  • Évitant l'utilisation à outrance des Exception

    • Elles sont beaucoup trop utilisés pour représenter des cas d'erreurs business sous contrôles

  • Les remplaçant par des retours de type Error

  • Utilisant les fameuses Monads

Comment ?

  • Prendre du temps pour lire ces pages :

  • À l'aide de T.D.D et du Strangler pattern, refactorer le Use Case Tirer afin que la signature de Handle ressemble à :

    • TRequest -> Either<Error, TResponse>

    • soit Commands.Tirer -> Either<Error, VoidResponse>

      • On limitera le type Error à 1 message décrivant l'erreur qui s'est produite

Le Use Case : Tirer

Pour implémenter notre méthode nous allons repartir de la Test List actuelle :

🔴 On commence par écrire 1 test qui échoue dans la classe de test existante

  • On y décrit nos attentes vis-à vis de la future méthode

  • On ne compile pas et donc le test échoue

First failing test
  • On génère depuis le test le code de la méthode HandleSansException

  • On ajoute les références nous permettant d'utiliser des monades existantes : LanguageExt

  • On doit maintenant générer la classe Error

Error missing
  • On "fixe" les assertions du test pour pouvoir compiler

  • On est maintenant au rouge pour une bonne raison

No more compilation issues

🟢 On fait passer le test au vert le plus rapidement possible

  • Ici on peut retourner directement 1 Error grâce à l'import de namespace ci-dessous et l'implicit conversion :

🔵 Qu'est-ce qui peut être amélioré ici ?

  • On peut également refactorer le test afin que ce dernier respecte la structure Given / When / Then

    • On peut isoler les méthodes actuelles dans une classe partial

    • On sait qu'à terme elle pourra être supprimée...

Extract partial
  • On décrit ce qu'on voudrait dans le Given / When / Then et l'extrait puis "générifie" le tout

Voici où on en est :

Le chasseur n'est pas dans la partie

🔴 On modifie le test existant

🟢 On doit adapter la méthode HandleSansException pour supporter cette fonctionnalité

🔵 Appeler le code du Domain

Le chasseur n'a plus de balle

🔴 On modifie le test existant

🟢 On adapte encore une fois la méthode HandleSansException

🔵 Cette logique d'exception devra disparaitre, il est donc temps de s'y attaquer.

  • On va modifier la méthode du Domain pour qu'elle ressemble à celà :

    • string -> Func<DateTime> -> IPartieDeChasseRepository -> Either<Error, PartieDeChasse>

  • Nous voulons quelque chose du genre dans le Use Case :

  • On adapte le code de la PartieDeChasse afin de couvrir les besoins actuelles :

On a bien avancé :

Finir la Test-List

  • En finissant la Test-List nous avonc le code du Domain qui ressemble à ça :

  • On a pas mal de duplication à supprimer

    • Chaque message est construit 2 fois -> pour l'event et l'erreur

    • De la même manière, avons-nous encore besoin d'appeler le Save du repository ?

      • Celui-ci était présent car les exceptions "coupaient" le flow...

      • Notre Use Case peut très bien le faire de manière systématique

🔵 On extrait une nouvelle méthode

  • On itère dessus, ainsi que sur le code du Use Case :

  • On ajoute une méthode sur le repository afin de pouvoir construire 1 pipeline :

Après avoir tout refactoré...

Une fois que l'on a fini de refactoré le Domain et tous les Use Cases pour retourner des monads il est temps de nettoyer le code

  • On commence par vérifier que les exceptions ne sont plus utilisées

No more caller
  • On peut les supprimer de manière safe

Delete exceptions
  • On supprime le code mort comme la méthode GetById du repository

Remove unused
  • On supprime les paramètres inutiles (repository)

Remove repository parameter
  • À la fin de cette étape le code du `PartieDeChasseUseCase` ressemble à ça :

  • Et le code d'un Use Case comme Tirer :

Impact sur Sonar Cloud

Nouveau rapport Sonar

Plus aucun Code Smells à signaler sans se focaliser dessus 🤗👍

Nouveau rapport SonarCloud disponible ici.

Reflect

  • Qu'est-ce que vous pensez des Monads ?

  • Quel est leur impact sur notre code ?

  • Quel impact ce refactoring a eu ?

  • Qu'est-ce que ça pourrait avoir comme impact sur votre code de production ?

Last updated

Was this helpful?