Où nous devrions mettre la validation pour le modèle de domaine

38

Je cherche toujours les meilleures pratiques pour la validation de modèle de domaine. Est-ce bien de mettre la validation en constructeur de modèle de domaine? mon exemple de validation de modèle de domaine comme suit:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Merci pour toutes vos suggestions.

adisembiring
la source

Réponses:

47

Martin Fowler a écrit un article intéressant sur ce sujet qui met en évidence un aspect que la plupart des gens (y compris moi) ont tendance à négliger:

Mais une chose qui, je pense, surprend constamment les gens, c’est quand ils pensent que la validité d’un objet dépend d’un contexte indépendant de celui qu’implique une méthode isValid.

Je pense qu'il est beaucoup plus utile de penser à la validation comme quelque chose qui est lié à un contexte - généralement une action que vous voulez faire. Est-ce que cette commande est valide pour être remplie, est-ce que ce client est valide pour s'enregistrer à l'hôtel? Ainsi, plutôt que d’avoir des méthodes comme isValid, il existe des méthodes comme isValidForCheckIn.

Il s'ensuit que le constructeur ne doit pas effectuer de validation, à l'exception peut-être de quelques vérifications de base très fondamentales partagées par tous les contextes.

Encore de l'article:

Dans À propos de Face, Alan Cooper a préconisé de ne pas laisser nos idées d'Etats valides empêcher un utilisateur d'entrer (et de sauvegarder) des informations incomplètes. Cela me rappelait cela il y a quelques jours en lisant l'ébauche d'un livre sur lequel travaille Jimmy Nilsson. Il a énoncé un principe selon lequel vous devriez toujours pouvoir sauvegarder un objet, même s'il contient des erreurs. Bien que je ne sois pas convaincu que cela devrait être une règle absolue, je pense que les gens ont tendance à empêcher l'épargne plus qu'ils ne le devraient. Penser au contexte de la validation peut aider à éviter cela.

