Validation de domaine dans une architecture CQRS
Danger ... Danger Dr Smith... Philosophique post d'avance
le but de ce post est de déterminer si placer la logique de validation en dehors de mes entités de domaine (agréger root en fait) me donne réellement plus de flexibilité ou c'est code kamikaze
en gros je veux savoir si il ya une meilleure façon de valider mon domaine entités. C'est de cette façon j'ai l'intention de le faire, mais j'aimerais avoir votre avis
La première l'approche que j'en considération est:
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 lorsque je suis en train d'encapsuler la logique de validation dans la bonne entité, cela viole le principe D'ouverture/fermeture (Open for extension but Close for modification) et j'ai constaté que violer ce principe, la maintenance du code devient une vraie douleur quand l'application grandit en complexité. Pourquoi? Parce que les règles de domaine changent plus souvent que nous aimerions l'admettre, et si les règles sont cachés et embedded dans une entité comme celle-ci, ils sont difficiles à tester, difficiles à lire, difficiles à maintenir mais la vraie raison pour laquelle je n'aime pas cette approche est: si les règles de validation changent, je dois venir éditer mon entité de domaine. Ceci a été un exemple très simple mais dans RL la validation pourrait être plus complexe
donc en suivant la philosophie D'Udi Dahan, rendre les rôles explicites, et la recommandation D'Eric Evans dans le livre bleu, la prochaine essayez de mettre en œuvre la spécification du modèle, quelque chose comme ceci
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
public bool IsSatisfiedBy(Customer customer)
{
return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
}
}
mais alors je me rends compte que pour suivre cette approche j'ai dû muter mes entités d'abord pour passer le valeur étant valdiée, dans ce cas l'e-mail, mais leur mutation provoquerait le déclenchement de mes événements de domaine que je n'aimerais pas voir se produire jusqu'à ce que le nouvel e-mail soit valide
donc après avoir considéré ces approches, je suis sorti avec celle-ci, puisque je vais aller pour 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(“...”);
}
}
Eh bien c'est l'idée principale, l'entity est passé au validateur au cas où nous aurions besoin d'une valeur de l'entity pour effectuer la validation, la commande contient les données provenant de l'utilisateur et puisque les validateurs sont considérés injectable objets ils peuvent avoir des dépendances externes injectées si la validation l'exige.
Maintenant le dilemme je suis heureux, avec une conception de ce genre parce que ma validation est encapsulée dans des objets individuels ce qui apporte de nombreux avantages: Test unitaire facile, facile à maintenir, les invariants de domaine sont explicitement exprimés en utilisant le langage ubiquitaire, facile à étendre, la logique de validation est centralisée et les validateurs peuvent être utilisés ensemble 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'eux (vous pourriez argumenter une odeur de code - domaine anémique) mais je pense que le compromis est acceptable
mais il y a une chose que je n'ai pas compris comment la mettre en œuvre d'une manière propre. comment utiliser ces composants...
Puisqu'ils seront injectés, ils ne s'intégreront pas naturellement à l'intérieur de mes entités du domaine, donc en gros je vois deux options:
passer les validateurs à chaque méthode de mon entity
valider mes objets extérieurement (à partir de la commande manipulateur)
Je ne suis pas satisfait de l'option 1 alors j'expliquerais comment je le ferais 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();
}
}
}
Eh bien voilà. Pouvez-vous me donner vos idées à ce sujet ou partager vos expériences avec Domain entities validation
EDIT
je pense que ce n'est pas clair de ma question, mais le vrai problème est: Cacher les règles de domaine a de sérieuses implications dans la maintenabilité future de l'application, et aussi les règles de domaine changent souvent pendant le cycle de vie de l'application. Par conséquent, les mettre en œuvre dans cet esprit nous permettrait de les étendre facilement. Maintenant imaginez dans le futur un moteur de règles est mis en œuvre, si les règles sont encapsulées en dehors des entités de domaine, ce changement serait plus facile à mettre en œuvre
je suis conscient que 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 la validation dans des objets individuels est beaucoup plus importante que la simple conservation de l'encapsulation d'une entité. Maintenant je pense que l'encapsulation a plus de sens dans une architecture n-tier traditionnelle parce que les entités ont été utilisées dans plusieurs endroits de la couche de domaine, mais dans une architecture CQRS, quand une commande arrive, il y aura un handler de commande accédant à une racine d'agrégat et effectuant des opérations contre la racine d'agrégat créant seulement une fenêtre parfaite pour placer le validation.
j'aimerais faire une petite comparaison entre les avantages de placer la validation à l'intérieur d'une entité vs la placer dans des objets individuels
Validation dans les différents objets
- Pro. Facile à écrire
- Pro. Facile à tester
- Pro. Il est explicitement exprimé
- Pro. Il devient partie intégrante de la conception du domaine, exprimée avec le langage omniprésent actuel
- Pro. Depuis il fait maintenant partie de la conception, il peut être modélisé en utilisant des diagrammes UML
- Pro. Extrêmement facile à entretenir
- Pro. Rend mes entities et la logique de validation vaguement couplées
- Pro. Facile à étendre
- Pro. En suivant le SRP
- Pro. Suite à l'ouverture/fermeture principe
- Pro. Vous n'enfreignez pas la loi de Demeter?
- Pro. Je c'est centralisée
- 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 plug-in, de nouveaux validateurs peuvent être ajoutés simplement en abandonnant les nouveaux assemblages sans qu'il soit nécessaire de recompiler toute l'application
- Pro. La mise en place d'un moteur de règles serait plus facile
- Conditionné. Casser l'encapsulation
- con. Si l'encapsulation est obligatoire, nous devrions passer les validateurs individuels à l'entité (agrégat) méthode
Validation encapsulée à l'intérieur de l'entité
- Pro. Encapsulé?
- Pro. Réutilisable?
je serais ravi de lire vos réflexions à ce sujet
11 réponses
je suis d'accord avec un certain nombre de concepts présentés dans d'autres réponses, mais je les ai mis dans mon code.
tout d'abord, je suis d'accord que l'utilisation D'objets de valeur pour les valeurs qui incluent le comportement est un excellent moyen d'encapsuler des règles commerciales communes et une adresse e-mail est un candidat parfait. Cependant, j'ai tendance à limiter cela à des règles qui sont constantes et qui ne changeront pas fréquemment. Je suis sûr que vous êtes à la recherche d'une approche plus générale et l'e-mail est juste un exemple, donc je ne me concentrerai pas sur cela un cas d'utilisation.
la clé de mon approche est de reconnaître que la validation sert à des fins différentes à différents endroits dans une application. En termes simples, validez seulement ce qui est requis pour s'assurer que l'opération actuelle peut être exécutée sans résultats inattendus ou involontaires. Cela nous amène à la question de savoir quelle validation devrait avoir lieu où?
dans votre exemple, je me demanderais si l'entité du domaine se soucie vraiment que l'adresse e-mail soit conforme à un modèle et un autre règles ou est-ce que nous nous soucions simplement que "email" ne peut pas être null ou blanc quand ChangeEmail est appelé? Si ce dernier, qu'un simple contrôle pour s'assurer qu'une valeur est présente est tout ce qui est nécessaire dans la méthode ChangeEmail.
dans CQRS, tous les changements qui modifient l'état de l'application se produisent sous forme de commandes avec l'implémentation dans les handlers de commandes (comme vous l'avez montré). En général, je place les "crochets" dans les règles d'affaires, etc. qui valident que l'opération peut être effectuée dans la commande manipulateur. Je suis en fait votre approche d'injecter des validateurs dans le gestionnaire de commandes qui me permet d'étendre / remplacer le jeu de règles sans apporter de changements au gestionnaire. Ces règles "dynamiques" me permettent de définir les règles d'affaires, telles que ce qui constitue une adresse e-mail valide, avant de changer l'état de l'entité - en veillant à ce qu'elle n'entre pas dans un état invalide. Mais la "nullité" dans ce cas est défini par la logique et, comme vous l'avez souligné, est très volitile.
ayant gravi les échelons de la CSLA, j'ai trouvé ce changement difficile à adopter parce qu'il semble briser l'encapsulation. Mais, je suis d'accord que l'encapsulation n'est pas brisée si vous prenez un peu de recul et demandez quel rôle la validation sert vraiment dans le modèle.
j'ai trouvé ces nuances très importantes pour garder la tête froide sur ce sujet. Il y a une validation pour empêcher les mauvaises données (par exemple les arguments manquants, les valeurs nulles, les chaînes vides, etc.) qui appartiennent à la méthode elle-même et il y a validation pour s'assurer que les règles d'affaires sont appliquées. Dans le premier cas, si le client doit avoir une adresse e-mail, alors la seule règle que je dois être préoccupé pour empêcher mon objet de domaine de devenir invalide est de s'assurer qu'une adresse e-mail a été fournie à la méthode ChangeEmail. Les autres règles sont des préoccupations quant à la validité de la valeur elle-même et n'ont pas vraiment d'incidence sur la validité de l'entité de domaine m'.
cela a été la source de beaucoup de 'discussions' avec d'autres développeurs, mais lorsque la plupart d'entre eux adoptent une vision plus large et examinent le rôle que joue réellement la validation, ils ont tendance à voir la lumière.
enfin, il y a aussi une place pour la validation de L'UI (et par L'UI j'entends ce qui sert d'interface à l'application que ce soit un écran, un point de service ou quoi que ce soit). Je trouve qu'il est parfaitement raisonnable de dupliquer une partie de la logique dans l'INTERFACE utilisateur de fournir une meilleure interactivité pour l'utilisateur. Mais c'est parce que cette validation sert ce but unique que j'autorise une telle duplication. Cependant, l'utilisation d'objets de validation/spécification injectés favorise la réutilisation de cette façon sans les implications négatives de ces règles définies dans plusieurs endroits.
Je ne suis pas sûr que ça aide ou pas...
Je ne suggérerais pas de graver de gros morceaux de code dans votre domaine pour validation. Nous avons éliminé la plupart de nos validations mal placées en les voyant comme une odeur de concepts manquants dans notre domaine. Dans votre exemple de code que vous écrivez je vois validation pour une adresse e-mail. Un client n'a rien à voir avec la validation d'un courriel.
Pourquoi ne pas faire un ValueObject
Email
qui ne cette validation à construire?
D'après mon expérience, les validations mal placées sont des indices de concepts manqués dans votre domaine. Vous pouvez les attraper dans les objets Validator, mais je préfère objet value parce que vous faites le concept associé partie de votre domaine.
vous avez mis la validation au mauvais endroit.
vous devriez utiliser des objets de valeur pour de telles choses. Regardez cette présentation!3-->http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson Il vous enseignera également sur les données en tant que Centres de gravité.
il y a aussi un échantillon de la façon de réutiliser la validation des données, comme par exemple en utilisant des méthodes de validation statique ala Email.IsValid (string)
je suis au début d'un projet et je vais implémenter ma validation en dehors de mes entités de domaine. Les entités de mon domaine contiennent de la logique pour protéger les invariants (comme les arguments manquants, les valeurs nulles, les chaînes vides, les collections, etc.). Mais les règles d'affaires réelles vivront dans des classes de validateur. Je suis de l'état d'esprit de @SonOfPirate...
j'utilise FluentValidation cela me donnera essentiellement un tas de validateurs qui agissent sur mes entités de domaine: alias, le modèle de spécification. En outre, conformément aux modèles décrits dans le livre bleu D'Eric, je peux construire les validateurs avec toutes les données dont ils peuvent avoir besoin pour effectuer les validations (que ce soit à partir de la base de données ou d'un autre dépôt ou service). Je voudrais aussi avoir la possibilité d'injecter des dépendances ici aussi. Je peux également composer et réutiliser ces validateurs (p. ex. un validateur d'adresse peut être réutilisé à la fois dans un validateur D'employé et dans un validateur D'entreprise). J'ai une usine de validateur qui agit comme un "localisateur de service":
public class ParticipantService : IParticipantService
{
public void Save(Participant participant)
{
IValidator<Participant> validator = _validatorFactory.GetValidator<Participant>();
var results = validator.Validate(participant);
//if the participant is valid, register the participant with the unit of work
if (results.IsValid)
{
if (participant.IsNew)
{
_unitOfWork.RegisterNew<Participant>(participant);
}
else if (participant.HasChanged)
{
_unitOfWork.RegisterDirty<Participant>(participant);
}
}
else
{
_unitOfWork.RollBack();
//do some thing here to indicate the errors:generate an exception (or fault) that contains the validation errors. Or return the results
}
}
}
et le validateur contiendrait du code, quelque chose comme ceci:
public class ParticipantValidator : AbstractValidator<Participant>
{
public ParticipantValidator(DateTime today, int ageLimit, List<string> validCompanyCodes, /*any other stuff you need*/)
{...}
public void BuildRules()
{
RuleFor(participant => participant.DateOfBirth)
.NotNull()
.LessThan(m_today.AddYears(m_ageLimit*-1))
.WithMessage(string.Format("Participant must be older than {0} years of age.", m_ageLimit));
RuleFor(participant => participant.Address)
.NotNull()
.SetValidator(new AddressValidator());
RuleFor(participant => participant.Email)
.NotEmpty()
.EmailAddress();
...
}
}
nous devons prendre en charge plus d'un type de présentation: sites Web, winforms et chargement en vrac de données via des services. Sous épingler tous ceux-ci sont un ensemble de services qui exposent la fonctionnalité du système d'une manière unique et cohérente. Nous n'utilisons pas Entity Framework ou ORM pour des raisons que je ne vous ennuierai pas.
Voici pourquoi j'aime ça approche:
- les règles d'affaires qui sont contenues dans les validateurs sont entièrement vérifiables à l'unité.
- je peux composer des règles plus complexes à partir de règles plus simples
- je peux utiliser les validateurs à plus d'un endroit dans mon système (nous supportons les sites Web et Winforms, et les services qui exposent la fonctionnalité), donc s'il y a une règle légèrement différente requise pour un cas d'utilisation dans un service qui diffère des sites web, alors je peux gérer que.
- toute la vaildation est exprimée en un seul endroit et je peux choisir comment / où injecter et composer ceci.
Je n'appellerais pas une classe qui hérite de EntityBase
mon modèle de domaine puisqu'il l'associe à votre couche de persistance. Mais c'est juste mon avis.
Je ne déplacerais pas la logique de validation du courriel de Customer
de rien d'autre pour suivre le Ouvert/Fermé principe. Pour moi, suivre open / closer signifierait que vous avez la hiérarchie suivante:
public class User
{
// some basic validation
public virtual void ChangeEmail(string email);
}
public class Employee : User
{
// validates internal email
public override void ChangeEmail(string email);
}
public class Customer : User
{
// validate external email addresses.
public override void ChangeEmail(string email);
}
vos suggestions déplace le contrôle du modèle de domaine à une classe arbitraire, donc brisant le encapsulation. Je préfère refactoriser ma classe (Customer
) pour se conformer aux nouvelles règles commerciales que de le faire.
utilisez les événements de domaine pour déclencher d'autres parties du système pour obtenir une architecture plus lâche, mais n'utilisez pas les commandes/événements pour violer l'encapsulation.
Exceptions
je viens de remarquer que vous jetez DomainException
. C'est une façon de générique exception. Pourquoi n'utilisez-vous pas l'argument exceptions ou le