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
  • Extraire un premier Use Case
  • Supprimer le service
  • Mise à jour des règles ArchUnit
  • Impact sur l'analyse comportementale
  • Reflect

Was this helpful?

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

8) Use Cases

Previous7) Tests d'architectureNext9) Tell Don't Ask

Last updated 1 year ago

Was this helpful?

Maintenant que nous sommes confiants vis-à-vis de nos tests nous allons pouvoir commencer à refactorer.

Nous pouvons démarrer en splittant notre principal hostpot : PartieDeChasseService.

Pour ce faire, nous allons utiliser la stratégie Divide and Conquer :

    • Notamment la notion de Use Case

  • Extraire 1 Use Case par méthode du Service

  • Améliorer la définition de notre architecture via nos tests Archunit

Extraire un premier Use Case

  • Utiliser les fonctionnalités de notre IDE pour extraire la méthode Démarrer

    • Refactor -> Extract -> Extract Class

    • Penser à configurer l'extraction de la méthode en Create delegating Wrapper

  • Voici le résultat :

public class DemarrerPartieDeChasseUseCase
{
    private readonly IPartieDeChasseRepository _repository;
    private readonly Func<DateTime> _timeProvider;

    public DemarrerPartieDeChasseUseCase(IPartieDeChasseRepository repository, Func<DateTime> timeProvider)
    {
        _repository = repository;
        _timeProvider = timeProvider;
    }

    public Guid Demarrer((string nom, int nbGalinettes) terrainDeChasse, List<(string nom, int nbBalles)> chasseurs)
    {
        if (terrainDeChasse.nbGalinettes <= 0)
        {
            throw new ImpossibleDeDémarrerUnePartieSansGalinettes();
        }

        var partieDeChasse =
            new PartieDeChasse(Guid.NewGuid(),
                new Terrain(terrainDeChasse.nom)
                {
                    NbGalinettes = terrainDeChasse.nbGalinettes
                }
            );

        foreach (var chasseur in chasseurs)
        {
            if (chasseur.nbBalles == 0)
            {
                throw new ImpossibleDeDémarrerUnePartieAvecUnChasseurSansBalle();
            }

            partieDeChasse.Chasseurs.Add(new Chasseur(chasseur.nom)
            {
                BallesRestantes = chasseur.nbBalles
            });
        }

        if (partieDeChasse.Chasseurs.Count == 0)
        {
            throw new ImpossibleDeDémarrerUnePartieSansChasseur();
        }

        string chasseursToString = string.Join(
            ", ",
            partieDeChasse.Chasseurs.Select(c => c.Nom + $" ({c.BallesRestantes} balles)")
        );

        partieDeChasse.Events.Add(new Event(_timeProvider(),
            $"La partie de chasse commence à {partieDeChasse.Terrain.Nom} avec {chasseursToString}")
        );

        _repository.Save(partieDeChasse);

        return partieDeChasse.Id;
    }
}
  • La classe PartieDeChasseService délègue maintenant les appels de la méthode Démarrer à notre Use Case

    • Ainsi, nous compilons et nos tests sont toujours au vert

public Guid Demarrer((string nom, int nbGalinettes) terrainDeChasse, List<(string nom, int nbBalles)> chasseurs)
        => _demarrerPartieDeChasseUseCase.Demarrer(terrainDeChasse, chasseurs);
  • On adapte les tests de ce Use Case

namespace Bouchonnois.Tests.Unit
{
    [UsesVerify]
    public class DemarrerUnePartieDeChasse : PartieDeChasseServiceTest
    {
        // On appelle le Use Case et non plus le Service 
        private readonly DemarrerPartieDeChasse _useCase;

        public DemarrerUnePartieDeChasse()
        {
            _useCase = new DemarrerPartieDeChasse(Repository, TimeProvider);
        }

        [Fact]
        public Task AvecPlusieursChasseurs()
        {
            var command = DémarrerUnePartieDeChasse()
                .Avec((Data.Dédé, 20), (Data.Bernard, 8), (Data.Robert, 12))
                .SurUnTerrainRicheEnGalinettes();

            _useCase.Handle(
                command.Terrain,
                command.Chasseurs
            );

            return Verify(Repository.SavedPartieDeChasse())
                .DontScrubDateTimes();
        }

        [Property]
        public Property Sur1TerrainAvecGalinettesEtChasseursAvecTousDesBalles() =>
            ForAll(
                terrainRicheEnGalinettesGenerator(),
                chasseursAvecBallesGenerator(),
                (terrain, chasseurs) => DémarreLaPartieAvecSuccès(terrain, chasseurs));

