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
  • DemarrerPartieDeChasse
  • Refactoring du code de production
  • Reproduire ces étapes pour les autres Commands
  • Factoriser le code des Use Cases
  • Confinement des Commands
  • Reflect

Was this helpful?

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

10) "Avoid Primitives" - Commands

Previous9) Tell Don't AskNext11) "Avoid Exceptions"

Last updated 1 year ago

Was this helpful?

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)

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

  • Puis on configure l'extraction

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

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

var id = _demarrerPartieDeChasse.Handle(new DemarrerPartieDeChasseCommand(command.Terrain, command.Chasseurs));
  • On peut maintenant renommer et bouger cette classe dans notre Domain

    • Dans le namespace Commands

    • Pour en savoir plus sur le choix de placer les Commands dans le Domain, je vous invite à lire ce super article de Vladimir Khorikov

using Commands = Bouchonnois.Domain.Commands;

namespace Bouchonnois.UseCases
{
    public sealed class DemarrerPartieDeChasse
    {
        private readonly IPartieDeChasseRepository _repository;
        private readonly Func<DateTime> _timeProvider;

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

        public Guid Handle(Commands.DemarrerPartieDeChasse demarrerPartieDeChasse)
        {
            var partieDeChasse = PartieDeChasse.Create(_timeProvider, demarrerPartieDeChasse.TerrainDeChasse, demarrerPartieDeChasse.Chasseurs);
            _repository.Save(partieDeChasse);

            return partieDeChasse.Id;
        }
    }
}

public record DemarrerPartieDeChasse((string nom, int nbGalinettes) TerrainDeChasse, List<(string nom, int nbBalles)> Chasseurs);
  • 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

namespace Bouchonnois.Domain.Commands
{
    public record DemarrerPartieDeChasse(TerrainDeChasse TerrainDeChasse, IEnumerable<Chasseur> Chasseurs);
    public record TerrainDeChasse(string Nom, int NbGalinettes);
    public record Chasseur(string Nom, int NbBalles);
}
  • 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

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

    // C'est ce que l'on veut avec l'utilisation de notre CommandBuilder
    var id = _demarrerPartieDeChasse.Handle(command.Build());
    ...
  • On adapte notre Builder

public class CommandBuilder
{
    private (string, int)[] _chasseurs = Array.Empty<(string, int)>();
    private int _nbGalinettes;

    public static CommandBuilder DémarrerUnePartieDeChasse() => new();

    public CommandBuilder Avec(params (string, int)[] chasseurs)
    {
        _chasseurs = chasseurs;
        return this;
    }

    public CommandBuilder SurUnTerrainRicheEnGalinettes(int nbGalinettes = 3)
    {
        _nbGalinettes = nbGalinettes;
        return this;
    }

    // On choisit de ne pas changer le contrat mais uniquement ajouter la méthode Build
    public DemarrerPartieDeChasse Build() 
        => new(
                new TerrainDeChasse("Pitibon sur Sauldre", _nbGalinettes),
                _chasseurs.Select(c => new Chasseur(c.Item1, c.Item2))
            );
}

Refactoring du code de production

  • On se focalise maintenant sur le code de production

  • 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

public static PartieDeChasse Create(Func<DateTime> timeProvider, DemarrerPartieDeChasse demarrerPartieDeChasse)
{
    throw new NotImplementedException();
}
  • 🟢 On commence par utiliser l'ancienne méthode dans à partir de la nouvelle pour faire passer les tests

 public static PartieDeChasse Create(Func<DateTime> timeProvider, DemarrerPartieDeChasse demarrerPartieDeChasse) =>
    Create(timeProvider,
        (demarrerPartieDeChasse.TerrainDeChasse.Nom, demarrerPartieDeChasse.TerrainDeChasse.NbGalinettes),
        demarrerPartieDeChasse.Chasseurs.Select(c => (c.Nom, c.NbBalles)).ToList()
    );

public static PartieDeChasse Create(
    Func<DateTime> timeProvider,
    (string nom, int nbGalinettes) terrainDeChasse,
    List<(string nom, int nbBalles)> chasseurs)
{
    CheckTerrainValide(terrainDeChasse);
    CheckChasseursValides(chasseurs);

    return new PartieDeChasse(
        Guid.NewGuid(),
        timeProvider,
        new Terrain(terrainDeChasse.nom, terrainDeChasse.nbGalinettes),
        chasseurs
    );
}
  • 🔵 On peut maintenant "copier / coller" le code de l'ancienne méthode dans la nouvelle

