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 Architecture
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
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
Copy 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
Copy 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
Copy 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 :
Copy 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
Copy 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 PartieDeChasseService
de manière totalement safe
On place l'ensemble des Exceptions
au plus proche de leur utilisation (dans le namespace UseCases
)
Notre architecture ressemble maintenant à ça
Mise à jour des règles ArchUnit
Copy 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
?