        private bool DémarreLaPartieAvecSuccès((string nom, int nbGalinettes) terrain,
            IEnumerable<(string nom, int nbBalles)> chasseurs)
            => _useCase.Handle(
                terrain,
                chasseurs.ToList()) == Repository.SavedPartieDeChasse()!.Id;

        public class Echoue : DemarrerUnePartieDeChasse
        {
            [Property]
            public Property SansChasseursSurNimporteQuelTerrainRicheEnGalinette()
                => ForAll(
                    terrainRicheEnGalinettesGenerator(),
                    terrain =>
                        EchoueAvec<ImpossibleDeDémarrerUnePartieSansChasseur>(
                            terrain,
                            PasDeChasseurs,
                            savedPartieDeChasse => savedPartieDeChasse == null)
                );

            [Property]
            public Property AvecUnTerrainSansGalinettes()
                => ForAll(
                    terrainSansGalinettesGenerator(),
                    chasseursAvecBallesGenerator(),
                    (terrain, chasseurs) =>
                        EchoueAvec<ImpossibleDeDémarrerUnePartieSansGalinettes>(
                            terrain,
                            chasseurs.ToList(),
                            savedPartieDeChasse => savedPartieDeChasse == null)
                );

            [Property]
            public Property SiAuMoins1ChasseurSansBalle() =>
                ForAll(
                    terrainRicheEnGalinettesGenerator(),
                    chasseursSansBallesGenerator(),
                    (terrain, chasseurs) =>
                        EchoueAvec<ImpossibleDeDémarrerUnePartieAvecUnChasseurSansBalle>(
                            terrain,
                            chasseurs.ToList(),
                            savedPartieDeChasse => savedPartieDeChasse == null)
                );

            private bool EchoueAvec<TException>(
                (string nom, int nbGalinettes) terrain,
                IEnumerable<(string nom, int nbBalles)> chasseurs,
                Func<PartieDeChasse?, bool>? assert = null) where TException : Exception
                => MustFailWith<TException>(() => _useCase.Handle(terrain, chasseurs.ToList()), assert);
        }
    }
}

Répliquer cela pour tous les Use Cases

A la fin, le PartieDeChasseService ressemble à cela :

public class PartieDeChasseService
{
    private readonly IPartieDeChasseRepository _repository;
    private readonly DemarrerPartieDeChasse _demarrerPartieDeChasse;
    private readonly TirerSurUneGalinette _tirerSurUneGalinette;
    private readonly Tirer _tirer;
    private readonly PrendreLapéro _prendreLapéro;
    private readonly ReprendreLaPartie _reprendreLaPartie;
    private readonly TerminerLaPartie _terminerLaPartie;
    private readonly ConsulterStatus _consulterStatus;

    public PartieDeChasseService(
        IPartieDeChasseRepository repository,
        Func<DateTime> timeProvider)
    {
        _repository = repository;

        _consulterStatus = new ConsulterStatus(repository);
        _terminerLaPartie = new TerminerLaPartie(repository, timeProvider);
        _reprendreLaPartie = new ReprendreLaPartie(repository, timeProvider);
        _prendreLapéro = new PrendreLapéro(repository, timeProvider);
        _tirer = new Tirer(repository, timeProvider);
        _tirerSurUneGalinette = new TirerSurUneGalinette(repository, timeProvider);
        _demarrerPartieDeChasse = new DemarrerPartieDeChasse(repository, timeProvider);
    }

    public Guid Demarrer((string nom, int nbGalinettes) terrainDeChasse, List<(string nom, int nbBalles)> chasseurs)
        => _demarrerPartieDeChasse.Handle(terrainDeChasse, chasseurs);

    public void TirerSurUneGalinette(Guid id, string chasseur)
        => _tirerSurUneGalinette.Handle(id, chasseur);

    public void Tirer(Guid id, string chasseur) => _tirer.Handle(id, chasseur);

    public void PrendreLapéro(Guid id) => _prendreLapéro.Handle(id);

    public void ReprendreLaPartie(Guid id) => _reprendreLaPartie.Handle(id);

    public string TerminerLaPartie(Guid id) => _terminerLaPartie.Handle(id);

    public string ConsulterStatus(Guid id) => _consulterStatus.Handle(id);
}

Supprimer le service

  • L'arborescence de notre projet ressemble désormais à celà :

  • Il ne reste plus qu'un seul appelant du srvice PartieDeChasseService -> ScenarioTests

  • Nous allons rediriger les appels des tests vers les Use Case

