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

Je suis nouveau chez DDD et j’essaie de l’appliquer dans la vraie vie. Il n’y a pas de questions sur une telle logique de validation, telle que la vérification nulle, la vérification des chaînes vides, etc. – qui va directement au constructeur / à la propriété de l’entité. Mais où mettre la validation de certaines règles globales telles que «Nom d’utilisateur unique»?

Nous avons donc une entité utilisateur

public class User : IAggregateRoot { private ssortingng _name; public ssortingng Name { get { return _name; } set { _name = value; } } // other data and behavior } 

Et repository pour les utilisateurs

 public interface IUserRepository : IRepository { User FindByName(ssortingng name); } 

Les options sont les suivantes:

  1. Injecter le référentiel à 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 .Inject repository to entity

Je peux interroger le référentiel dans les entités constructeur / propriété. Mais je pense que garder une référence au référentiel dans l’entité est une mauvaise odeur.

 public User(IUserRepository repository) { _repository = repository; } public ssortingng 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’object 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?

3. Créer une 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 d’utilisateur sans appeler ce service …

 public class AdministrationService { private IUserRepository _userRepository; public AdministrationService(IUserRepository userRepository) { _userRepository = userRepository; } public void RenameUser(ssortingng oldName, ssortingng 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!

La plupart du temps, il est préférable de placer ce type de règles dans Specification objects de Specification . Vous pouvez placer ces Specification dans vos packages de domaine afin que quiconque utilisant votre package de domaine puisse y accéder. À l’aide d’une spécification, vous pouvez regrouper vos règles métier avec vos entités sans créer des 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 objects de spécification.

La principale préoccupation des entités devrait être le suivi de 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 ssortingng Id { get; set; } public ssortingng Name { get; set; } } 

Deux spécifications:

 public class IdNotEmptySpecification : ISpecification { public bool IsSatisfiedBy(User subject) { return !ssortingng.IsNullOrEmpty(subject.Id); } } public class NameNotTakenSpecification : ISpecification { // 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 { private readonly IList> Rules = new List> { 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 BrokenRules(User entity) { return Rules.Where(rule => !rule.IsSatisfiedBy(entity)) .Select(rule => GetMessageForBrokenRule(rule)); } // ... } 

Pour être complet, les interfaces:

 public interface IValidator { bool IsValid(T entity); IEnumerable BrokenRules(T entity); } public interface ISpecification { bool IsSatisfiedBy(T subject); } 

Remarques

Je pense que la réponse précédente de Vijay Patel va dans la bonne direction, mais j’estime 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 par une dépendance de spécification.

Les références

Une question connexe avec une bonne réponse avec exemple: Validation dans un design piloté par domaine .

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

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

Je ne recommanderais pas de refuser de modifier les propriétés dans 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 les domaines”, recommande de valider pour une opération particulière, et pas seulement pour la persistance. Bien qu’une entité puisse être persistée avec succès, la validation réelle se produit lorsqu’une entité est sur le sharepoint modifier son état, par exemple l’état “Commandé” change pour “Acheté”.

Lors de la création, l’instance doit être valide pour la sauvegarde, ce qui implique la vérification de l’unicité. C’est différent de la validation pour 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.

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

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

Pour empêcher une personne de modifier le nom sans le valider, définissez la spécification comme un aspect requirejs pour modifier le nom. Vous pouvez y parvenir en changeant l’API de votre entité en quelque chose comme ceci:

 public class User { public ssortingng Name { get; private set; } public void SetName(ssortingng name, ISpecification specification) { // Insert basic null validation here. if (!specification.IsSatisfiedBy(this, name)) { // Throw some validation exception. } this.Name = name; } } public interface ISpecification { bool IsSatisfiedBy(TType obj, TValue value); } public class UniqueUserNameSpecification : ISpecification { private IUserRepository repository; public UniqueUserNameSpecification(IUserRepository repository) { this.repository = repository; } public bool IsSatisfiedBy(User obj, ssortingng 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(); var specification = new UniqueUserNameSpecification(userRepository); user.SetName("John", specification); 

Et bien sûr, vous pouvez ISpecification dans vos tests unitaires pour faciliter les tests.

J’utiliserais une spécification pour encapsuler la règle. Vous pouvez ensuite appeler lorsque la propriété UserName est mise à jour (ou depuis n’importe quel autre endroit qui pourrait en avoir besoin):

 public class UniqueUserNameSpecification : ISpecification { public bool IsSatisifiedBy(User user) { // Check if the username is unique here } } public class User { ssortingng _Name; UniqueUserNameSpecification _UniqueUserNameSpecification; // You decide how this is injected public ssortingng Name { get { return _Name; } set { if (_UniqueUserNameSpecification.IsSatisifiedBy(this)) { _Name = value; } else { // Execute your custom warning here } } } } 

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

Découvrez plus ici

Je ne suis pas un expert en DDD mais je me suis posé les mêmes questions et c’est ce que j’ai imaginé: la logique de validation devrait normalement aller dans les constructeurs / usines et les installateurs. De cette façon, vous garantissez que vous avez toujours des objects de domaine valides. Mais si la validation implique des requêtes de firebase database qui affectent vos performances, une implémentation efficace nécessite une conception différente.

(1) Injecting Entities (Injecter des entités): L’injection d’entités peut s’avérer difficile d’un sharepoint vue technique et rend très difficile la gestion des performances des applications en raison de la fragmentation de la logique de votre firebase database. Des opérations apparemment simples peuvent désormais avoir un impact inattendu sur les performances. Il est également impossible d’optimiser l’object de votre 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 vous avez toujours des requêtes individuelles pour chaque entité.

(2) Référentiel d’injection: vous ne devez placer aucune 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 append, supprimer et rechercher des objects (certains détournant même les méthodes de recherche vers d’autres objects).

(3) Service de domaine Cela semble être l’endroit le plus logique pour gérer la validation nécessitant une interrogation de la firebase database. Une bonne implémentation impliquerait que le constructeur / usine et les opérateurs impliquent un package privé, de sorte que les entités ne peuvent être créées / modifiées qu’avec le service de domaine.

Dans mon environnement CQRS, chaque classe de gestionnaire de commandes contient également une méthode ValidateCommand, qui appelle ensuite la logique métier / de 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 ferait comme si:

 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 (). Ainsi, même si le client ne s’embête pas, rien ne se produira dans le domaine qui n’est pas censé le faire.

Créez une méthode, par exemple, appelée IsUserNameValid () et rendez-la accessible partout. Je le mettrais dans le service utilisateur moi-même. Cela ne vous limitera pas lorsque de futurs changements surviendront. Il conserve le code de validation en un seul endroit (implémentation), et les autres codes qui en dépendent n’ont pas à être modifiés si la validation change. sans avoir à recourir à la gestion des exceptions. 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.

J’aime l’option 3. L’implémentation la plus simple pourrait être la suivante:

 public interface IUser { ssortingng Name { get; } bool IsNew { get; } } public class User : IUser { public ssortingng 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"); } } 

Créer un service de domaine

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

Si vous avez correctement conçu vos entités, cela ne devrait pas poser problème.