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 :

  • 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

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 génère un nouvel overload

public static PartieDeChasse Create(Func<DateTime> timeProvider, DemarrerPartieDeChasse demarrerPartieDeChasse)
{
    throw new NotImplementedException();
}
 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
    );
}
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()
            )
);

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();

Reflect

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

  • Quel impact ce refactoring a eu ?

Prendre du temps pour comprendre ce qu'est le principe

Pour en savoir plus sur le choix de placer les Commands dans le Domain, je vous invite à lire de

On utiliser le pour refactorer notre code

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 peut supprimer de manière totalement safe la méthode "étranglée"

Nouveau rapport SonarCloud disponible .

🔴
🟢
🔵
🔵
Avoid Primitive Types
ce super article
Vladimir Khorikov
Strangler Pattern
ici
Step 10 : "Avoid Primitives" - Commands
Transform parameters
Configure Transform Parameters
Failure
Usages