Michael Borgwardt
la source
Dieu merci, quelqu'un a dit ça. Les formulaires qui contiennent 90% des données, mais ne sauvegardent rien, sont injustes pour les utilisateurs, qui constituent souvent les 10% restants pour ne pas perdre de données. La validation n’a fait que forcer le système à perdre la trace, dont 10%. a été constitué. Des problèmes similaires peuvent se produire sur le back-end - par exemple, une importation de données. J'ai trouvé qu'il est généralement préférable d'essayer de travailler correctement avec des données non valides que d'essayer de les empêcher.
psr
2
@psr Avez-vous même besoin d'une logique back-end si vos données ne sont pas conservées? Vous pouvez laisser toute la manipulation du côté client si vos données n'ont aucune signification pour votre modèle d'entreprise. Ce serait aussi un gaspillage de ressources pour envoyer des messages (client - serveur) si les données n’ont pas de sens. Nous revenons donc à l'idée de "ne jamais vous permettre que des objets de domaine entrent dans un état invalide!" .
Geo C.
2
Je me demande pourquoi tant de votes pour une réponse aussi ambiguë. Lorsque vous utilisez DDD, il existe parfois des règles qui vérifient simplement que certaines données sont INT ou se trouvent dans une plage. Par exemple, lorsque vous laissez l'utilisateur de votre application choisir certaines contraintes sur ses produits (combien de fois un utilisateur peut-il prévisualiser mon produit et dans quel intervalle de jours d'un mois). Ici, les deux contraintes doivent être int et l'une d'entre elles doit être comprise entre 0 et 31. Cela semble être une validation du format des données qui, dans un environnement autre que DDD, pourrait s’intégrer dans un service ou un contrôleur. Mais dans DDD, je suis favorable à la validité du domaine (90%).
Geo C.
2
Faire en sorte que les couches supérieures en sachent trop sur le domaine pour le conserver dans un état valide sent le mauvais dessin. Le domaine doit être celui qui garantit la validité de son état. Déplacer trop sur les épaules des couches supérieures peut rendre votre domaine anémique et vous pourriez échapper à des contraintes importantes qui pourraient nuire à votre entreprise. Ce que je réalise maintenant, une bonne généralisation serait de garder votre validation aussi proche que possible de votre persistance, ou aussi proche de votre code de manipulation de données (lorsqu'il est manipulé pour atteindre un état final).
Geo C.
PS Je ne mélange pas l'autorisation (est autorisé à faire quelque chose), l'authentification (le message venait-il du bon endroit ou a-t-il été envoyé par le bon client, tous deux identifiés par la clé api / le jeton / le nom d'utilisateur ou autre) avec la validation du format ou des règles commerciales. Quand je dis 90%, je veux dire ces règles métier que la plupart d'entre elles incluent également la validation du format. La validation du format Ofcourse peut se faire dans les couches supérieures, mais la plupart d’entre elles seront dans le domaine (même le format de l’adresse électronique qui sera validée dans l’objet valeur EmailAddress).
Geo C.
6

Malgré le fait que cette question est un peu fade, j'aimerais ajouter quelque chose de valable:

Je voudrais être d'accord avec @MichaelBorgwardt et étendre en évoquant la testabilité. Dans "Travailler efficacement avec le code hérité", Michael Feathers parle beaucoup d'obstacles aux tests et l'un de ces obstacles est "difficile à construire". Construire un objet invalide devrait être possible, et comme le suggère Fowler, les contrôles de validité dépendants du contexte devraient pouvoir identifier ces conditions. Si vous ne savez pas comment construire un objet dans un faisceau de test, vous aurez du mal à tester votre classe.

En ce qui concerne la validité, j'aime penser aux systèmes de contrôle. Les systèmes de contrôle fonctionnent en analysant en permanence l'état d'une sortie et en appliquant une action corrective lorsque la sortie s'écarte du point de consigne, on parle de contrôle en boucle fermée. Le contrôle en boucle fermée attend intrinsèquement des déviations et agit pour les corriger. C'est ainsi que fonctionne le monde réel. C'est pourquoi tous les systèmes de contrôle réels utilisent généralement des contrôleurs en boucle fermée.

Je pense que l'utilisation d'une validation dépendant du contexte et d'une construction facile des objets rendra votre système plus facile à utiliser.

Paul
la source
1
Souvent, les objets ne semblent que difficiles à construire. Par exemple, dans ce cas, vous pouvez contourner le constructeur public en créant une classe Wrapper qui hérite de la classe en cours de test et vous permet de créer une instance de l'objet de base dans un état non valide. C’est là que l’utilisation des modificateurs d’accès corrects sur les classes et les constructeurs entre en jeu et peut vraiment être préjudiciable aux tests si elle est utilisée de manière incorrecte. En outre, éviter les classes et méthodes "scellées", sauf dans les cas appropriés, facilitera grandement le test du code.
P. Roe
4

Comme je suis sûr que tu le sais déjà ...

En programmation orientée objet, un constructeur (parfois abrégé en ctor) dans une classe est un type spécial de sous-routine appelé à la création d'un objet. Il prépare le nouvel objet à l’utilisation, acceptant souvent les paramètres que le constructeur utilise pour définir les variables de membre requises lors de la création de l’objet. Il s'appelle un constructeur car il construit les valeurs des données membres de la classe.

Vérifier la validité des données transmises en tant que paramètres c'tor est définitivement valide dans le constructeur - sinon, vous autorisez éventuellement la construction d'un objet non valide.

Cependant (et c'est juste mon opinion, je ne trouve aucune bonne documentation à ce stade) - si la validation des données nécessite des opérations complexes (telles que des opérations asynchrones - peut-être une validation sur serveur si vous développez une application de bureau), il est préférable de mettre dans une fonction d'initialisation ou de validation explicite d'une certaine sorte et les membres mis à des valeurs par défaut (telles que null) dans le c'tor.


En outre, juste comme une note latérale que vous avez inclus dans votre exemple de code ...

À moins que vous ne procédiez à une validation supplémentaire (ou à une autre fonctionnalité) AddOrderLine, j'exposerais probablement la List<LineItem>propriété comme une propriété plutôt que d' Orderagir comme une façade .

Demian Brecht
la source
Pourquoi exposer le conteneur? Qu'importe pour les couches supérieures ce que le conteneur est? Il est parfaitement raisonnable d'avoir une AddLineItemméthode. En fait, pour DDD, c'est préférable. Si List<LineItem>est remplacé par un objet de collection personnalisé, la propriété exposée et tout ce qui en dépend List<LineItem>sont sujets à modification, erreur et exception.
Résumé de la
4

La validation doit être effectuée dès que possible.

La validation dans n’importe quel contexte, que ce soit un modèle de domaine ou tout autre moyen d’écrire un logiciel, doit servir à CE QUE vous voulez valider et à quel niveau vous vous trouvez actuellement.

Sur la base de votre question, je suppose que la réponse serait de scinder la validation.

  1. La validation de propriété vérifie si la valeur de cette propriété est correcte, par exemple lorsqu'une plage comprise entre 1 et 10 est spécifiée.

  2. La validation d'objet garantit que toutes les propriétés de l'objet sont valides conjointement. Par exemple, BeginDate est avant EndDate. Supposons que vous lisiez une valeur du magasin de données et que BeginDate et EndDate soient initialisés à DateTime.Min par défaut. Lors de la définition de BeginDate, il n'y a aucune raison d'appliquer la règle "doit être avant EndDate", car cela ne s'applique pas ENCORE. Cette règle doit être vérifiée APRÈS que toutes les propriétés aient été définies. Ceci peut être appelé au niveau racine agrégé

  3. La validation doit également être effectuée sur l'entité agrégée (ou racine agrégée). Un objet Order peut contenir des données valides, tout comme ses OrderLines. Mais une règle de gestion stipule qu’aucun ordre ne peut dépasser 1 000 dollars. Comment appliqueriez-vous cette règle dans certains cas, cela EST autorisé. vous ne pouvez pas simplement ajouter une propriété "ne pas valider le montant" car cela entraînerait des abus (tôt ou tard, peut-être même vous, juste pour obtenir cette "vilaine demande" de la route).

  4. Ensuite, il y a validation au niveau de la couche de présentation. Allez-vous vraiment envoyer l'objet sur le réseau, sachant qu'il échouera? Ou allez-vous épargner ce burdon à l'utilisateur et l'informer dès qu'il entrera une valeur invalide. Par exemple, la plupart du temps, votre environnement DEV sera plus lent que la production. Souhaitez-vous attendre 30 secondes avant d’être informé de "vous avez de nouveau oublié ce champ pendant un autre test", en particulier lorsqu’un bug de production doit être corrigé alors que votre patron vous respire?

  5. La validation au niveau de la persistance est supposée être aussi proche que possible de la validation de la valeur de la propriété. Cela aidera à éviter les exceptions avec la lecture des erreurs "NULL" ou "Valeur invalide" lors de l'utilisation de mappeurs de tout type ou de lecteurs de données anciens L'utilisation de procédures stockées résout ce problème, mais nécessite d'écrire AGAIN et de l'exécuter AGAIN. Et les procédures stockées sont le domaine d'administration de la base de données, alors n'essayez pas de faire son travail aussi bien (ou pire, dérangez-le avec ce «médiocre choix pour lequel il n'est pas payé».

pour le dire avec quelques mots célèbres "ça dépend", mais au moins vous savez maintenant POURQUOI ça dépend.

J'aimerais pouvoir regrouper tout cela dans un seul endroit, mais malheureusement, cela ne peut être fait. Cela créerait une dépendance sur un "objet Dieu" contenant TOUTES les validations pour TOUTES les couches. Vous ne voulez pas aller dans cette voie sombre.

Pour cette raison, je ne lance que des exceptions de validation au niveau de la propriété. Tous les autres niveaux, j'utilise ValidationResult avec une méthode IsValid pour rassembler toutes les "règles brisées" et les transmettre à l'utilisateur dans une seule exception AggregateException.

Lors de la propagation de la pile d'appels, je les rassemble à nouveau dans AggregateExceptions jusqu'à atteindre la couche de présentation. La couche service peut envoyer cette exception directement au client dans le cas de WCF en tant qu'exception FaultException.

Cela me permet de prendre l'exception et de la scinder pour afficher les erreurs individuelles à chaque contrôle d'entrée ou de l'aplatir et de l'afficher dans une seule liste. Le choix t'appartient.

c'est pourquoi j'ai également évoqué la validation de la présentation, afin de les court-circuiter autant que possible.

Dans le cas où vous vous demandez pourquoi je dispose également de la validation au niveau de l'agrégation (ou du niveau de service si vous le souhaitez), c'est parce que je n'ai pas de boule de cristal me disant qui utilisera mes services dans le futur. Vous aurez suffisamment de difficulté à trouver vos propres erreurs pour empêcher les autres de commettre les mêmes erreurs que vous :) en saisissant des données non valides.Vous administrez l'application A, mais l'application B alimente certaines données à l'aide de votre service. Devinez qui ils demandent en premier quand il y a un bug? L’administrateur de l’application B informera volontiers l’utilisateur "il n’ya pas d’erreur de ma part, j’ai simplement introduit les données".

Wesley Kenis
la source