Avec toutes les découvertes réalisées jusqu'à présent on a pu se rendre compte que l'architecture désirée était une architecture en Onion
:
On va s'assurer que le code actuel respecte le Design escompté :
Prendre du temps pour comprendre ce que sont des
Ecrire des tests d'architecture en utilisant la librairie
Pour aller plus vite, voici une classe contenant des extensions facilitant l'écriture et le lancement de tels tests :
Copy using ArchUnitNET.Fluent;
using ArchUnitNET.Fluent.Syntax.Elements.Types;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Bouchonnois.Service;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
namespace Bouchonnois.Tests.Architecture
{
public static class ArchUnitExtensions
{
private static readonly ArchUnitNET.Domain.Architecture Architecture =
new ArchLoader()
.LoadAssemblies(typeof(PartieDeChasseService).Assembly)
.Build();
public static GivenTypesConjunction TypesInAssembly() =>
Types().That().Are(Architecture.Types);
public static void Check(this IArchRule rule) => rule.Check(Architecture);
}
// Exemple de test
public class Guidelines
{
private static GivenMethodMembersThat Methods() => MethodMembers().That().AreNoConstructors().And();
[Fact]
public void NoGetMethodShouldReturnVoid() =>
Methods()
.HaveName("Get[A-Z].*", useRegularExpressions: true).Should()
.NotHaveReturnType(typeof(void))
.Check();
}
}
Inward Dependencies
On va valider le sens des dépendances en ajoutant une nouvelle classe de tests
Copy public class ArchitectureRules
{
[Fact]
public void ApplicationServicesRules() =>
{
// Les classes dans l'Application Services ne devraient pas dépendre de classes dans Infrastructure
}
[Fact]
public void InfrastructureRules()
{
// Quelles sont les classes de l'infrastructure ?
// Que devrions nous faire de ce qui est contenu dans Infra ?
}
[Fact]
public void DomainModelRules()
{
// Les classes dans Domain ne devraient pas dépendre de classes dans Infrastructure ou Application Services
}
}
On définit les couches de notre onion
:
Copy private static GivenTypesConjunctionWithDescription ApplicationServices() =>
TypesInAssembly().And()
.ResideInNamespace("Service", true)
.As("Application Services");
private static GivenTypesConjunctionWithDescription DomainModel() =>
TypesInAssembly().And()
.ResideInNamespace("Domain", true)
.As("Domain Model");
private static GivenTypesConjunctionWithDescription Infrastructure() =>
TypesInAssembly().And()
.ResideInNamespace("Repository", true)
.As("Infrastructure");
Domain Model
On peut alors écrire une première règle pour notre Domain Model
:
Copy [Fact]
public void DomainModelRules() =>
DomainModel().Should()
.NotDependOnAny(ApplicationServices()).AndShould()
.NotDependOnAny(Infrastructure())
.Check();
Celle-ci échoue :
La classe Terrain
se trouve dans l'Application Services alors qu'elle est une entité à part entière du Domain
...
On corrige cela en déplaçant la classe :
Règle de l'Application Services
On en profite pour implémenter une règle sur l'Application Service :
Copy [Fact]
public void ApplicationServicesRules() =>
Infrastructure().Should()
.NotDependOnAny(Infrastructure())
.Check();
Quid de l'infrastructure ?
Pour le moment nous n'avons qu'une interface de Repository
(Un Port) au sein du namespace Infrastructure
.
Est-ce que cela fait du sens au regard de la règle de dépendance ?
Nous allons déplacer ce port
dans le Domain
.
Nous pouvons tout de même implémenter une règle spécifiant que les items présents dans le namespace Repository
doit implémenter l'interface IPartieDeChasseRepository
:
Copy [Fact]
public void InfrastructureRules() =>
Infrastructure().Should()
.ImplementInterface(typeof(IPartieDeChasseRepository))
.Check();
Règles d'équipe
On peut ajouter certaines règles d'équipe du genre :
Toutes les interfaces doivent commencer par I
Une méthode commençant par Get
doit retourner quelque chose
Copy public class Guidelines
{
private static GivenMethodMembersThat Methods() => MethodMembers().That().AreNoConstructors().And();
[Fact]
public void NoGetMethodShouldReturnVoid() =>
Methods()
.HaveName("Get[A-Z].*", useRegularExpressions: true).Should()
.NotHaveReturnType(typeof(void))
.Check();
[Fact]
public void IserAndHaserShouldReturnBooleans() =>
Methods()
.HaveName("Is[A-Z].*", useRegularExpressions: true).Or()
.HaveName("Has[A-Z].*", useRegularExpressions: true).Should()
.HaveReturnType(typeof(bool))
.Check();
[Fact]
public void SettersShouldNotReturnSomething() =>
Methods()
.HaveName("Set[A-Z].*", useRegularExpressions: true).Should()
.HaveReturnType(typeof(void))
.Check();
[Fact]
public void InterfacesShouldStartWithI() =>
Interfaces().Should()
.HaveName("^I[A-Z].*", useRegularExpressions: true)
.Because("C# convention...")
.Check();
}
Reflect
A quoi cette technique pourrait vous servir ?
Quelles règles pourraient être utiles dans votre quotidien ?