Validation et autorisation dans une architecture en couches

13

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.):

  1. Le montant de la remise en espèces doit être supérieur à zéro.
  2. La remise en espèces doit avoir un chauffeur valide.
  3. 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.

SonOfPirate
la source
SE n'est pas exactement la bonne plateforme pour «favoriser une discussion théorique et subjective». Voter pour clore.
tdammers
Déclaration mal formulée. Je recherche vraiment les meilleures pratiques.
SonOfPirate
2
@tdammers - Oui, c'est le bon endroit. Au moins, il veut l'être. De la FAQ: «Les questions subjectives sont autorisées.» C'est pourquoi ils ont créé ce site au lieu de Stack Overflow. Ne soyez pas un nazi proche. Si la question craint, elle disparaîtra dans l'obscurité.
FastAl
@FastAI: Ce n'est pas tant la partie «subjective», mais plutôt la «discussion» qui me dérange.
tdammers
Je pense que vous pourriez tirer parti des objets de valeur ici en ayant un CashDropAmountobjet de valeur plutôt qu'en utilisant un Decimal. 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 comme Approver approver = approverService.findById(employeeId)où elle est lancée si l'employé n'est pas dans le rôle d'approbateur. Approverserait 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(...).
plalx

Réponses:

2

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.

jpm70
la source
1

Votre première règle commerciale

Le montant de la remise en espèces doit être supérieur à zéro.

ressemble à un invariant de votre CashDropentité et de votre AddCashDropCommandclasse. Il y a deux façons d'imposer un invariant comme celui-ci:

  1. Prenez la route Design By Contract et utilisez les contrats de code avec une combinaison de conditions préalables, de postconditions et d'une [ContractInvariantMethod] selon votre cas.
  2. Écrivez du code explicite dans le constructeur / setters qui lève une ArgumentException si vous passez un montant inférieur à 0.

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' Driverinstance de votre IEmployeeRepository, comme vous le faites location 4dans 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

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Les problèmes ici CashDropServicene peuvent peut-être pas être interceptés car il n'y a pas d'interface / classe de base. Ou AddCashDropCommandHandlern'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.

RobertMS
la source
Pouvez-vous expliquer comment j'utiliserais votre conteneur IoC pour injecter ce comportement d'autorisation là où il doit être? Cela semble attrayant, mais faire fonctionner ensemble AOP et IoC m'échappe jusqu'à présent.
SonOfPirate
Pour le reste, je suis d'accord pour placer la validation dans le constructeur et / ou les setters pour empêcher l'objet d'entrer dans un état invalide (gestion des invariants). Mais au-delà de cela et une référence à la vérification nulle après être allé à IEmployeeRepository pour localiser le pilote, vous ne fournissez aucun détail où vous effectueriez le reste de la validation. Compte tenu de l'utilisation de FluentValidation et de la réutilisation, etc. qu'elle fournit, où appliqueriez-vous les règles dans le modèle donné?
SonOfPirate
J'ai modifié ma réponse - voyez si cela aide. Quant à "où appliqueriez-vous les règles dans le modèle donné?"; probablement environ 4, 5, 6, 7 dans votre gestionnaire de commandes. Vous avez accès aux référentiels qui peuvent fournir les informations dont vous avez besoin pour effectuer la validation au niveau de l'entreprise. Mais je pense qu'il y en a d'autres qui seraient en désaccord avec moi ici.
RobertMS
Pour clarifier, toutes les dépendances sont injectées. Je l'ai laissé pour garder le code de référence bref. Mon enquête a plus à voir avec une dépendance au sein de l'aspect puisque les aspects ne sont pas injectés via le conteneur. Alors, comment l'AutorizationAspect obtient-il une référence au AuthorizationService, par exemple?
SonOfPirate
1

Pour les règles:

1- Le montant de la goutte d'argent doit être supérieur à zéro.

2- La goutte d'argent doit avoir un chauffeur valide.

3- L'utilisateur actuel doit être autorisé à ajouter des espèces (l'utilisateur actuel n'est pas le conducteur).

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!

Aucune chance
la source
+1 pour l'autorisation - le mettre dans l'interface utilisateur est une alternative que je n'ai pas mentionnée dans ma réponse.
RobertMS
Bien que la vérification des autorisations dans l'interface utilisateur offre une expérience plus interactive pour l'utilisateur, je développe une API basée sur les services et je ne peux faire aucune hypothèse sur les règles que l'appelant a ou n'a pas mises en œuvre. C'est parce que beaucoup de ces vérifications peuvent être facilement déléguées à l'interface utilisateur que j'ai choisi d'utiliser le projet API comme base pour la publication. Je recherche les meilleures pratiques plutôt que des manuels rapides et faciles.
SonOfPirate
@SonOfPirate, INMO, l'interface utilisateur doit effectuer des validations car elle est plus rapide et contient plus de données que le service (dans certains cas). Désormais, le service ne doit pas envoyer de données en dehors de ses limites sans effectuer ses propres validations, car cela fait partie de ses responsabilités tant que vous souhaitez que le service ne fasse pas confiance au client. En conséquence, je suggère que des vérifications non-db soient effectuées (à nouveau) dans le service avant d'envoyer des données à la base de données pour un traitement ultérieur.
NoChance