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