Où placer la validation des règles globales dans DDD

je suis nouveau à DDD, et j'essaie de l'appliquer dans la vraie vie. Il n'y a pas de questions sur une telle logique de validation, comme la vérification nulle, la vérification des chaînes vides, etc - qui va directement au constructeur/propriété de l'entité. Mais où mettre la validation de certaines règles comme "Unique" nom d'utilisateur?

donc, nous avons l'utilisateur entité

public class User : IAggregateRoot
{
   private string _name;

   public string Name
   {
      get { return _name; }
      set { _name = value; }
   }

   // other data and behavior
}

et dépôt pour utilisateurs

public interface IUserRepository : IRepository<User>
{
   User FindByName(string name);
}

Options sont:

  1. injecter le dépôt à l'entité
  2. injecter le dépôt à l'usine
  3. créer une opération sur le service de domaine
  4. ???

et chaque option plus détaillé:

1 .Injecter le dépôt à l'entité

je peux requête de dépôt dans des entités constructeur/propriété. Mais je pense que garder une référence au dépôt dans entity est une mauvaise odeur.

public User(IUserRepository repository)
{
    _repository = repository;
}

public string Name
{
    get { return _name; }
    set 
    {
       if (_repository.FindByName(value) != null)
          throw new UserAlreadyExistsException();

       _name = value; 
    }
}

mise à jour: nous pouvons utiliser DI pour cacher la dépendance entre L'Utilisateur et IUserRepository via un objet de spécification.

2. Injecter le dépôt à l'usine

je peux mettre cette logique de vérification dans UserFactory. Mais que faire si nous voulons changer le nom de l'utilisateur?

3. Créer l'opération sur le service de domaine

je peux créer un service de domaine pour créer et éditer des utilisateurs. Mais quelqu'un peut modifier directement le nom de l'utilisateur sans appeler ce service...

public class AdministrationService
{
    private IUserRepository _userRepository;

    public AdministrationService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void RenameUser(string oldName, string newName)
    {
        if (_userRepository.FindByName(newName) != null)
            throw new UserAlreadyExistException();

        User user = _userRepository.FindByName(oldName);
        user.Name = newName;
        _userRepository.Save(user);
    }
}

4. ???

où mettez-vous la logique de validation globale pour les entities?

Merci!

57
demandé sur Sergey Berezovskiy 0000-00-00 00:00:00

9 réponses

la plupart du temps, il est préférable de placer ce genre de règles dans les objets Specification . Vous pouvez placer ces Specification dans vos paquets de domaine, de sorte que quiconque utilisant votre paquet de domaine y a accès. En utilisant une spécification, vous pouvez regrouper vos règles d'affaires avec vos entités, sans créer des entités difficiles à lire avec des dépendances non désirées sur les services et les dépôts. Si nécessaire, vous pouvez injecter des dépendances sur des services ou des dépôts dans une spécification.

selon le contexte, vous pouvez construire différents validateurs en utilisant les objets de spécification.

la principale préoccupation des entités devrait être de garder une trace de l'état des affaires - c'est assez d'une responsabilité et ils ne devraient pas être concernés par la validation.

exemple

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}

deux spécifications:

public class IdNotEmptySpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User subject)
    {
        return !string.IsNullOrEmpty(subject.Id);
    }
}


public class NameNotTakenSpecification : ISpecification<User>
{
    // omitted code to set service; better use DI
    private Service.IUserNameService UserNameService { get; set; } 

    public bool IsSatisfiedBy(User subject)
    {
        return UserNameService.NameIsAvailable(subject.Name);
    }
}

et un validateur:

public class UserPersistenceValidator : IValidator<User>
{
    private readonly IList<ISpecification<User>> Rules =
        new List<ISpecification<User>>
            {
                new IdNotEmptySpecification(),
                new NameNotEmptySpecification(),
                new NameNotTakenSpecification()
                // and more ... better use DI to fill this list
            };

    public bool IsValid(User entity)
    {
        return BrokenRules(entity).Count() > 0;
    }

    public IEnumerable<string> BrokenRules(User entity)
    {
        return Rules.Where(rule => !rule.IsSatisfiedBy(entity))
                    .Select(rule => GetMessageForBrokenRule(rule));
    }

    // ...
}

pour l'exhaustivité, les interfaces:

public interface IValidator<T>
{
    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);
}

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T subject);
}

Notes