public static PartieDeChasse Create(Func<DateTime> timeProvider, DemarrerPartieDeChasse demarrerPartieDeChasse)
{
    CheckTerrainValide(demarrerPartieDeChasse.TerrainDeChasse);
    CheckChasseursValides(demarrerPartieDeChasse.Chasseurs.ToArray());

    return new PartieDeChasse(
        Guid.NewGuid(),
        timeProvider,
        new Terrain(demarrerPartieDeChasse.TerrainDeChasse.Nom, demarrerPartieDeChasse.TerrainDeChasse.NbGalinettes),
        demarrerPartieDeChasse.Chasseurs.Select(c => new Chasseur(c.Nom, c.NbBalles)).ToArray()
    );
}

public static PartieDeChasse Create(
    Func<DateTime> timeProvider,
    (string nom, int nbGalinettes) terrainDeChasse,
    List<(string nom, int nbBalles)> chasseurs) =>
    Create(timeProvider, new DemarrerPartieDeChasse(
        new TerrainDeChasse(terrainDeChasse.nom, terrainDeChasse.nbGalinettes),
        chasseurs.Select(c => new Commands.Chasseur(c.nom, c.nbBalles))
    ));
  • On regarde qui appelait l'ancienne méthode de création

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

var partieDeChasse = PartieDeChasse.Create(
    timeProvider,
    new DemarrerPartieDeChasse(
            new TerrainDeChasse("Pitibon sur Sauldre", _nbGalinettes),
            builtChasseurs
                .Select(c => new Domain.Commands.Chasseur(c.Nom, c.BallesRestantes > 0 ? c.BallesRestantes : 1))
                .ToList()
            )
);
  • 🔵 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

public interface IUseCase<in TRequest, out TResponse> where TRequest : ICommand
{
    public TResponse Handle(TRequest command);
}

public interface ICommand
{
}
  • On l'utilise pour ConsulterStatus

    • Le gain n'est pas flagrant...

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

public sealed class ConsulterStatus : IUseCase<Domain.Commands.ConsulterStatus, string>
{
    private readonly IPartieDeChasseRepository _repository;

    public ConsulterStatus(IPartieDeChasseRepository repository)
        => _repository = repository;

    public string Handle(Domain.Commands.ConsulterStatus consulterStatus)
    {
        // On voudrait factoriser ce code...
        var partieDeChasse = _repository.GetById(consulterStatus.PartieDeChasseId);

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

        return partieDeChasse.Consulter();
    }
}
  • On continue avec un autre Use Case : PrendreLapéro

    • On tombe sur un problème

    • Le type de retour ici est void

public sealed class PrendreLapéro : IUseCase<Domain.Commands.PrendreLapéro, void>
  • On crée un type de retour pour ce besoin

public class VoidResponse
{
    public static readonly VoidResponse Empty = new();
}

public sealed class PrendreLapéro : IUseCase<Domain.Commands.PrendreLapéro, VoidResponse>
{
    private readonly IPartieDeChasseRepository _repository;
    private readonly Func<DateTime> _timeProvider;

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

    public VoidResponse Handle(Domain.Commands.PrendreLapéro prendreLapéro)
    {
        var partieDeChasse = _repository.GetById(prendreLapéro.PartieDeChasseId);

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

        partieDeChasse.PrendreLapéro(_timeProvider);
        _repository.Save(partieDeChasse);

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

public record PartieDeChasseCommand(Guid PartieDeChasseId) : ICommand;

public class PartieDeChasseUseCase<TRequest, TResponse> : IUseCase<TRequest, TResponse>
    where TRequest : PartieDeChasseCommand
{
    public TResponse Handle(TRequest command)
    {
        throw new NotImplementedException();
    }
}
  • On utilise cette classe dans 1 premier Use Case

public sealed class ConsulterStatus : PartieDeChasseUseCase<Domain.Commands.ConsulterStatus, string>
{
    private readonly IPartieDeChasseRepository _repository;

    public ConsulterStatus(IPartieDeChasseRepository repository)
        => _repository = repository;
}
  • On implémente la méthode Handle

public abstract class PartieDeChasseUseCase<TRequest, TResponse> : IUseCase<TRequest, TResponse>
        where TRequest : PartieDeChasseCommand
{
    private readonly IPartieDeChasseRepository _repository;

    public PartieDeChasseUseCase(IPartieDeChasseRepository repository) => _repository = repository;

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

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

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

        return response;
    }
    
    // Chaque Use Case devra implémenter cette méthode abstract
    protected abstract TResponse Handle(PartieDeChasse partieDeChasse, TRequest command);
}
  • On "fixe" la classe ConsulterStatus

public sealed class ConsulterStatus : PartieDeChasseUseCase<Domain.Commands.ConsulterStatus, string>
{
    public ConsulterStatus(IPartieDeChasseRepository repository) : base(repository)
    {
    }

