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.):
- Der Betrag des Bargeldabfalls muss größer als Null sein.
- Die Kasse muss einen gültigen Fahrer haben.
- 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.
quelle
CashDropAmount
Wertobjekt haben, anstatt einDecimal
. 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, indemApprover approver = approverService.findById(employeeId)
Sie eine Aktion ausführen, bei der der Mitarbeiter nicht die Rolle des Genehmigenden übernimmt.Approver
wä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(...)
.Antworten:
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.
quelle
Ihre erste Geschäftsregel
sieht aus wie eine Invariante Ihrer
CashDrop
Entität und IhrerAddCashDropCommand
Klasse. Es gibt verschiedene Möglichkeiten, wie ich eine Invariante wie diese erzwinge: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
Driver
Instanz von Ihrem abrufenIEmployeeRepository
, wie Sie eslocation 4
in 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
Probleme können hier vielleicht
CashDropService
nicht abgefangen werden, weil es keine Schnittstelle / Basisklasse gibt. OderAddCashDropCommandHandler
wird 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.
quelle
Für die Regeln:
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!
quelle