je pense que la réponse précédente de Vijay Patel est dans la bonne direction, mais je pense que c'est un peu bizarre. Il suggère que l'entité utilisateur dépend de la spécification, où j'ai la conviction que ce devrait être l'inverse. De cette façon, vous pouvez laisser la spécification dépendre des services, des dépôts et du contexte en général, sans faire dépendre votre entité par le biais d'une dépendance de spécification.

Références

Une question connexe, avec une bonne réponse avec un exemple: Validation dans un Domain Driven Design .

Eric Evans décrit l'utilisation de la spécification pattern for validation, selection and object construction dans chapter 9, pp 145 .

cet" article de 1519410920 " sur le modèle de spécification avec une application en .Net pourrait être d'intérêt pour vous.

52
répondu Marijn 2017-05-23 12:25:29

Je ne recommande pas de refuser de changer les propriétés dans entity, si c'est une entrée de l'utilisateur. Par exemple, si la validation ne passe pas, vous pouvez toujours utiliser l'instance pour l'afficher dans l'interface utilisateur avec les résultats de la validation, permettant à l'utilisateur de corriger l'erreur.

Jimmy Nilsson dans son" application de la conception et des modèles par Domaine " recommande de valider pour une opération particulière, pas seulement pour la persistance. Bien qu'une entité puisse être maintenue avec succès, la la validation se produit lorsqu'une entité est sur le point de changer son état, par exemple l'état "ordonné" change en "Acheté".

lors de la création, l'instance doit être valid-for-saving, ce qui implique de vérifier l'unicité. Il est différent de valid-for-ordering, où non seulement l'unicité doit être vérifiée, mais aussi, par exemple, la crédibilité d'un client, et la disponibilité au magasin.

ainsi, la logique de validation ne doit pas être invoquée sur une cession de propriété, elle devrait être invoquée pour les opérations au niveau agrégé, qu'elles soient persistantes ou non.

11
répondu George Polevoy 2011-04-28 18:41:38

modifier: a en juger par les autres réponses, le nom correct pour un tel" service de domaine "est spécification . J'ai mis à jour ma réponse pour refléter ceci, y compris un échantillon de code plus détaillé.

je choisirais l'option 3; Créer un domain service spécification qui encapsule la logique réelle qui effectue la validation. Par exemple, la spécification appelle initialement un dépôt, mais vous pourrait la remplacer avec un appel de service web à un stade ultérieur. Le fait d'avoir toute cette logique derrière une spécification abstraite rendra la conception globale plus flexible.

pour empêcher quelqu'un d'éditer le nom sans le valider, faire de la spécification un aspect requis pour éditer le nom. Vous pouvez y parvenir en changeant l'API de votre entity en quelque chose comme ceci:

public class User
{
    public string Name { get; private set; }

    public void SetName(string name, ISpecification<User, string> specification)
    {
        // Insert basic null validation here.

        if (!specification.IsSatisfiedBy(this, name))
        {
            // Throw some validation exception.
        }

        this.Name = name;
    }
}

public interface ISpecification<TType, TValue>
{
    bool IsSatisfiedBy(TType obj, TValue value);
}

public class UniqueUserNameSpecification : ISpecification<User, string>
{
    private IUserRepository repository;

    public UniqueUserNameSpecification(IUserRepository repository)
    {
        this.repository = repository;
    }

    public bool IsSatisfiedBy(User obj, string value)
    {
        if (value == obj.Name)
        {
            return true;
        }

        // Use this.repository for further validation of the name.
    }
}

votre code d'appel ressemblerait à quelque chose comme ceci:

var userRepository = IoC.Resolve<IUserRepository>();
var specification = new UniqueUserNameSpecification(userRepository);

user.SetName("John", specification);

et bien sûr, vous pouvez vous moquer de ISpecification dans vos tests unitaires pour des tests plus faciles.

7
répondu Niels van der Rest 2011-04-30 09:47:51

j'utiliserais une spécification pour encapsuler la règle. Vous pouvez ensuite appeler lorsque la propriété UserName est mise à jour (ou de n'importe où ailleurs qui pourrait en avoir besoin):

public class UniqueUserNameSpecification : ISpecification
{
  public bool IsSatisifiedBy(User user)
  {
     // Check if the username is unique here
  }
}

public class User
{
   string _Name;
   UniqueUserNameSpecification _UniqueUserNameSpecification;  // You decide how this is injected 

   public string Name
   {
      get { return _Name; }
      set
      {
        if (_UniqueUserNameSpecification.IsSatisifiedBy(this))
        {
           _Name = value;
        }
        else
        {
           // Execute your custom warning here
        }
      }
   }
}

cela n'a pas d'importance si un autre développeur essaie de modifier User.Name directement, parce que la règle sera toujours exécutée.

