Knowledge-base
  • Home
  • Samman Technical Coaching
  • Software craftsmanship
    • Practices
      • Pair Programming
      • Code Review
      • Co-designs
      • Design sessions
      • Interview Domain Experts
      • Dev ethics
    • The Software Craftsman
    • Egoless Crafting
    • Technical debt workshop
    • Functional Programming made easy in C# with Language-ext
    • F# for OO programmers
    • Domain Modeling Made Functional
    • Testing
      • Clean Tests
      • Improve the design and testing of your micro-services through CDC Tests
        • CDC testing made simple with Pact
        • Pact broker : the missing piece of your Consumer-Driven Contract approach
      • Improve your test quality with Mutation testing
      • How to name our Unit Tests
      • How to write better assertions
    • Katas
      • TDD
        • Stack kata
        • Fizzbuzz
        • Outside-in TDD (London Style)
      • Improve your software quality with Property-Based Testing
        • A journey to Property-Based Testing
      • Clean Code
      • Clean Architecture
      • Write S.O.L.I.D code
      • Mocking
      • Gilded Rose (Approval Testing)
      • Mikado method
        • Mikado kata
      • Pure functions
      • Theatrical players refactoring Kata
        • Let's refactor (OOP style)
        • Let's refactor (FP style)
      • Functional Programming made easy in Java & C#
      • Refactoring journey
      • Refactoring du Bouchonnois
        • 1) Se faire une idée du code
        • 2) "Treat warnings as errors"
        • 3) Let's kill some mutants
        • 4) Améliorer la lisibilité des tests
        • 5) "Approve Everything"
        • 6) Définir des propriétés
        • 7) Tests d'architecture
        • 8) Use Cases
        • 9) Tell Don't Ask
        • 10) "Avoid Primitives" - Commands
        • 11) "Avoid Exceptions"
        • 12) "Event Sourcing"
    • Software Design X-Rays
      • Workshop
    • The Programmer's Brain
      • How to read code better
  • Software Architecture
    • Fundamentals of Software Architecture
    • Aligning Product & Software Design
    • DDD re-distilled
    • Test your architecture with Archunit
    • NoSQL
  • Agile coaching
    • How to run a Community of Practices (COP)
    • The developers — the forgotten of agility
      • The secrets to re-on-board the devs in agility
    • Coaching toolbox
      • Echelle
      • Learning expedition
    • How to improve Team Decision making ?
      • Decision Making Principles and Practices
    • Learning 3.0
    • Retrospectives
      • Back to the Future
      • Mission Impossible
      • Movie themes
      • Rétro dont vous êtes le héros
      • Sad/Mad/Glad
      • Speed boat
      • Star wars theme
      • Story cubes
    • Technical Agile Coaching with the Samman Method
    • Xanpan - a team centric agile method story
    • XTREM WATCH — Découvrez la puissance de la veille collective
    • Become a better speaker through peer feedback
    • Project-to-Product Principles
  • Leadership
    • Bref. J'ai pris une tarte dans la gueule (et ça fait extrêmement de bien)
    • Forward Summit 2020
    • Learn leadership from the Navy SEALs
    • Learn to lead and help your team(s) to be successful
    • Towards a learning organization and beyond
    • Leadership is language
  • Serious games
    • My serious games
    • Libérez vos entretiens d’embauche avec la gamification
    • How to create a game
    • How to debrief a game ?
    • Lego Serious Play (LSP)
      • LSP in your job interviews
  • Xtrem Reading
    • Cultivate Team Learning with Xtrem Reading
    • My Book Infographics
    • How to make book infographics
    • En route vers l’apprenance avec Xtrem Reading
    • Resources
      • Book notes
        • Agile People: A Radical Approach for HR & Managers
        • Agile testing : A Practical Guide for Testers and Agile Teams
        • Boite à outils de l'intelligence émotionnelle
        • Building a better business using Lego Serious Play method
        • Building evolutionary architectures
        • Code that fits in your head
        • Culture Agile
        • Culture is everything
        • Domain-Driven Design: The First 15 Years
        • Dynamic Reteaming - The Art and Wisdom of Changing Teams
        • How to avoid a Climate Disaster
        • La liberté du commandement
        • Réaliser ses rêves, ça s'apprend
        • Refactoring at Scale
        • Succeeding with OKRs in Agile
        • Team Topologies
        • The Good Life
        • Tu fais quoi dans la vie
        • Who Does What By How Much?
  • My Activity
    • Retour sur mon année 2020
