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:

  1. passer les validateurs à chaque méthode de mon entity

  2. 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

43
demandé sur Jupaol 2012-06-04 13:54:20
la source

11 ответов

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

11
répondu SonOfPirate 2012-06-11 17:31:01
la source

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 ValueObjectEmail 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.

7
répondu pjvds 2012-06-06 10:08:37
la source

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)

5
répondu Yevhen Bobrov 2012-06-06 23:44:10
la source

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.
5
répondu RobertMS 2012-06-12 19:05:28
la source

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

Je ne peux pas dire que ce que j'ai fait est la chose parfaite à faire car je suis encore aux prises avec ce problème moi-même et je me bats un combat à la fois. Mais je l'ai fait jusqu'à présent la chose suivante :

j'ai des classes de base pour l'encapsulation de validation :

public interface ISpecification<TEntity> where TEntity : class, IAggregate
    {
        bool IsSatisfiedBy(TEntity entity);
    }

internal class AndSpecification<TEntity> : ISpecification<TEntity> where TEntity: class, IAggregate
    {
        private ISpecification<TEntity> Spec1;
        private ISpecification<TEntity> Spec2;

        internal AndSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
        {
            Spec1 = s1;
            Spec2 = s2;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return Spec1.IsSatisfiedBy(candidate) && Spec2.IsSatisfiedBy(candidate);
        }


    }

    internal class OrSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
    {
        private ISpecification<TEntity> Spec1;
        private ISpecification<TEntity> Spec2;

        internal OrSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
        {
            Spec1 = s1;
            Spec2 = s2;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return Spec1.IsSatisfiedBy(candidate) || Spec2.IsSatisfiedBy(candidate);
        }
    }

    internal class NotSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
    {
        private ISpecification<TEntity> Wrapped;

        internal NotSpecification(ISpecification<TEntity> x)
        {
            Wrapped = x;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return !Wrapped.IsSatisfiedBy(candidate);
        }
    }

    public static class SpecsExtensionMethods
    {
        public static ISpecification<TEntity> And<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
        {
            return new AndSpecification<TEntity>(s1, s2);
        }

        public static ISpecification<TEntity> Or<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
        {
            return new OrSpecification<TEntity>(s1, s2);
        }

        public static ISpecification<TEntity> Not<TEntity>(this ISpecification<TEntity> s) where TEntity : class, IAggregate
        {
            return new NotSpecification<TEntity>(s);
        }
    }

et pour l'utiliser, je ne les suivants :

gestionnaire de commande :

 public class MyCommandHandler :  CommandHandler<MyCommand>
{
  public override CommandValidation Execute(MyCommand cmd)
        {
            Contract.Requires<ArgumentNullException>(cmd != null);

           var existingAR= Repository.GetById<MyAggregate>(cmd.Id);

            if (existingIntervento.IsNull())
                throw new HandlerForDomainEventNotFoundException();

            existingIntervento.DoStuff(cmd.Id
                                , cmd.Date
                                ...
                                );


            Repository.Save(existingIntervento, cmd.GetCommitId());

            return existingIntervento.CommandValidationMessages;
        }

total :

 public void DoStuff(Guid id, DateTime dateX,DateTime start, DateTime end, ...)
        {
            var is_date_valid = new Is_dateX_valid(dateX);
            var has_start_date_greater_than_end_date = new Has_start_date_greater_than_end_date(start, end);

        ISpecification<MyAggregate> specs = is_date_valid .And(has_start_date_greater_than_end_date );

        if (specs.IsSatisfiedBy(this))
        {
            var evt = new AgregateStuffed()
            {
                Id = id
                , DateX = dateX

                , End = end        
                , Start = start
                , ...
            };
            RaiseEvent(evt);
        }
    }

la spécification est maintenant incorporé dans ces deux classes :

public class Is_dateX_valid : ISpecification<MyAggregate>
    {
        private readonly DateTime _dateX;

        public Is_data_consuntivazione_valid(DateTime dateX)
        {
            Contract.Requires<ArgumentNullException>(dateX== DateTime.MinValue);

            _dateX= dateX;
        }

        public bool IsSatisfiedBy(MyAggregate i)
        {
            if (_dateX> DateTime.Now)
            {
                i.CommandValidationMessages.Add(new ValidationMessage("datex greater than now"));
                return false;
            }

            return true;
        }
    }

    public class Has_start_date_greater_than_end_date : ISpecification<MyAggregate>
    {
        private readonly DateTime _start;
        private readonly DateTime _end;

        public Has_start_date_greater_than_end_date(DateTime start, DateTime end)
        {
            Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);
            Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);