pour en savoir plus, cliquez ici "1519100920

3
répondu Vijay Patel 2011-04-29 08:22:29

Je ne suis pas un expert en DDD mais je me suis posé les mêmes questions et c'est ce que j'ai trouvé: La logique de Validation devrait normalement aller dans le constructeur/l'usine et les setters. De cette façon, vous garantissez que vous avez toujours des objets de domaine valides. Mais si la validation implique des requêtes de base de données qui affectent vos performances, une mise en œuvre efficace nécessite une conception différente.

(1) entités D'injection: les entités D'injection peuvent être: technique difficile et rend également la gestion des performances de l'application très difficile en raison de la fragmentation de votre logique de base de données. Des opérations apparemment simples peuvent maintenant avoir un impact inattendu sur le rendement. Cela rend également impossible d'optimiser votre objet de domaine pour des opérations sur des groupes du même type d'entités, vous ne pouvez plus écrire une seule requête de groupe, et à la place vous avez toujours des requêtes individuelles pour chaque entité.

(2) l'Injection de référentiel: vous ne devez pas mettre de logique d'affaires dans les dépôts. Gardez les dépôts simples et concentrés. Ils doivent agir comme s'il s'agissait de collections et ne contenir que de la logique pour ajouter, enlever et trouver des objets (certains vont même jusqu'à dériver les méthodes find vers d'autres objets).

(3) Service de domaine cela semble l'endroit le plus logique pour traiter la validation qui nécessite une interrogation de la base de données. Une bonne mise en œuvre ferait du constructeur/de l'usine et les setters ont impliqué le paquet privé, de sorte que les entités ne peuvent être créées / modifiées qu'avec le service de domaine.

2
répondu Kdeveloper 2011-04-28 18:59:39

dans mon cadre CQRS, chaque classe de gestionnaire de commandes contient également une méthode ValidateCommand, qui appelle alors la logique de business/validation appropriée dans le domaine (généralement implémentée en tant que Méthodes Entity ou méthodes Entity static).

donc l'appelant ferait comme ça:

if (cmdService.ValidateCommand(myCommand) == ValidationResult.OK)
{
    // Now we can assume there will be no business reason to reject
    // the command
    cmdService.ExecuteCommand(myCommand); // Async
}

chaque gestionnaire de commandes Spécialisé contient la logique wrapper, par exemple:

public ValidationResult ValidateCommand(MakeCustomerGold command)
{
    var result = new ValidationResult();
    if (Customer.CanMakeGold(command.CustomerId))
    {
        // "OK" logic here
    } else {
        // "Not OK" logic here
    }
}

la méthode ExecuteCommand de la le gestionnaire de commandes appellera alors à nouveau le ValidateCommand (), donc même si le client ne s'en est pas soucié, rien ne se passera dans le domaine qui n'est pas supposé le faire.

1
répondu Roy Dictus 2011-05-05 12:33:44

créer une méthode, par exemple, appelée IsUserNameValid() et rendre accessible de partout. Je le mettrais moi-même dans le service Utilisateur. Cela ne vous limitera pas lorsque de futurs changements se produiront. Il garde le code de validation à un endroit (implémentation), et d'autres codes qui en dépendent n'auront pas à changer si les changements de validation vous pouvez trouver que vous avez besoin de l'appeler de plusieurs endroits plus tard, comme l'interface utilisateur pour l'indication visuelle sans avoir à recourir à la gestion des exceptions. La couche service pour les opérations correctes, et le dépôt (cache, db,etc. couche pour s'assurer que les articles stockés sont valides.

0
répondu Charles Lambert 2011-05-07 02:07:51

j'aime l'option 3. La mise en œuvre la plus simple pourrait en avoir l'air:

public interface IUser
{
    string Name { get; }
    bool IsNew { get; }
}

public class User : IUser
{
    public string Name { get; private set; }
    public bool IsNew { get; private set; }
}

public class UserService : IUserService
{
    public void ValidateUser(IUser user)
    {
        var repository = RepositoryFactory.GetUserRepository(); // use IoC if needed

        if (user.IsNew && repository.UserExists(user.Name))
            throw new ValidationException("Username already exists");
    }
}
0
répondu SlavaGu 2011-05-08 11:23:19

Créer un domaine de service

ou je peux créer un service de domaine pour créer et éditer des utilisateurs. Mais quelqu'un peut directement modifier le nom de l'utilisateur sans appeler ce service...

si vous avez bien conçu vos entités, cela ne devrait pas poser de problème.

-1
répondu epitka 2011-04-28 14:50:59