Danger... Danger Dr. Smith... Poste philosophique à venir
L'objectif de cet article est de déterminer si le fait de placer la logique de validation en dehors de mes entités de domaine (racine d'agrégat en fait) m'apporte plus de flexibilité ou si c'est code kamikaze
En fait, je veux savoir s'il existe un meilleur moyen de valider mes entités de domaine. Voici comment je compte procéder, mais j'aimerais avoir votre avis.
La première approche que j'ai envisagée est la suivante :
class Customer : EntityBase<Customer>
{
public void ChangeEmail(string email)
{
if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”);
if(!email.IsEmail()) throw new DomainException();
if(email.Contains(“@mailinator.com”)) throw new DomainException();
}
}
En fait, je n'aime pas cette validation parce que même si j'encapsule la logique de validation dans l'entité correcte, cela viole le principe d'ouverture/fermeture (ouvert pour l'extension mais fermé pour la modification) et j'ai constaté qu'en violant ce principe, la maintenance du code devient un véritable casse-tête lorsque l'application devient plus complexe. Pourquoi ? Parce que les règles du domaine changent plus souvent qu'on ne veut bien l'admettre, et si les règles sont caché et intégré dans une entité comme celle-ci, elles sont difficiles à tester, difficiles à lire, difficiles à maintenir mais la vraie raison pour laquelle je n'aime pas cette approche est la suivante : si les règles de validation changent, je dois venir modifier l'entité de mon domaine. Il s'agit d'un exemple très simple, mais la validation pourrait être plus complexe dans RL.
Donc en suivant la philosophie de Udi Dahan, rendre les rôles explicites et la recommandation d'Eric Evans dans le livre bleu, la tentative suivante a été d'implémenter le modèle de spécification, quelque chose comme ceci
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
public bool IsSatisfiedBy(Customer customer)
{
return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
}
}
Mais je me rends compte que pour suivre cette approche, je devais d'abord faire muter mes entités afin de passer l'étape de l'échange de données. valeur en cours d'évaluation dans ce cas, l'e-mail, mais leur mutation entraînerait le déclenchement d'événements relatifs à mon domaine, ce que je ne voudrais pas voir se produire avant que le nouvel e-mail ne soit valide.
Après avoir examiné ces approches, j'ai choisi celle-ci, puisque je vais mettre en œuvre une architecture CQRS :
class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
public void IsValid(Customer entity, ChangeEmailCommand command)
{
if(!command.Email.HasValidDomain()) throw new DomainException(“...”);
}
}
C'est l'idée principale, l'entité est transmise au validateur au cas où nous aurions besoin d'une valeur de l'entité pour effectuer la validation, la commande contient les données provenant de l'utilisateur et comme les validateurs sont considérés comme étant injectable ils peuvent avoir des dépendances externes injectées si la validation l'exige.
Maintenant, le dilemme Je suis satisfait d'une telle conception car ma validation est encapsulée dans des objets individuels, ce qui présente de nombreux avantages : test unitaire facile, maintenance facile, invariants de domaine explicitement exprimés à l'aide du langage omniprésent, extension facile, logique de validation centralisée et utilisation conjointe de validateurs pour appliquer des règles de domaine complexes. Et même si je sais que je place la validation de mes entités en dehors d'elles (on pourrait parler d'une odeur de code - domaine anémique), je pense que le compromis est acceptable.
Mais il y a une chose que je n'ai pas trouvée : comment l'implémenter de manière propre. Comment dois-je utiliser ces composants...
Comme ils seront injectés, ils ne s'inscriront pas naturellement dans les entités de mon domaine, et je vois donc deux options :
-
Passer les validateurs à chaque méthode de mon entité
-
Valider mes objets en externe (depuis le gestionnaire de commandes)
Je ne suis pas satisfait de l'option 1. Je vais donc expliquer comment je vais procéder avec l'option 2.
class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
// here I would get the validators required for this command injected
private IEnumerable<IDomainInvariantValidator> validators;
public void Execute(ChangeEmailCommand command)
{
using (var t = this.unitOfWork.BeginTransaction())
{
var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
// here I would validate them, something like this
this.validators.ForEach(x =. x.IsValid(customer, command));
// here I know the command is valid
// the call to ChangeEmail will fire domain events as needed
customer.ChangeEmail(command.Email);
t.Commit();
}
}
}
Voilà, c'est fait. Pouvez-vous me faire part de vos réflexions à ce sujet ou partager vos expériences avec la validation des entités du Domaine
EDIT
Je pense que ma question n'est pas claire, mais le vrai problème est le suivant : le fait de cacher les règles du domaine a de sérieuses implications sur la maintenabilité future de l'application, et les règles du domaine changent souvent au cours du cycle de vie de l'application. Par conséquent, les mettre en œuvre en gardant cela à l'esprit nous permettrait de les étendre facilement. Imaginons maintenant qu'un moteur de règles soit implémenté à l'avenir. Si les règles sont encapsulées en dehors des entités du domaine, ce changement sera plus facile à mettre en œuvre.
Je suis conscient que le fait de placer la validation en dehors de mes entités brise l'encapsulation comme @jgauffin l'a mentionné dans sa réponse, mais je pense que les avantages de placer la validation dans des objets individuels sont beaucoup plus substantiels que de simplement conserver l'encapsulation d'une entité. Maintenant, je pense que l'encapsulation a plus de sens dans une architecture traditionnelle n-tier parce que les entités ont été utilisées à plusieurs endroits de la couche de domaine, mais dans une architecture CQRS, quand une commande arrive, il y aura un gestionnaire de commande accédant à une racine agrégée et effectuant des opérations sur la racine agrégée, créant ainsi une fenêtre parfaite pour placer la validation.
J'aimerais faire une petite comparaison entre les avantages de placer la validation dans une entité et ceux de la placer dans des objets individuels.
-
Validation dans les objets individuels
- Pro. Facile à écrire
- Pro. Facile à tester
- Pour. C'est explicitement exprimé
- Pro. Il fait partie de la conception du domaine, exprimée par le langage omniprésent actuel.
- Pro. Puisqu'il fait maintenant partie de la conception, il peut être modélisé à l'aide de diagrammes UML.
- Pro. Extrêmement facile à entretenir
- Pro. Cela rend mes entités et la logique de validation faiblement couplées.
- Pro. Facile à étendre
- Pro. Après l'ASR
- Pro. Suivre le principe d'ouverture/fermeture
- Pro. Ne pas enfreindre la loi de Déméter (mmm) ?
- Pro. I'est centralisé
- Pro. Il pourrait être réutilisable
- Pro. Si nécessaire, des dépendances externes peuvent être facilement injectées.
- Pro. Si vous utilisez un modèle de plug-in, vous pouvez ajouter de nouveaux validateurs en déposant simplement les nouveaux assemblages sans avoir à recompiler l'ensemble de l'application.
- Pro. L'implémentation d'un moteur de règles serait plus facile
- Con. Rupture de l'encapsulation
- Con. Si l'encapsulation est obligatoire, nous devrions passer les validateurs individuels à la méthode d'entité (agrégat).
-
Validation encapsulée dans l'entité
- Pro. Encapsulé ?
- Pro. Réutilisable ?
J'aimerais lire vos réflexions à ce sujet