Validierung und Autorisierung in Schichtenarchitektur

13

Ich weiß, dass Sie denken (oder vielleicht schreien), "nicht eine andere Frage, wo Validierung in einer geschichteten Architektur gehört?!?" Nun ja, aber hoffentlich wird dies eine etwas andere Sicht auf das Thema sein.

Ich bin der festen Überzeugung, dass die Validierung viele Formen annimmt, kontextabhängig ist und auf jeder Ebene der Architektur variiert. Auf dieser Grundlage kann nachträglich ermittelt werden, welche Art von Validierung in den einzelnen Ebenen durchgeführt werden soll. Außerdem stellt sich häufig die Frage, wo Berechtigungsprüfungen angesiedelt sind.

Das Beispielszenario stammt aus einer Anwendung für ein Catering-Unternehmen. Während des Tages kann ein Fahrer in regelmäßigen Abständen überschüssiges Bargeld im Büro abgeben, das er gesammelt hat, während er den Lastwagen von Ort zu Ort gebracht hat. Die Anwendung ermöglicht es einem Benutzer, den "Geldverlust" durch Sammeln des Fahrerausweises und des Betrags aufzuzeichnen. Hier ist ein Code, der die beteiligten Ebenen veranschaulicht:

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
        ...
    }
}

Ich habe 10 Stellen angegeben, an denen ich Validierungsprüfungen im Code gesehen habe. Meine Frage ist, welche Überprüfungen Sie gegebenenfalls bei den folgenden Geschäftsregeln durchführen würden (zusammen mit Standardüberprüfungen für Länge, Bereich, Format, Typ usw.):

  1. Der Betrag des Bargeldabfalls muss größer als Null sein.
  2. Die Kasse muss einen gültigen Fahrer haben.
  3. Der aktuelle Benutzer muss berechtigt sein, Geldeinzahlungen hinzuzufügen (der aktuelle Benutzer ist nicht der Fahrer).

Bitte teilen Sie Ihre Gedanken mit, wie Sie dieses Szenario angehen oder angehen würden und welche Gründe für Ihre Wahl sprechen.

SonOfPirate
quelle
SE ist nicht gerade die richtige Plattform, um "eine theoretische und subjektive Diskussion zu fördern". Abstimmung zum Abschluss.
Tdammers
Schlecht formulierte Aussage. Ich bin wirklich auf der Suche nach Best Practices.
SonOfPirate
2
@tdammers - Ja, es ist der richtige Ort. Zumindest will es sein. Aus der FAQ: "Subjektive Fragen sind erlaubt." Das ist der Grund, warum sie diese Site anstelle von Stack Overflow erstellt haben. Sei kein enger Nazi. Wenn die Frage scheitert, wird sie in Vergessenheit geraten.
FastAl
@FastAI: Es ist nicht so sehr der "subjektive" Teil, sondern vielmehr die "Diskussion", die mich stört.
Tdammers
Ich denke, dass Sie hier Wertobjekte nutzen können, indem Sie ein CashDropAmountWertobjekt haben, anstatt ein Decimal. Die Überprüfung, ob der Treiber vorhanden ist oder nicht, wird im Befehlshandler durchgeführt, und dies gilt auch für die Autorisierungsregeln. Sie können die Autorisierung kostenlos erhalten, indem Approver approver = approverService.findById(employeeId)Sie eine Aktion ausführen, bei der der Mitarbeiter nicht die Rolle des Genehmigenden übernimmt. Approverwäre nur ein Wertobjekt, keine Entität. Sie können auch Ihre Fabrik oder Verwendung Factory - Methode auf einem AR statt loszuwerden: cashDrop = driver.dropCash(...).
Plalx

Antworten:

2

Ich bin damit einverstanden, dass das, was Sie validieren, in jeder Ebene der Anwendung anders sein wird. Normalerweise überprüfe ich nur, was erforderlich ist, um den Code in der aktuellen Methode auszuführen. Ich versuche, die zugrunde liegenden Komponenten als Black Boxes zu behandeln und nicht anhand der Implementierung dieser Komponenten zu validieren.

Als Beispiel würde ich in Ihrer CashDropApi-Klasse nur überprüfen, ob 'contract' nicht null ist. Dies verhindert NullReferenceExceptions und ist alles, was erforderlich ist, um sicherzustellen, dass diese Methode ordnungsgemäß ausgeführt wird.

Ich weiß nicht, dass ich irgendetwas in den Service- oder Befehlsklassen validieren würde, und der Handler würde nur überprüfen, dass 'command' aus den gleichen Gründen wie in der CashDropApi-Klasse nicht null ist. Ich habe gesehen (und getan), wie die Validierung sowohl für die Factory- als auch für die Entity-Klasse durchgeführt wurde. Auf der einen oder anderen Seite möchten Sie den Wert von 'amount' überprüfen und sicherstellen, dass die anderen Parameter nicht null sind (Ihre Geschäftsregeln).

