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