namespace Bouchonnois.Tests.Acceptance
{
    [UsesVerify]
    public class ScenarioTests
    {
        private DateTime _time = new(2024, 4, 25, 9, 0, 0);

        private readonly DemarrerPartieDeChasse _demarrerPartieDeChasse;
        private readonly Tirer _tirer;
        private readonly TirerSurUneGalinette _tirerSurUneGalinette;
        private readonly PrendreLapéro _prendreLapéro;
        private readonly ReprendreLaPartie _reprendreLaPartie;
        private readonly TerminerLaPartie _terminerLaPartie;
        private readonly ConsulterStatus _consulterStatus;

        public ScenarioTests()
        {
            var repository = new PartieDeChasseRepositoryForTests();
            var timeProvider = () => _time;

            _demarrerPartieDeChasse = new DemarrerPartieDeChasse(repository, timeProvider);
            _tirer = new Tirer(repository, timeProvider);
            _tirerSurUneGalinette = new TirerSurUneGalinette(repository, timeProvider);
            _prendreLapéro = new PrendreLapéro(repository, timeProvider);
            _reprendreLaPartie = new ReprendreLaPartie(repository, timeProvider);
            _terminerLaPartie = new TerminerLaPartie(repository, timeProvider);
            _consulterStatus = new ConsulterStatus(repository);
        }

        [Fact]
        public Task DéroulerUnePartie()
        {
            var command = DémarrerUnePartieDeChasse()
                .Avec((Data.Dédé, 20), (Data.Bernard, 8), (Data.Robert, 12))
                .SurUnTerrainRicheEnGalinettes(4);

            var id = _demarrerPartieDeChasse.Handle(
                command.Terrain,
                command.Chasseurs
            );

            After(10.Minutes(), () => _tirer.Handle(id, Data.Dédé));
            After(30.Minutes(), () => _tirerSurUneGalinette.Handle(id, Data.Robert));
            After(20.Minutes(), () => _prendreLapéro.Handle(id));
            After(1.Hours(), () => _reprendreLaPartie.Handle(id));
            After(2.Minutes(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Minutes(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Minutes(), () => _tirerSurUneGalinette.Handle(id, Data.Dédé));
            After(26.Minutes(), () => _tirerSurUneGalinette.Handle(id, Data.Robert));
            After(10.Minutes(), () => _prendreLapéro.Handle(id));
            After(170.Minutes(), () => _reprendreLaPartie.Handle(id));
            After(11.Minutes(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Seconds(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Seconds(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Seconds(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Seconds(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Seconds(), () => _tirer.Handle(id, Data.Bernard));
            After(1.Seconds(), () => _tirer.Handle(id, Data.Bernard));
            After(19.Minutes(), () => _tirerSurUneGalinette.Handle(id, Data.Robert));
            After(30.Minutes(), () => _terminerLaPartie.Handle(id));

            return Verify(_consulterStatus.Handle(id));
        }
        ...
    }
}
  • On peut maintenant supprimer la classe PartieDeChasseService de manière totalement safe

  • On place l'ensemble des Exceptions au plus proche de leur utilisation (dans le namespace UseCases)

Notre architecture ressemble maintenant à ça

Mise à jour des règles ArchUnit

public class ArchitectureRules
{
    private static GivenTypesConjunctionWithDescription UseCases() =>
        TypesInAssembly().And()
            .ResideInNamespace("UseCases", true)
            .As("Use Cases");

    private static GivenTypesConjunctionWithDescription Infrastructure() =>
        TypesInAssembly().And()
            .ResideInNamespace("Repository", true)
            .As("Infrastructure");

    [Fact]
    public void UseCasesRules() =>
        UseCases().Should()
            .NotDependOnAny(Infrastructure())
            .Check();
      
    ...

Impact sur l'analyse comportementale

Après avoir extrait les Use Cases, nous avons "tué" le hotspot identifié par codescene :

Nous avons simplement divisé cette grosse classe "fourre-tout" en plus petites unités, avec des dépendances clairement identifiées qu'on va pouvoir tranquillement refactorer.

Reflect

  • Quel est l'impact sur le design ? les tests ?

  • En quoi pouvons nous parler ici de Screaming Architecture ?

Prendre du temps pour comprendre ce qu'est la

Nouveau rapport SonarCloud disponible .

Clean Architecture
ici
Step 8 : Use Cases
Service Hotspot
Extract class
Remove Service
Exceptions
Architecture
Hotspots mis à jour