Powered by GitBook
On this page
  • Comment ?
  • Le Use Case : Tirer
  • Le chasseur n'est pas dans la partie
  • Le chasseur n'a plus de balle
  • Finir la Test-List
  • Après avoir tout refactoré...
  • Impact sur Sonar Cloud
  • Reflect

Was this helpful?

  1. Software craftsmanship
  2. Katas
  3. Refactoring du Bouchonnois

11) "Avoid Exceptions"

Previous10) "Avoid Primitives" - CommandsNext12) "Event Sourcing"

Last updated 1 year ago

Was this helpful?

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...

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 :

Échoue car :
- La partie n'existe pas
- Le chasseur n'a plus de balle
- Le chasseur n'est pas dans la partie
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée

Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles
  • On y décrit nos attentes vis-à vis de la future méthode

[Fact]
public void CarPartieNexistePasSansException()
{
    // TODO extract to Given When Then methods
    
    // Arrange
    var partieDeChasseId = UnePartieDeChasseInexistante();
    
    // Act
    var result = _useCase.HandleSansException(new Domain.Commands.Tirer(partieDeChasseId, Data.Bernard));
    
    // Assert
    result.Should().BeLeft(); // Par convention Left contient le cas d'erreur
    result.Left().Should().Be($"La partie de chasse {partieDeChasseId} n'existe pas");
    SavedPartieDeChasse().Should().BeNull();
}
  • On ne compile pas et donc le test échoue

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

public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command) => throw new NotImplementedException();
dotnet add package "LanguageExt.Core"
dotnet add package "FluentAssertions.LanguageExt"
  • On doit maintenant générer la classe Error

public record Error(string Message);
  • On "fixe" les assertions du test pour pouvoir compiler

[Fact]
public void CarPartieNexistePasSansException()
{
    // TODO extract to Given When Then methods

    // Arrange
    var partieDeChasseId = UnePartieDeChasseInexistante();

    // Act
    var result = _useCase.HandleSansException(new Domain.Commands.Tirer(partieDeChasseId, Data.Bernard));

    // Assert
    result.Should().BeLeft(); // Par convention Left contient le cas d'erreur
    result.IfLeft(error =>
    {
        error.Message.Should().Be($"La partie de chasse {partieDeChasseId} n'existe pas");
        SavedPartieDeChasse().Should().BeNull();
    });
}
  • On est maintenant au rouge pour une bonne raison

public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command) => new Error($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
  • Ici on peut retourner directement 1 Error grâce à l'import de namespace ci-dessous et l'implicit conversion :

using static LanguageExt.Prelude;
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command) => AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");

public record Error
{
    public string Message { get; }
    private Error(string message) => Message = message;
    public static Error AnError(string message) => new(message);
}
  • 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...

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

public class Tirer : UseCaseTestWithoutException<UseCases.Tirer, VoidResponse>
{
    ...

    public class Echoue : UseCaseTestWithoutException<UseCases.Tirer, VoidResponse>
    {
        ...
        
        [Fact]
        public void CarPartieNexistePasSansException()
        {
            Given(UnePartieDeChasseInexistante());

            When(partieDeChasseId =>
                _useCase.HandleSansException(new Domain.Commands.Tirer(partieDeChasseId, Data.Bernard)));

            ThenFailWith(
                $"La partie de chasse {_partieDeChasseId} n'existe pas",
                savedPartieDeChasse => savedPartieDeChasse.Should().BeNull()
            );
        }
    ...
}

public abstract class UseCaseTestWithoutException<TUseCase, TSuccessResponse> : UseCaseTest<TUseCase>
{
    protected UseCaseTestWithoutException(Func<IPartieDeChasseRepository, Func<DateTime>, TUseCase> useCaseFactory)
        : base(useCaseFactory)
    {
    }

    private Func<Guid, Either<Error, TSuccessResponse>>? _act;
    protected void When(Func<Guid, Either<Error, TSuccessResponse>>? act) => _act = act;

