Je recherche toujours les meilleures pratiques pour la validation des modèles de domaine. Est-ce bon 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 toute votre suggestion.
Il y a un article intéressant de Martin Fowler sur ce sujet qui met en évidence un aspect que la plupart des gens (dont moi) ont tendance à négliger:
Mais une chose qui, selon moi, fait constamment trébucher les gens, c'est lorsqu'ils pensent que la validité d'un objet d'une manière indépendante du contexte, telle qu'une méthode isValid, l'implique.
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. Cette commande est-elle valable pour être remplie, est-ce que ce client est valide pour s'enregistrer à l'hôtel. Donc, plutôt que d'avoir des méthodes comme isValid, ayez des méthodes comme isValidForCheckIn.
Il s'ensuit que le constructeur ne doit pas faire de validation, à l'exception peut-être de quelques vérifications de base très communes partagées par tous les contextes.
Encore une fois à partir de l'article:
Dans About Face, Alan Cooper a préconisé de ne pas laisser nos idées d'états valides empêcher un utilisateur d'entrer (et de sauvegarder) des informations incomplètes. Cela m'a rappelé il y a quelques jours lors de la lecture d'une ébauche d'un livre sur lequel Jimmy Nilsson travaille. Il a énoncé un principe selon lequel vous devriez toujours pouvoir enregistrer 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 d'épargner plus qu'ils ne le devraient. Penser au contexte de la validation peut aider à éviter cela.
Malgré le fait que cette question soit un peu périmée, je voudrais 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 des obstacles aux tests et l'un de ces obstacles est "difficile à construire" des objets. La construction d'un objet non valide devrait être possible et, comme le suggère Fowler, les contrôles de validité dépendants du contexte devraient être en mesure d'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 constamment l'état d'une sortie et en appliquant une action corrective lorsque la sortie s'écarte du point de consigne, c'est ce qu'on appelle le contrôle en boucle fermée. Le contrôle en boucle fermée attend intrinsèquement les écarts et agit pour les corriger et c'est ainsi que le monde réel fonctionne, 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 de la validation dépendante du contexte et des objets faciles à construire rendra votre système plus facile à utiliser plus tard.
Comme je suis sûr que vous le savez déjà ...
En programmation orientée objet, un constructeur (parfois raccourci en ctor) dans une classe est un type spécial de sous-programme appelé lors de la création d'un objet. Il prépare le nouvel objet à l'utilisation, en acceptant souvent des paramètres que le constructeur utilise pour définir toutes les variables membres requises lors de la création de l'objet. Il est appelé constructeur car il construit les valeurs des données membres de la classe.
La vérification de la validité des données transmises en tant que paramètres c'tor est certainement valide dans le constructeur - sinon vous autorisez peut-être la construction d'un objet invalide.
Cependant (et c'est juste mon avis, je ne peux pas trouver de bons documents à ce stade) - si la validation des données nécessite des opérations complexes (telles que les opérations asynchrones - peut-être une validation basée sur le serveur si vous développez une application de bureau), alors c'est mieux mettre dans une fonction d'initialisation ou de validation explicite d'une certaine sorte et les membres définis sur des valeurs par défaut (telles que null
) dans le c'tor.
En outre, tout comme une note latérale que vous l'avez incluse dans votre exemple de code ...
À moins que vous ne fassiez une validation supplémentaire (ou d'autres fonctionnalités) dans AddOrderLine
, j'exposerais très probablement le List<LineItem>
en tant que propriété plutôt que d'avoir Order
agir en tant que façade .
La validation doit être effectuée dès que possible.
La validation dans n'importe quel contexte, que ce soit le modèle de domaine ou toute autre manière d'écrire un logiciel, devrait servir l'objectif de 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 diviser la validation.
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 attendue.
La validation d'objet garantit que toutes les propriétés de l'objet sont valables conjointement. par exemple. BeginDate est avant EndDate. Supposons que vous lisez une valeur dans le magasin de données et que BeginDate et EndDate soient initialisés par défaut à DateTime.Min. 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 ont été définies. Cela peut être appelé au niveau racine agrégé
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 et il en va de même pour OrderLines. Mais une règle commerciale stipule qu'aucune commande ne peut dépasser 1 000 $. Comment pourriez-vous appliquer cette règle dans certains cas, cela IS autorisé. Vous ne pouvez pas simplement ajouter une propriété "ne pas valider le montant" car cela conduirait à des abus (tôt ou tard, peut-être même vous, juste pour faire disparaître cette "méchante demande").
il y a ensuite la validation au niveau de la couche de présentation. Allez-vous vraiment envoyer l'objet sur le réseau, sachant qu'il échouera? Ou épargnerez-vous à l'utilisateur ce burdon et l'informerez 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 encore oublié ce champ lors d'un autre test", en particulier lorsqu'un bug de production doit être corrigé avec votre patron qui respire dans votre cou?
La validation au niveau de la persistance est censée être aussi proche que possible de la validation de la valeur de la propriété. Cela permettra d'éviter les exceptions lors de la lecture d'erreurs "nulles" ou "de valeurs non valides" lors de l'utilisation de mappeurs de tout type ou de simples anciens lecteurs de données. L'utilisation de procédures stockées résout ce problème, mais nécessite d'écrire à nouveau la même logique de valorisation et de l'exécuter à nouveau. 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 (ou pire, dérangez-le avec ce "choix judicieux dont il n'est pas payé").
donc pour le dire avec quelques mots célèbres "ça dépend", mais au moins maintenant vous savez POURQUOI cela dépend.
J'aimerais pouvoir mettre tout cela en un seul endroit, mais malheureusement, cela ne peut pas être fait. Faire cela placerait une dépendance sur un "objet Dieu" contenant TOUTES les validations pour TOUTES les couches. Vous ne voulez pas emprunter ce chemin sombre.
Pour cette raison, je ne lance les exceptions de validation qu'au niveau d'une 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 AggregateException.
Lors de la propagation de la pile d'appels, je les rassemble à nouveau dans AggregateExceptions jusqu'à ce que j'atteigne la couche de présentation. La couche de service peut envoyer cette exception directement au client dans le cas de WCF en tant qu'exception de défaut.
Cela me permet de prendre l'exception et de la diviser 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, pour les court-circuiter autant que possible.
Si vous vous demandez pourquoi j'ai également la validation au niveau de l'agrégation (ou au niveau du service si vous le souhaitez), c'est parce que je n'ai pas de boule de cristal me disant qui utilisera mes services à l'avenir. Vous aurez suffisamment de mal à trouver vos propres erreurs pour empêcher les autres de faire vos propres erreurs :) en saisissant des données invalides. vous administrez l'application A, mais l'application B alimente certaines données en utilisant votre service. Devinez à qui ils demandent en premier quand il y a un bug? L'administrateur de l'application B se fera un plaisir d'informer l'utilisateur "il n'y a pas d'erreur de ma part, je viens d'alimenter les données".