web-dev-qa-db-fra.com

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

Je suis nouveau sur 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 globales comme "Nom d'utilisateur unique"?

Donc, nous avons l'entité User

public class User : IAggregateRoot
{
   private string _name;

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

   // other data and behavior
}

Et référentiel pour les utilisateurs

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

Les options sont:

  1. Injection du référentiel dans l'entité
  2. Injecter le référentiel à l'usine
  3. Créer une opération sur le service de domaine
  4. ???

Et chaque option plus détaillée:

1 .Injecter le référentiel à l'entité

Je peux interroger le référentiel dans les entités constructeur/propriété. Mais je pense que garder la référence au référentiel dans l'entité 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 masquer la dépendance entre l'utilisateur et IUserRepository via l'objet Specification.

2. Injecter le référentiel à l'usine

Je peux mettre cette logique de vérification dans UserFactory. Mais que faire si nous voulons changer le nom d'un utilisateur déjà existant?

. Créer une opération sur le service de domaine

Je peux créer un service de domaine pour créer et modifier des utilisateurs. Mais quelqu'un peut directement modifier 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ù placez-vous la logique de validation globale pour les entités?

Merci!

63
Sergey Berezovskiy

La plupart du temps, il est préférable de placer ce type de règles dans les objets Specification. Vous pouvez placer ces Specification dans vos packages de domaine, afin que toute personne utilisant votre package de domaine y ait accès. À l'aide d'une spécification, vous pouvez regrouper vos règles métier avec vos entités, sans créer d'entités difficiles à lire avec des dépendances indésirables sur les services et les référentiels. Si nécessaire, vous pouvez injecter des dépendances sur des services ou des référentiels dans une spécification.

Selon le contexte, vous pouvez créer différents validateurs à l'aide des objets de spécification.

La principale préoccupation des entités devrait être de suivre l'état des affaires - c'est une responsabilité suffisante et elles ne devraient pas se préoccuper de 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 être complet, 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 va dans la bonne direction, mais je pense que c'est un peu décalé. Il suggère que l'entité utilisateur dépend de la spécification, où je pense que cela devrait être l'inverse. De cette façon, vous pouvez laisser la spécification dépendre des services, des référentiels et du contexte en général, sans que votre entité en dépende via une dépendance de spécification.

Références

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

Eric Evans décrit l'utilisation du modèle de spécification pour la validation, la sélection et la construction d'objets dans chapitre 9, pp 145 .

Cette article sur le modèle de spécification avec une application en .Net pourrait vous intéresser.

56
Marijn

Je ne recommanderais pas de ne pas modifier les propriétés de l'entité, s'il s'agit d'une entrée utilisateur. Par exemple, si la validation n'a pas réussi, vous pouvez toujours utiliser l'instance pour l'afficher dans l'interface utilisateur avec les résultats de la validation, ce qui permet à l'utilisateur de corriger l'erreur.

Jimmy Nilsson dans son "Application de la conception et des modèles pilotés par domaine" recommande de valider pour une opération particulière, pas seulement pour persister. Bien qu'une entité puisse être conservée avec succès, la véritable validation se produit lorsqu'une entité est sur le point de changer son état, par exemple l'état "Commandé" devient "Acheté".

Lors de la création, l'instance doit être valide pour l'enregistrement, ce qui implique de vérifier l'unicité. C'est différent de la validité de la commande, où non seulement l'unicité doit être vérifiée, mais aussi, par exemple, la crédibilité d'un client et la disponibilité dans le magasin.

Ainsi, la logique de validation ne doit pas être invoquée sur une affectation de propriété, elle doit être invoquée lors d'opérations de niveau agrégé, qu'elles soient persistantes ou non.

11
George Polevoy

Edit: 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 cela, y compris un exemple de code plus détaillé.

J'irais avec l'option 3; créer un service de domaine spécification qui encapsule la logique réelle qui effectue la validation. Par exemple, la spécification appelle initialement un référentiel, mais vous pouvez le remplacer par un appel de service Web à un stade ultérieur. Avoir toute cette logique derrière une spécification abstraite gardera la conception globale plus flexible.

Pour empêcher quelqu'un de modifier le nom sans le valider, faites de la spécification un aspect obligatoire de la modification du nom. Vous pouvez y parvenir en changeant l'API de votre entité 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 à 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 un test plus facile.

7
Niels van der Rest

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

Peu importe si un autre développeur essaie de modifier User.Name directement, car la règle s'exécutera toujours.

En savoir plus ici

3
Vijay Patel

Je ne suis pas un expert en DDD mais je me suis posé les mêmes questions et voici ce que j'ai trouvé: la logique de validation devrait normalement aller dans le constructeur/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 ont un impact sur vos performances, une implémentation efficace nécessite une conception différente.

(1) Injection d'entités: L'injection d'entités peut être difficile sur le plan technique et rend également la gestion des performances des applications très difficile en raison de la fragmentation de la logique de votre base de données. Des opérations apparemment simples peuvent désormais avoir un impact inattendu sur les performances. Cela rend également impossible l'optimisation de votre objet de domaine pour les 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) Injection du référentiel: Vous ne devez pas mettre de logique métier dans les référentiels. Gardez les référentiels simples et ciblés. Ils doivent agir comme s'ils étaient des collections et ne contenir que la logique pour ajouter, supprimer et rechercher des objets (certains dérivent même les méthodes de recherche à d'autres objets).

(3) Service de domaine Cela semble l'endroit le plus logique pour gérer la validation qui nécessite une interrogation de la base de données. Une bonne implémentation rendrait privé le constructeur/fabrique et les setters impliqués, de sorte que les entités ne peuvent être créées/modifiées qu'avec le service de domaine.

2
Kdeveloper

Dans mon cadre CQRS, chaque classe de gestionnaire de commandes contient également une méthode ValidateCommand, qui appelle ensuite la logique métier/validation appropriée dans le domaine (principalement implémentée en tant que méthodes d'entité ou méthodes statiques d'entité).

Donc, l'appelant aimerait faire ainsi:

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 d'encapsulation, 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 du gestionnaire de commandes appellera alors à nouveau ValidateCommand (), donc même si le client n'a pas dérangé, rien ne se passera dans le domaine qui n'est pas censé le faire.

1
Roy Dictus

J'aime l'option 3. La mise en œuvre la plus simple pourrait ressembler à ceci:

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
SlavaGu

Créez une méthode, par exemple, appelée IsUserNameValid () et rendez-la accessible de partout. Je le mettrais moi-même dans le service utilisateur. Cela ne vous limitera pas lorsque de futurs changements surviendront. Il conserve le code de validation en un seul endroit (implémentation), et tout autre code qui en dépendra n'aura pas à changer si la validation change Vous pouvez constater que vous devrez l'appeler à partir de plusieurs endroits plus tard, comme l'interface utilisateur pour une indication visuelle sans avoir à recourir à une gestion d'exception. La couche de service pour les opérations correctes et la couche de référentiel (cache, db, etc.) pour garantir la validité des éléments stockés.

0
Charles Lambert