8) Use Cases
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 :
Prendre du temps pour comprendre ce qu'est la
Clean ArchitectureNotamment la notion de
Use Case
Extraire 1
Use Casepar méthode duServiceAméliorer la définition de notre architecture via nos tests
Archunit
Extraire un premier Use Case
Utiliser les fonctionnalités de notre
IDEpour extraire la méthodeDémarrerRefactor->Extract->Extract ClassPenser à 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
PartieDeChasseServicedélègue maintenant les appels de la méthodeDémarrerà notreUse CaseAinsi, 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->ScenarioTestsNous 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
PartieDeChasseServicede manière totalementsafe

On place l'ensemble des
Exceptionsau plus proche de leur utilisation (dans le namespaceUseCases)

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.
Nouveau rapport SonarCloud disponible ici.
Reflect
Quel est l'impact sur le design ? les tests ?
En quoi pouvons nous parler ici de
Screaming Architecture?

Last updated
Was this helpful?