Je sais que vous pensez (ou criez peut-être), "pas une autre question demandant où appartient la validation dans une architecture en couches?!?" Eh bien, oui, mais j'espère que ce sera un peu une vision différente du sujet.
Je suis fermement convaincu que la validation prend de nombreuses formes, est basée sur le contexte et varie à chaque niveau de l'architecture. C'est la base pour le post - aider à identifier quel type de validation doit être effectué dans chaque couche. De plus, une question qui revient souvent est celle de savoir où appartiennent les contrôles d'autorisation.
Le scénario d'exemple provient d'une application pour une entreprise de restauration. Périodiquement pendant la journée, un chauffeur peut remettre au bureau tout excédent d'argent qu'il a accumulé en transportant le camion d'un site à l'autre. L'application permet à un utilisateur d'enregistrer la «goutte d'argent» en collectant l'ID du conducteur et le montant. Voici un code squelette pour illustrer les couches impliquées:
public class CashDropApi // This is in the Service Facade Layer
{
[WebInvoke(Method = "POST")]
public void AddCashDrop(NewCashDropContract contract)
{
// 1
Service.AddCashDrop(contract.Amount, contract.DriverId);
}
}
public class CashDropService // This is the Application Service in the Domain Layer
{
public void AddCashDrop(Decimal amount, Int32 driverId)
{
// 2
CommandBus.Send(new AddCashDropCommand(amount, driverId));
}
}
internal class AddCashDropCommand // This is a command object in Domain Layer
{
public AddCashDropCommand(Decimal amount, Int32 driverId)
{
// 3
Amount = amount;
DriverId = driverId;
}
public Decimal Amount { get; private set; }
public Int32 DriverId { get; private set; }
}
internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
internal ICashDropFactory Factory { get; set; } // Set by IoC container
internal ICashDropRepository CashDrops { get; set; } // Set by IoC container
internal IEmployeeRepository Employees { get; set; } // Set by IoC container
public void Handle(AddCashDropCommand command)
{
// 4
var driver = Employees.GetById(command.DriverId);
// 5
var authorizedBy = CurrentUser as Employee;
// 6
var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
// 7
CashDrops.Add(cashDrop);
}
}
public class CashDropFactory
{
public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
{
// 8
return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
}
}
public class CashDrop // The domain object (entity)
{
public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
{
// 9
...
}
}
public class CashDropRepository // The implementation is in the Data Access Layer
{
public void Add(CashDrop item)
{
// 10
...
}
}
J'ai indiqué 10 emplacements où j'ai vu des contrôles de validation placés dans le code. Ma question est de savoir quelles vérifications vous feriez, le cas échéant, pour chacune des règles commerciales suivantes (avec les vérifications standard pour la longueur, la plage, le format, le type, etc.):
- Le montant de la remise en espèces doit être supérieur à zéro.
- La remise en espèces doit avoir un chauffeur valide.
- L'utilisateur actuel doit être autorisé à ajouter des espèces (l'utilisateur actuel n'est pas le conducteur).
Veuillez partager vos réflexions, comment vous envisagez ou envisagez ce scénario et les raisons de vos choix.
la source
CashDropAmount
objet de valeur plutôt qu'en utilisant unDecimal
. Vérifier si le pilote existe ou non serait fait dans le gestionnaire de commandes et il en va de même pour les règles d'autorisation. Vous pouvez obtenir une autorisation gratuitement en faisant quelque chose commeApprover approver = approverService.findById(employeeId)
où elle est lancée si l'employé n'est pas dans le rôle d'approbateur.Approver
serait juste un objet de valeur, pas une entité. Vous pouvez également vous débarrasser de votre usine ou utiliser la méthode de l' usine sur un AR à la place:cashDrop = driver.dropCash(...)
.Réponses:
J'accepte que ce que vous validez sera différent dans chaque couche de l'application. En règle générale, je ne valide que ce qui est nécessaire pour exécuter le code dans la méthode actuelle. J'essaie de traiter les composants sous-jacents comme des boîtes noires et je ne valide pas en fonction de la façon dont ces composants sont mis en œuvre.
Ainsi, à titre d'exemple, dans votre classe CashDropApi, je vérifierais seulement que «contrat» n'est pas nul. Cela empêche NullReferenceExceptions et est tout ce qui est nécessaire pour garantir que cette méthode s'exécute correctement.
Je ne sais pas que je validerais quoi que ce soit dans les classes de service ou de commande et le gestionnaire vérifierait seulement que «commande» n'est pas nulle pour les mêmes raisons que dans la classe CashDropApi. J'ai vu (et fait) la validation dans les deux sens par rapport aux classes d'usine et d'entité. L'un ou l'autre est l'endroit où vous souhaitez valider la valeur de «montant» et que les autres paramètres ne sont pas nuls (vos règles métier).
Le référentiel ne doit valider que la cohérence des données contenues dans l'objet avec le schéma défini dans votre base de données et l'opération daa réussira. Par exemple, si vous avez une colonne qui ne peut pas être nulle ou a une longueur maximale, etc.
Quant au contrôle de sécurité, je pense que c'est vraiment une question d'intention. Étant donné que la règle vise à empêcher tout accès non autorisé, je voudrais effectuer cette vérification le plus tôt possible dans le processus afin de réduire le nombre de mesures inutiles que j'ai prises si l'utilisateur n'est pas autorisé. Je l'aurais probablement mis dans le CashDropApi.
la source
Votre première règle commerciale
ressemble à un invariant de votre
CashDrop
entité et de votreAddCashDropCommand
classe. Il y a deux façons d'imposer un invariant comme celui-ci:Votre deuxième règle est de nature plus large (à la lumière des détails de la question): est-ce que cela signifie que l'entité Conducteur a un drapeau indiquant qu'elle peut conduire (c'est-à-dire que son permis de conduire n'a pas été suspendu), cela signifie-t-il que le conducteur était fonctionne réellement ce jour-là ou cela signifie-t-il simplement que le driverId, transmis au CashDropApi, est valide dans le magasin de persistance.
Dans tous ces cas, vous devrez naviguer dans votre modèle de domaine et obtenir l'
Driver
instance de votreIEmployeeRepository
, comme vous le faiteslocation 4
dans votre exemple de code. Donc, ici, vous devez vous assurer que l'appel au référentiel ne retourne pas null, auquel cas votre driverId n'était pas valide et vous ne pouvez plus poursuivre le traitement.Pour les 2 autres (mon hypothétique) vérification (le conducteur a-t-il un permis de conduire valide, le conducteur travaillait-il aujourd'hui), vous exécutez des règles commerciales.
Ce que j'ai tendance à faire ici, c'est d'utiliser une collection de classes de validation qui opèrent sur des entités (tout comme le modèle de spécification du livre d'Eric Evans - Domain Driven Design). j'ai utilisé FluentValidation pour construire ces règles et validateurs. Je peux alors composer (et donc réutiliser) des règles plus complexes / plus complètes à partir de règles plus simples. Et je peux décider quelles couches de mon architecture les exécuter. Mais je les ai tous encodés en un seul endroit, pas dispersés à travers le système.
Votre troisième règle concerne une préoccupation transversale: l'autorisation. Étant donné que vous utilisez déjà un conteneur IoC (en supposant que votre conteneur IoC prend en charge l'interception de méthode), vous pouvez effectuer un AOP . Écrivez un apsect qui fait l'autorisation et vous pouvez utiliser votre conteneur IoC pour injecter ce comportement d'autorisation là où il doit être. La grande victoire ici est que vous avez écrit la logique une fois, mais vous pouvez la réutiliser sur votre système.
Pour utiliser l'interception via un proxy dynamique (Castle Windsor, Spring.NET, Ninject 3.0, etc.), votre classe cible doit implémenter une interface ou hériter d'une classe de base. Vous intercepteriez avant l'appel à la méthode cible, vérifieriez l'autorisation de l'utilisateur et empêcheriez l'appel de passer à la méthode réelle (lancer une exception, consigner, renvoyer une valeur indiquant l'échec, ou autre chose) si l'utilisateur n'a pas les bons rôles pour effectuer l'opération.
Dans votre cas, vous pouvez intercepter l'appel vers
Les problèmes ici
CashDropService
ne peuvent peut-être pas être interceptés car il n'y a pas d'interface / classe de base. OuAddCashDropCommandHandler
n'est pas créé par votre IoC, donc votre IoC ne peut pas créer de proxy dynamique pour intercepter l'appel. Spring.NET a une fonctionnalité utile où vous pouvez cibler une méthode sur une classe dans un assembly via une expression régulière, donc cela peut fonctionner.J'espère que cela vous donnera quelques idées.
la source
Pour les règles:
Je ferais une validation à l'emplacement (1) pour la règle métier (1) et je m'assurerais que l'ID n'est pas nul ou négatif (en supposant que zéro est valide) comme pré-vérification de la règle (2). Les raisons sont ma règle de "Ne pas franchir une limite de couche avec des données erronées que vous pouvez vérifier avec les informations disponibles". Une exception à cette règle serait si le service effectue la validation dans le cadre de son devoir envers les autres appelants. Dans ce cas, il suffira d'avoir la validation uniquement là.
Pour les règles (2) et (3), cela doit être fait au niveau de la couche d'accès à la base de données (ou de la couche db elle-même) uniquement car cela implique un accès db. Pas besoin de voyager intentionnellement entre les couches.
En particulier, la règle (3) peut être évitée si nous laissons l'interface graphique empêcher les utilisateurs non autorisés d'appuyer sur le bouton permettant ce scénario. Bien que cela soit plus difficile à coder, c'est mieux.
Bonne question!
la source