Das Repository sollte nur überprüfen, ob die im Objekt enthaltenen Daten mit dem in Ihrer Datenbank definierten Schema übereinstimmen, und die Daa-Operation wird erfolgreich ausgeführt. Zum Beispiel, wenn Sie eine Spalte haben, die nicht null sein kann oder eine maximale Länge hat, usw.

Was die Sicherheitskontrolle angeht, denke ich, dass es wirklich eine Frage der Absicht ist. Da die Regel den unbefugten Zugriff verhindern soll, möchte ich diese Prüfung so früh wie möglich durchführen, um die Anzahl der unnötigen Schritte zu verringern, die ich unternommen habe, wenn der Benutzer nicht autorisiert ist. Ich würde es wahrscheinlich in die CashDropApi setzen.

jpm70
quelle
1

Ihre erste Geschäftsregel

Der Betrag des Bargeldabfalls muss größer als Null sein.

sieht aus wie eine Invariante Ihrer CashDropEntität und Ihrer AddCashDropCommandKlasse. Es gibt verschiedene Möglichkeiten, wie ich eine Invariante wie diese erzwinge:

  1. Nehmen Sie die Route Design By Contract und verwenden Sie je nach Fall Code Contracts mit einer Kombination aus Vorbedingungen, Nachbedingungen und einer [ContractInvariantMethod].
  2. Schreiben Sie expliziten Code in den Konstruktor / Setter, der eine ArgumentException auslöst, wenn Sie eine Menge übergeben, die kleiner als 0 ist.

Ihre zweite Regel ist allgemeiner Natur (in Anbetracht der Details in der Frage): Bedeutet gültig, dass die Entität Fahrer eine Flagge hat, die angibt, dass sie fahren können (dh, ihr Führerschein wurde nicht ausgesetzt), bedeutet dies, dass der Fahrer war An diesem Tag tatsächlich arbeiten oder bedeutet dies einfach, dass die an CashDropApi übergebene driverId im Persistenzspeicher gültig ist.

In jedem dieser Fälle müssen Sie in Ihrem Domain-Modell navigieren und die DriverInstanz von Ihrem abrufen IEmployeeRepository, wie Sie es location 4in Ihrem Codebeispiel tun . Hier müssen Sie also sicherstellen, dass der Aufruf des Repositorys nicht null zurückgibt. In diesem Fall war Ihre driverId ungültig und Sie können die Verarbeitung nicht fortsetzen.

Für die anderen 2 (meine hypothetischen) Prüfungen (hat der Fahrer einen gültigen Führerschein, hat der Fahrer heute gearbeitet) führen Sie Geschäftsregeln aus.

Ich tendiere dazu, hier eine Sammlung von Validierungsklassen zu verwenden, die Entitäten verarbeiten (genau wie das Spezifikationsmuster aus Eric Evans Buch - Domain Driven Design). Ich habe FluentValidation verwendet , um diese Regeln und Validatoren zu erstellen. Ich kann dann komplexere / vollständigere Regeln aus einfacheren Regeln zusammenstellen (und daher wiederverwenden). Und ich kann entscheiden, auf welchen Ebenen in meiner Architektur sie ausgeführt werden sollen. Aber ich habe sie alle an einem Ort verschlüsselt, nicht über das System verteilt.

Ihre dritte Regel bezieht sich auf ein Querschnittsthema: die Autorisierung. Da Sie bereits einen IoC-Container verwenden (vorausgesetzt, Ihr IoC-Container unterstützt das Abfangen von Methoden), können Sie einige AOPs durchführen . Schreiben Sie einen Apsect, der die Autorisierung ausführt, und Sie können Ihren IoC-Container verwenden, um dieses Autorisierungsverhalten dort einzufügen, wo es sein muss. Der große Vorteil dabei ist, dass Sie die Logik einmal geschrieben haben, sie aber systemweit wiederverwenden können.

Um das Abfangen über einen dynamischen Proxy (Castle Windsor, Spring.NET, Ninject 3.0 usw.) zu verwenden, muss Ihre Zielklasse eine Schnittstelle implementieren oder von einer Basisklasse erben. Sie würden vor dem Aufruf der Zielmethode abfangen, die Berechtigung des Benutzers überprüfen und verhindern, dass der Aufruf zur eigentlichen Methode übergeht (eine Ausnahme auslösen, protokollieren, einen Wert zurückgeben, der auf einen Fehler hinweist, oder etwas anderes), wenn der Benutzer dies nicht hat die richtigen Rollen, um die Operation durchzuführen.

In Ihrem Fall könnten Sie den Anruf entweder abfangen

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Probleme können hier vielleicht CashDropServicenicht abgefangen werden, weil es keine Schnittstelle / Basisklasse gibt. Oder AddCashDropCommandHandlerwird nicht von Ihrem IoC erstellt, daher kann Ihr IoC keinen dynamischen Proxy zum Abfangen des Anrufs erstellen. Spring.NET verfügt über eine nützliche Funktion, mit der Sie eine Methode für eine Klasse in einer Assembly über einen regulären Ausdruck als Ziel festlegen können.