    protected void ThenFailWith(string expectedErrorMessage, Action<PartieDeChasse?>? assertSavedPartieDeChasse)
    {
        var result = _act!(_partieDeChasseId);
        result.Should().BeLeft();
        result.IfLeft(r =>
        {
            r.Message.Should().Be(expectedErrorMessage);
            assertSavedPartieDeChasse?.Invoke(SavedPartieDeChasse());
        });
    }
}

Voici où on en est :

Échoue car :
✅ La partie n'existe pas
- Le chasseur n'est pas dans la partie
- Le chasseur n'a plus de balle
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée

Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles

Le chasseur n'est pas dans la partie

[Fact]
public void CarLeChasseurNestPasDansLaPartie()
{
    Given(
        UnePartieDeChasseExistante(
            SurUnTerrainRicheEnGalinettes()
        ));

    When(id => _useCase.HandleSansException(new Domain.Commands.Tirer(id, Data.ChasseurInconnu)));

    ThenFailWith("Chasseur inconnu Chasseur inconnu",
        savedPartieDeChasse => savedPartieDeChasse.Should().BeNull());
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
    if (_repository.GetById(command.PartieDeChasseId) == null)
    {
        return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
    }

    return AnError($"Chasseur inconnu {command.Chasseur}");
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command,
            Func<DateTime> timeProvider)
{
    var partieDeChasse = _repository.GetById(command.PartieDeChasseId);

    if (partieDeChasse == null)
        return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");

    try
    {
        partieDeChasse.Tirer(command.Chasseur, timeProvider, _repository);
    }
    catch (ChasseurInconnu)
    {
        return AnError($"Chasseur inconnu {command.Chasseur}");
    }

    return VoidResponse.Empty;
}
Échoue car :
✅ La partie n'existe pas
✅ Le chasseur n'est pas dans la partie
- Le chasseur n'a plus de balle
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée

Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles

Le chasseur n'a plus de balle

[Fact]
public void AvecUnChasseurNayantPlusDeBalles()
{
    Given(
        UnePartieDeChasseExistante(
            SurUnTerrainRicheEnGalinettes()
                .Avec(Dédé(), Bernard().SansBalles(), Robert())
        ));

    When(id => _useCase.HandleSansException(new Domain.Commands.Tirer(id, Data.Bernard)));

    ThenFailWith("Bernard tire -> T'as plus de balles mon vieux, chasse à la main",
        savedPartieDeChasse => savedPartieDeChasse.Should().HaveEmittedEvent(Now,
            $"Bernard tire -> T'as plus de balles mon vieux, chasse à la main")
    );
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
    var partieDeChasse = _repository.GetById(command.PartieDeChasseId);

    if (partieDeChasse == null)
        return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");

    try
    {
        partieDeChasse.Tirer(command.Chasseur, _timeProvider, _repository);
    }
    catch (ChasseurInconnu)
    {
        return AnError($"Chasseur inconnu {command.Chasseur}");
    }
    catch (TasPlusDeBallesMonVieuxChasseALaMain)
    {
        return AnError($"{command.Chasseur} tire -> T'as plus de balles mon vieux, chasse à la main");
    }

    return VoidResponse.Empty;
}
  • 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 :

public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
    var partieDeChasse = _repository.GetById(command.PartieDeChasseId);

    if (partieDeChasse == null)
        return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");

    return partieDeChasse
        .TirerSansException(command.Chasseur, _timeProvider, _repository)
        .Map(_ => VoidResponse.Empty);
}
  • On adapte le code de la PartieDeChasse afin de couvrir les besoins actuelles :

public Either<Error, PartieDeChasse> TirerSansException(
    string chasseur,
    Func<DateTime> timeProvider,
    IPartieDeChasseRepository repository)
    => TirerSansException(chasseur,
        timeProvider,
        repository,
        debutMessageSiPlusDeBalles: $"{chasseur} tire");