            _start = start;
            _end = end;
        }

        public bool IsSatisfiedBy(MyAggregate i)
        {
            if (_start > _end)
            {
                i.CommandValidationMessages.Add(new ValidationMessage(start date greater then end date"));
                return false;
            }

            return true;
        }
    }

cela me permet de réutiliser certaines validations pour différents agrégats et c'est facile à tester. Si vous voyez des flux. Je serais heureux d'en discuter.

le vôtre,

2
répondu Arthis 2012-06-04 18:02:55
la source

d'après mon expérience OO (Je ne suis pas un expert DDD) déplacer votre code de l'entité vers un niveau d'abstraction supérieur (dans un gestionnaire de commandes) causera une duplication de code. Cela est dû au fait que chaque fois qu'un gestionnaire de commandes reçoit une adresse e-mail, il doit instancier des règles de validation e-mail. Ce genre de code va pourrir au bout d'un moment, et il va sentir très mauvais. Dans l'exemple actuel, il ne pourrait pas, si vous n'avez pas d'autre commande qui change l'adresse e-mail, mais dans d'autres cas, sûrement sera...

si vous ne voulez pas ramener les règles à un niveau d'abstraction plus bas, comme l'objet entity ou un objet email value, alors je vous suggère fortement de réduire la douleur en regroupant les règles. Ainsi, dans votre exemple de courriel, les 3 règles suivantes:

  if(string.IsNullOrWhitespace(email))   throw new DomainException(“...”);
  if(!email.IsEmail())  throw new DomainException();
  if(email.Contains(“@mailinator.com”))  throw new DomainException();

peut-être partie d'un EmailValidationRule groupe que vous pouvez réutiliser plus facile.

de mon point de vue, il n'y a pas de réponse explicite à la question Où mettre la logique de validation. Il peut faire partie de chaque objet en fonction du niveau d'abstraction. Dans votre cas actuel, la vérification formelle de l'adresse e-mail peut faire partie d'un EmailValueObject et mailinator la règle peut faire partie d'un concept de niveau d'abstraction supérieur dans lequel vous déclarez que votre utilisateur ne peut pas avoir une adresse e-mail pointant sur ce domaine. Par exemple, si quelqu'un veut entrer en contact avec votre utilisateur sans inscription, vous pouvez vérifier son e-mail par rapport à la validation formelle, mais vous n'avez pas à vérifier son e-mail par rapport à la mailinator la règle. Et ainsi de sur...

donc je suis tout à fait d'accord avec @pjvds qui a affirmé que ce genre de validation mal placée est un signe d'une mauvaise conception. Je ne pense pas que tu gagneras à briser l'encapsulation, mais c'est ton choix et ce sera ta douleur.

1
répondu inf3rno 2014-09-25 07:02:07
la source

la validation dans votre exemple est la validation d'un objet value, pas d'une entité (ou d'une racine agrégée).

Je séparerais la validation en zones distinctes.

  1. valider les caractéristiques internes du Email valeur objet interne.

j'adhère à la règle selon laquelle les agrégats ne doivent jamais être dans un état invalide. J'étend ce principe à la valeur des objets lorsque cela est possible.

Utiliser createNew() pour instancier un email à partir de la saisie de l'utilisateur. Cela l'oblige à être valide selon vos règles actuelles (le "[email protected]" format, par exemple).

Utiliser createExisting() pour instancier un e-mail de stockage persistant. Ceci n'effectue aucune validation, ce qui est important - vous ne voulez pas qu'une exception soit lancée pour un email stocké qui était valide hier mais invalide aujourd'hui.

class Email
{
    private String value_;

    // Error codes
    const Error E_LENGTH = "An email address must be at least 3 characters long.";
    const Error E_FORMAT = "An email address must be in the '[email protected]' format.";

    // Private constructor, forcing the use of factory functions
    private Email(String value)
    {
        this.value_ = value;
    }

    // Factory functions
    static public Email createNew(String value)
    {
        validateLength(value, E_LENGTH);
        validateFormat(value, E_FORMAT);
    }

    static public Email createExisting(String value)
    {
        return new Email(value);
    }

    // Static validation methods
    static public void validateLength(String value, Error error = E_LENGTH)
    {
        if (value.length() < 3)
        {
            throw new DomainException(error);
        }
    }

    static public void validateFormat(String value, Error error = E_FORMAT)
    {
        if (/* regular expression fails */)
        {
            throw new DomainException(error);
        }
    }

}
  1. valider les caractéristiques "externes" de la Email valeur externe de l'objet, p.ex. dans un service.

    class EmailDnsValidator implements IEmailValidator
    {
        const E_MX_MISSING = "The domain of your email address does not have an MX record.";
    
        private DnsProvider dnsProvider_;
    
        EmailDnsValidator(DnsProvider dnsProvider)
        {
            dnsProvider_ = dnsProvider;
        }
    
        public void validate(String value, Error error = E_MX_MISSING)
        {
            if (!dnsProvider_.hasMxRecord(/* domain part of email address */))
            {
                throw new DomainException(error);
            }
        }
    }
    
    class EmailDomainBlacklistValidator implements IEmailValidator
    {
        const Error E_DOMAIN_FORBIDDEN = "The domain of your email address is blacklisted.";
    
        public void validate(String value, Error error = E_DOMAIN_FORBIDDEN)
        {
            if (/* domain of value is on the blacklist */))
            {
                throw new DomainException(error);
            }
        }
    }
    

les Avantages:

  • Utilisation du createNew() et createExisting() les fonctions d'usine permettent le contrôle de la validation interne.

  • il est possible de "se retirer" de certaines routines de validation, par exemple, sauter la vérification de la longueur, en utilisant directement les méthodes de validation.

  • il est également possible de "opt out" de la validation externe (DNS MX records et liste noire des domaines). E. g., un projet sur lequel j'ai travaillé a d'abord validé l'existence d'enregistrements MX pour un domaine, mais l'a finalement supprimé en raison du nombre de clients utilisant des solutions de type "dynamic IP".

  • il est facile d'interroger votre magasin persistant pour des adresses email qui ne correspondent pas aux règles de validation actuelles, mais exécuter une simple requête et traiter chaque email comme "nouveau" plutôt que "existant" - si une exception est lancée, il y a un problème. De là, vous pouvez, par exemple, un FlagCustomerAsHavingABadEmail commande, en utilisant le message d'erreur exception, pour guider l'utilisateur quand ils voient le message.

  • permettre au programmeur de fournir le code d'erreur offre de la flexibilité. Par exemple, lors de l'envoi d'un UpdateEmailAddress la commande, le message d'erreur "Votre adresse e-mail doit être d'au moins 3 caractères" est auto-explicatif. Cependant, lors de la mise à jour de plusieurs adresses e-mail (domicile et travail), le message d'erreur ci-dessus n'indique pas quel e-mail était erroné. La fourniture de l'erreur code/message vous permet de fournir une meilleure rétroaction à l'utilisateur final.

0
répondu magnus 2016-05-20 07:46:22
la source

j'ai écrit un billet de blog sur ce sujet il y a quelque temps. La prémisse du poste était qu'il existe différents types de validation. Je les ai appelés validation superficielle et Validation de commande basée sur le domaine.

Cette version, plus simple, est-ce. Valider des choses comme "est-ce un numéro" ou "adresse e-mail" sont le plus souvent superficielles. Celles-ci peuvent être effectuées avant que la commande n'atteigne les entités du domaine.

Toutefois, lorsque la validation est plus liée au domaine alors il est à la bonne place est dans le domaine. Par exemple, vous avez peut-être des règles sur le poids et le type de fret qu'un certain camion peut prendre. Cela ressemble beaucoup plus à de la logique de domaine.

alors vous avez les types hybrides. Des choses comme la validation basée sur set. Ceux - ci doivent se produire avant que la commande soit émise ou injectée dans le domaine (essayez d'éviter que si possible-limiter les dépendances est une bonne chose).

de toute façon, vous pouvez lire le post complet ici: Comment Valider Commandes dans une Application CQRS

0
répondu Codescribler 2017-02-17 12:56:09
la source

je suis encore en train d'expérimenter ce concept, mais vous pouvez essayer les décorateurs. Si vous utilisez SimpleInjector, vous pouvez facilement injecter vos propres classes de validation qui s'exécutent avant votre gestionnaire de commandes. Ensuite, la commande peut supposer qu'il est valide si elle est loin. Cependant, cela signifie que toute validation doit être faite sur la commande et non sur les entités. Les entités ne seront pas dans un état invalide. Mais chaque commande doit implémenter sa propre validation complètement pour que des commandes similaires puissent avoir une duplication des règles mais vous pouvez soit abstraire des règles communes pour partager ou traiter des commandes différentes comme vraiment séparées.

0
répondu Daniel Lorenz 2017-07-02 15:37:53
la source

vous pouvez utiliser une solution basée sur les messages avec des événements de domaine comme expliqué ici.

les Exceptions ne sont pas la bonne méthode pour toutes les erreurs de validation, n'est pas dit qu'un pas valide entité est un cas exceptionnel.

si la validation n'est pas triviale, la logique pour valider l'agrégat peut être exécutée directement sur le serveur et pendant que vous essayez de définir une nouvelle entrée, vous pouvez soulever un événement de domaine pour le dire à utilisateur (ou l' application qui utilise votre domaine) pourquoi la saisie n'est pas correcte.

-3
répondu Matteo Migliore 2012-06-27 01:45:01
la source