Hoffe, das gibt Ihnen einige Ideen.

RobertMS
quelle
Können Sie erklären, wie ich "Ihren IoC-Container verwenden würde, um dieses Autorisierungsverhalten dort einzufügen, wo es sein muss"? Das klingt ansprechend, aber die Zusammenarbeit von AOP und IoC entgeht mir bislang.
SonOfPirate
Im Übrigen bin ich damit einverstanden, dass die Validierung im Konstruktor und / oder Setter platziert wird, um zu verhindern, dass das Objekt in einen ungültigen Zustand übergeht (Umgang mit Invarianten). Abgesehen davon und einem Verweis auf die Nullprüfung nach dem Auffinden des Treibers im IEmployeeRepository geben Sie keine Details an, an denen Sie den Rest der Validierung durchführen würden. Wo würden Sie angesichts der Verwendung von FluentValidation und der darin enthaltenen Wiederverwendung usw. die Regeln im angegebenen Modell anwenden?
SonOfPirate,
Ich habe meine Antwort bearbeitet - sehen Sie, ob dies hilft. Wie für "wo würden Sie die Regeln in dem gegebenen Modell anwenden?"; wahrscheinlich um 4, 5, 6, 7 in Ihrem Befehlshandler. Sie haben Zugriff auf die Repositorys, die die Informationen enthalten, die Sie für die Validierung auf Unternehmensebene benötigen. Aber ich denke, es gibt andere, die mir hier nicht zustimmen würden.
RobertMS
Zur Verdeutlichung werden alle Abhängigkeiten injiziert. Ich habe das weggelassen, um den Referenzcode kurz zu halten. Meine Anfrage hat mehr mit einer Abhängigkeit innerhalb des Aspekts zu tun, da Aspekte nicht über den Container injiziert werden. Wie erhält der AuthorizationAspect beispielsweise einen Verweis auf den AuthorizationService?
SonOfPirate
1

Für die Regeln:

1- Der Betrag des Bargeldabfalls muss größer als Null sein.

2- Die Kasse muss einen gültigen Fahrer haben.

3- Der aktuelle Benutzer muss berechtigt sein, Geldeinzahlungen hinzuzufügen (der aktuelle Benutzer ist nicht der Fahrer).

Ich würde eine Validierung an Ort (1) für die Geschäftsregel (1) durchführen und sicherstellen, dass die ID nicht null oder negativ ist (unter der Annahme, dass null gültig ist), als Vorabprüfung für die Regel (2). Der Grund ist meine Regel: "Überschreiten Sie keine Ebenengrenze mit falschen Daten, die Sie anhand der verfügbaren Informationen überprüfen können". Eine Ausnahme wäre, wenn der Dienst die Validierung im Rahmen seiner Pflicht gegenüber anderen Anrufern durchführt. In diesem Fall reicht es aus, die Validierung nur dort zu haben.

Bei den Regeln (2) und (3) darf dies nur auf der Datenbankzugriffsebene (oder auf der Datenbankebene selbst) erfolgen, da es sich um einen Datenbankzugriff handelt. Sie müssen nicht absichtlich zwischen den Schichten wechseln.

Insbesondere Regel (3) kann vermieden werden, wenn die GUI verhindert, dass nicht autorisierte Benutzer den Knopf drücken, der dieses Szenario aktiviert. Das ist zwar schwieriger zu programmieren, aber besser.

Gute Frage!

Keine Chance
quelle
+1 für die Autorisierung - das Einfügen in die Benutzeroberfläche ist eine Alternative, die ich in meiner Antwort nicht erwähnt habe.
RobertMS
Während Berechtigungsprüfungen in der Benutzeroberfläche eine interaktivere Erfahrung für den Benutzer darstellen, entwickle ich eine service-basierte API und kann keine Annahmen darüber treffen, welche Regeln der Aufrufer implementiert hat oder nicht. Weil so viele dieser Prüfungen einfach an die Benutzeroberfläche delegiert werden können, habe ich mich entschieden, das API-Projekt als Grundlage für den Beitrag zu verwenden. Ich bin eher auf der Suche nach Best Practices als nach Lehrbüchern.
SonOfPirate
@SonOfPirate, INMO, die Benutzeroberfläche muss überprüft werden, da sie schneller ist und mehr Daten enthält als der Dienst (in einigen Fällen). Jetzt sollte der Dienst keine Daten außerhalb seiner Grenzen senden, ohne eigene Überprüfungen durchzuführen, da dies Teil seiner Verantwortung ist, solange Sie möchten, dass der Dienst dem Client nicht vertraut. Dementsprechend schlage ich vor, dass (erneut) Nicht-DB-Prüfungen im Service durchgeführt werden, bevor Daten zur weiteren Verarbeitung an die Datenbank gesendet werden.
NoChance