Validation dans une conception pilotée par domaine

Comment gérez-vous la validation sur des agrégats complexes dans une conception pilotée par un domaine? Vous consolidez vos règles métier / votre logique de validation?

Je comprends la validation des arguments. Et je comprends la validation de propriété qui peut être attachée aux modèles eux-mêmes et faire des choses comme vérifier qu’une adresse e-mail ou un code postal est valide ou qu’un prénom a une longueur minimum et maximum.

Mais qu’en est-il de la validation complexe impliquant plusieurs modèles? Où placez-vous ces règles et méthodes dans votre architecture? Et quels modèles, le cas échéant, utilisez-vous pour les mettre en œuvre?

J’aime la solution de Jimmy Bogard à ce problème. Il a publié sur son blog un article intitulé “Validation d’entité avec les visiteurs et les méthodes d’extension” dans lequel il présente une approche très élégante de la validation d’entités qui suggère l’implémentation d’une classe distincte pour stocker le code de validation.

public interface IValidator { bool IsValid(T entity); IEnumerable BrokenRules(T entity); } public class OrderPersistenceValidator : IValidator { public bool IsValid(Order entity) { return BrokenRules(entity).Count() == 0; } public IEnumerable BrokenRules(Order entity) { if (entity.Id < 0) yield return "Id cannot be less than 0."; if (string.IsNullOrEmpty(entity.Customer)) yield return "Must include a customer."; yield break; } } 

Au lieu d’ IsValid(xx) appels IsValid(xx) partout dans votre application, envisagez de prendre conseil auprès de Greg Young:

Ne laissez jamais vos entités entrer dans un état invalide.

En gros, cela signifie que vous ne pensez plus aux entités en tant que conteneurs de données purs, mais plutôt aux objects ayant des comportements.

Prenons l’exemple de l’adresse d’une personne:

  person.Address = "123 my street"; person.City = "Houston"; person.State = "TX"; person.Zip = 12345; 

Entre tous ces appels, votre entité est invalide (car vous auriez des propriétés qui ne sont pas en accord les unes avec les autres. Maintenant, considérez ceci:

 person.ChangeAddress(.......); 

tous les appels relatifs au comportement de changement d’adresse sont désormais une unité atomique. Votre entité n’est jamais invalide ici.

Si vous prenez cette idée de comportements de modélisation plutôt que d’état, vous pouvez atteindre un modèle qui n’autorise pas les entités non valides.

Pour une bonne discussion à ce sujet, consultez cette interview infoq: http://www.infoq.com/interviews/greg-young-ddd

J’utilise habituellement une classe de spécification, elle fournit une méthode (ceci est C # mais vous pouvez le traduire dans n’importe quel langage):

 bool IsVerifiedBy(TEntity candidate) 

Cette méthode effectue une vérification complète du candidat et de ses relations. Vous pouvez utiliser des arguments dans la classe de spécification pour le paramétrer, comme un niveau de vérification …

Vous pouvez également append une méthode pour savoir pourquoi le candidat n’a pas vérifié la spécification:

 IEnumerable BrokenRules(TEntity canditate) 

Vous pouvez simplement décider de mettre en œuvre la première méthode comme celle-ci:

 bool IsVerifiedBy(TEntity candidate) { return BrokenRules(candidate).IsEmpty(); } 

Pour les règles cassées, j’écris habituellement un iterator:

 IEnumerable BrokenRules(TEntity candidate) { if (someComplexCondition) yield return "Message describing cleary what is wrong..."; if (someOtherCondition) yield return ssortingng.Format("The amount should not be {0} when the state is {1}", amount, state); } 

Pour la localisation, vous devez utiliser des ressources et pourquoi pas passer une culture à la méthode BrokenRules. Je place ces classes dans l’espace de noms du modèle avec des noms suggérant leur utilisation.

La validation de plusieurs modèles doit passer par votre racine agrégée. Si vous devez valider les racines agrégées, vous avez probablement un défaut de conception.

La manière dont je procède à la validation des agrégats consiste à renvoyer une interface de réponse qui indique si la validation réussit / échoue et tout message expliquant pourquoi elle a échoué.

Vous pouvez valider tous les sous-modèles de la racine globale pour qu’ils restnt cohérents.

 // Command Response class to return from public methods that change your model public interface ICommandResponse { CommandResult Result { get; } IEnumerable Messages { get; } } // The result options public enum CommandResult { Success = 0, Fail = 1 } // My default implementation public class CommandResponse : ICommandResponse { public CommandResponse(CommandResult result) { Result = result; } public CommandResponse(CommandResult result, params ssortingng[] messages) : this(result) { Messages = messages; } public CommandResponse(CommandResult result, IEnumerable messages) : this(result) { Messages = messages; } public CommandResult Result { get; private set; } public IEnumerable Messages { get; private set; } } // usage public class SomeAggregateRoot { public ssortingng SomeProperty { get; private set; } public ICommandResponse ChangeSomeProperty(ssortingng newProperty) { if(newProperty == null) { return new CommandResponse(CommandResult.Fail, "Some property cannot be changed to null"); } SomeProperty = newProperty; return new CommandResponse(CommandResult.Success); } } 

Cette question est un peu ancienne maintenant, mais si quelqu’un est intéressé, voici comment mettre en œuvre la validation dans mes classes de service.

J’ai une méthode de validation privée dans chacune de mes classes de service qui prend en charge une instance d’entité et une action, si la validation échoue, une exception personnalisée est générée avec les détails des règles rompues.

Exemple DocumentService avec validation intégrée

 public class DocumentService : IDocumentService { private IRepository _documentRepository; public DocumentService(IRepository documentRepository) { _documentRepository = documentRepository; } public void Create(Document document) { Validate(document, Action.Create); document.CreatedDate = DateTime.Now; _documentRepository.Create(document); } public void Update(Document document) { Validate(document, Action.Update); _documentRepository.Update(document); } public void Delete(int id) { Validate(_documentRepository.GetById(id), Action.Delete); _documentRepository.Delete(id); } public IList GetAll() { return _documentRepository .GetAll() .OrderByDescending(x => x.PublishDate) .ToList(); } public int GetAllCount() { return _documentRepository .GetAll() .Count(); } public Document GetById(int id) { return _documentRepository.GetById(id); } // validation private void Validate(Document document, Action action) { var brokenRules = new List(); if (action == Action.Create || action == Action.Update) { if (ssortingng.IsNullOrWhiteSpace(document.Title)) brokenRules.Add("Title is required"); if (document.PublishDate == null) brokenRules.Add("Publish Date is required"); } if (brokenRules.Any()) throw new EntityException(ssortingng.Join("\r\n", brokenRules)); } private enum Action { Create, Update, Delete } } 

J’aime cette approche car elle me permet de mettre toute ma logique de validation au même endroit, ce qui simplifie les choses.