    protected override string Handle(PartieDeChasse partieDeChasse, Domain.Commands.ConsulterStatus command)
        => partieDeChasse.Consulter();
}

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

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

    protected PartieDeChasseUseCase(IPartieDeChasseRepository repository,
        Func<PartieDeChasse, TRequest, TResponse> domainHandler)
    {
        _repository = repository;
        _domainHandler = domainHandler;
    }
    ...
}

public sealed class ConsulterStatus : PartieDeChasseUseCase<Domain.Commands.ConsulterStatus, string>
{
    public ConsulterStatus(IPartieDeChasseRepository repository) :
        base(repository, (partieDeChasse, _) => partieDeChasse.Consulter())
    {
    }
}
  • 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)

public sealed class ConsulterStatus : PartieDeChasseUseCase<Domain.Commands.ConsulterStatus, string>
{
    public ConsulterStatus(IPartieDeChasseRepository repository) :
        base(repository, (partieDeChasse, _) => partieDeChasse.Consulter())
    {
    }
}

public sealed class PrendreLapéro : EmptyResponsePartieDeChasseUseCase<Domain.Commands.PrendreLapéro>
{
    public PrendreLapéro(IPartieDeChasseRepository repository, Func<DateTime> timeProvider)
        : base(repository, (partieDeChasse, _) => partieDeChasse.PrendreLapéro(timeProvider))
    {
    }
}

public sealed class ReprendreLaPartie : EmptyResponsePartieDeChasseUseCase<Domain.Commands.ReprendreLaPartie>
{
    public ReprendreLaPartie(IPartieDeChasseRepository repository, Func<DateTime> timeProvider)
        : base(repository, (partieDeChasse, _) => partieDeChasse.Reprendre(timeProvider))
    {
    }
}

public sealed class TerminerLaPartie : PartieDeChasseUseCase<Domain.Commands.TerminerLaPartie, string>
{
    public TerminerLaPartie(IPartieDeChasseRepository repository, Func<DateTime> timeProvider)
        : base(repository, (partieDeChasse, _) => partieDeChasse.Terminer(timeProvider))
    {
    }
}

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

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

// Use Case de création -> cas particulier
public sealed class DemarrerPartieDeChasse : IUseCase<Domain.Commands.DemarrerPartieDeChasse, Guid>
{
    private readonly IPartieDeChasseRepository _repository;
    private readonly Func<DateTime> _timeProvider;

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

    public Guid Handle(Domain.Commands.DemarrerPartieDeChasse demarrerPartieDeChasse)
    {
        var partieDeChasse = PartieDeChasse.Create(_timeProvider, demarrerPartieDeChasse);
        _repository.Save(partieDeChasse);

        return partieDeChasse.Id;
    }
}

Confinement des Commands

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

Toutes les Commandes doivent être déclarées au sein du Domain (Domain.Commands)

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

[Fact]
public void CommandsShouldBePartOfDomain() =>
    Classes().That()
        .ImplementInterface(typeof(ICommand))
        .Or()
        .HaveNameEndingWith("Command").Should()
        .ResideInNamespace("Domain.Commands", true)
        .Check();

Nouveau rapport SonarCloud disponible ici.

Reflect

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

  • Quel impact ce refactoring a eu ?

Step 10 : "Avoid Primitives" - Commands
Transform parameters
Configure Transform Parameters
Failure
Usages