// Le Domain renvoie directement l'erreur s'il y en a plutôt que de lancer des exceptions
private Either<Error, PartieDeChasse> TirerSansException(
    string chasseur,
    Func<DateTime> timeProvider,
    IPartieDeChasseRepository repository,
    string debutMessageSiPlusDeBalles,
    Action<Chasseur>? continueWith = null)
{
    if (DuringApéro())
    {
        EmitEventAndSave($"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!", timeProvider,
            repository);
        throw new OnTirePasPendantLapéroCestSacré();
    }

    if (DéjàTerminée())
    {
        EmitEventAndSave($"{chasseur} veut tirer -> On tire pas quand la partie est terminée", timeProvider,
            repository);
        throw new OnTirePasQuandLaPartieEstTerminée();
    }

    if (!ChasseurExiste(chasseur))
    {
        return AnError($"Chasseur inconnu {chasseur}");
    }

    var chasseurQuiTire = RetrieveChasseur(chasseur);

    if (!chasseurQuiTire.AEncoreDesBalles())
    {
        EmitEventAndSave($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main",
            timeProvider, repository);
        return AnError($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main");
    }

    chasseurQuiTire.ATiré();
    continueWith?.Invoke(chasseurQuiTire);

    EmitEvent($"{chasseur} tire", timeProvider);

    return this;
}

On a bien avancé :

Échoue car :
✅ La partie n'existe pas
✅ Le chasseur n'est pas dans la partie
✅ Le chasseur n'a plus de balle
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée

Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles

Finir la Test-List

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

public Either<Error, PartieDeChasse> TirerSansException(
    string chasseur,
    Func<DateTime> timeProvider,
    IPartieDeChasseRepository repository)
    => TirerSansException(chasseur,
        timeProvider,
        repository,
        debutMessageSiPlusDeBalles: $"{chasseur} tire");

private Either<Error, PartieDeChasse> TirerSansException(
    string chasseur,
    Func<DateTime> timeProvider,
    IPartieDeChasseRepository repository,
    string debutMessageSiPlusDeBalles,
    Action<Chasseur>? continueWith = null)
{
    if (DuringApéro())
    {
        EmitEventAndSave($"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!", timeProvider,
            repository);
        return AnError($"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!");
    }

    if (DéjàTerminée())
    {
        EmitEventAndSave($"{chasseur} veut tirer -> On tire pas quand la partie est terminée", timeProvider,
            repository);
        return AnError($"{chasseur} veut tirer -> On tire pas quand la partie est terminée");
    }

    if (!ChasseurExiste(chasseur))
    {
        return AnError($"Chasseur inconnu {chasseur}");
    }

    var chasseurQuiTire = RetrieveChasseur(chasseur);

    if (!chasseurQuiTire.AEncoreDesBalles())
    {
        EmitEventAndSave($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main",
            timeProvider, repository);
        return AnError($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main");
    }

    chasseurQuiTire.ATiré();
    continueWith?.Invoke(chasseurQuiTire);

    EmitEvent($"{chasseur} tire", timeProvider);

    return this;
}
  • 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

private Either<Error, PartieDeChasse> TirerSansException(
    string chasseur,
    Func<DateTime> timeProvider,
    IPartieDeChasseRepository repository,
    string debutMessageSiPlusDeBalles,
    Action<Chasseur>? continueWith = null)
{
    if (DuringApéro())
    {
        var message = $"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!";
        return EmitAndReturn(timeProvider, repository, message);
    }
    ...
}

private Either<Error, PartieDeChasse> EmitAndReturn(Func<DateTime> timeProvider, IPartieDeChasseRepository repository, string message)
{
    EmitEventAndSave(message, timeProvider,
        repository);
    return AnError(message);
}
  • On itère dessus, ainsi que sur le code du Use Case :

private Either<Error, PartieDeChasse> TirerSansException(
    string chasseur,
    Func<DateTime> timeProvider,
    string debutMessageSiPlusDeBalles,
    Action<Chasseur>? continueWith = null)
{
    if (DuringApéro())
    {
        return EmitAndReturn(
            $"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!",
            timeProvider);
    }

    if (DéjàTerminée())
    {
        return EmitAndReturn($"{chasseur} veut tirer -> On tire pas quand la partie est terminée",
            timeProvider);
    }

    if (!ChasseurExiste(chasseur))
    {
        return EmitAndReturn($"Chasseur inconnu {chasseur}", timeProvider);
    }

    var chasseurQuiTire = RetrieveChasseur(chasseur);

    if (!chasseurQuiTire.AEncoreDesBalles())
    {
        return EmitAndReturn($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main",
            timeProvider);
    }

    chasseurQuiTire.ATiré();
    continueWith?.Invoke(chasseurQuiTire);

    EmitEvent($"{chasseur} tire", timeProvider);

    return this;
}

private Either<Error, PartieDeChasse> EmitAndReturn(string message, Func<DateTime> timeProvider)
{
    EmitEvent(message, timeProvider);
    return AnError(message);
}

// Use Case
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
    var partieDeChasse = _repository.GetById(command.PartieDeChasseId);

    if (partieDeChasse == null)
        return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");

    var result = partieDeChasse
        .TirerSansException(command.Chasseur, _timeProvider)
        .Map(_ => VoidResponse.Empty);

    // On force le Save de la Partie de Chasse quelque soit le retour (Succès ou pas)
    _repository.Save(partieDeChasse);

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

public interface IPartieDeChasseRepository
{
    ...
    // Renvoie auelque chose ou pas
    Option<PartieDeChasse> GetByIdOption(Guid partieDeChasseId);
}

public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
    PartieDeChasse? foundPartieDeChasse = null;

    var result = _repository.GetByIdOption(command.PartieDeChasseId)
        // affecte la Partie De Chasse
        .Do(p => foundPartieDeChasse = p)
        .ToEither(() => AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas"))
        // Monadic Binding
        // Permet d'"aplatir" : avec Map on aurait 1 Either<Error, Either<Error, VoidResponse>> 
        .Bind(partieDeChasse => partieDeChasse.TirerSansException(command.Chasseur, _timeProvider));

    if (foundPartieDeChasse != null) _repository.Save(foundPartieDeChasse);

    return result;
}

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

  • On peut les supprimer de manière safe

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

  • On supprime les paramètres inutiles (repository)

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

public abstract class PartieDeChasseUseCase<TRequest, TResponse> : IUseCase<TRequest, TResponse>
    where TRequest : PartieDeChasseCommand
{
    private readonly IPartieDeChasseRepository _repository;
    private readonly Func<PartieDeChasse, TRequest, Either<Error, TResponse>> _handler;

    protected PartieDeChasseUseCase(
        IPartieDeChasseRepository repository,
        Func<PartieDeChasse, TRequest, Either<Error, TResponse>> handler)
    {
        _repository = repository;
        _handler = handler;
    }

    public Either<Error, TResponse> Handle(TRequest command) =>
        _repository
            .GetById(command.PartieDeChasseId)
            .ToEither(() => AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas"))
            .Bind(p => HandleCommand(p, command));

    private Either<Error, TResponse> HandleCommand(PartieDeChasse partieDeChasse, TRequest command) =>
        _handler(partieDeChasse, command)
            // Let is a scope function inspired by Kotlin : https://kotlinlang.org/docs/scope-functions.html
            .Let(_ => _repository.Save(partieDeChasse));

    protected static Either<Error, VoidResponse> ToEmpty(Either<Error, PartieDeChasse> either)
        => either.Map(_ => VoidResponse.Empty);
}
  • Et le code d'un Use Case comme Tirer :

public sealed class Tirer : PartieDeChasseUseCase<Domain.Commands.Tirer, VoidResponse>
{
    public Tirer(IPartieDeChasseRepository repository, Func<DateTime> timeProvider)
        : base(repository,
            (partieDeChasse, command) => ToEmpty(partieDeChasse.Tirer(command.Chasseur, timeProvider)))
    {
    }
}

Impact sur Sonar Cloud

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

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 ?

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

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

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

On fait passer le test au vert le plus rapidement possible

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

Peut-être, faire une pour l'instantiation des Error

On modifie le test existant

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

Appeler le code du Domain

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 extrait une nouvelle méthode

Nouveau rapport SonarCloud disponible .

🔴
🟢
🔵
🔴
🟢
🔵
🔴
🟢
🔵
🔵
Scott Wlaschin
Functional Design Patterns
Avoid Exceptions
Monads
LanguageExt
Factory Method
ici
Step 11 : "Avoid Exceptions"
Friends Don't Lie
First failing test
Error missing
No more compilation issues
Extract partial
No more caller
Delete exceptions
Remove unused
Remove repository parameter
Nouveau rapport Sonar