Wo sollten wir die Validierung für Domain-Modell setzen

38

Ich suche immer noch nach bewährten Methoden für die Validierung des Domain-Modells. Ist das gut, um die Validierung im Konstruktor des Domain-Modells zu platzieren? Beispiel für die Validierung meines Domain-Modells:

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

Vielen Dank für all Ihren Vorschlag.

adisembiring
quelle

Antworten:

47

Es gibt einen interessanten Artikel von Martin Fowler zu diesem Thema, der einen Aspekt hervorhebt, den die meisten Leute (einschließlich ich) gerne übersehen:

Aber eine Sache, die ich denke, die die Leute ständig auslöst, ist, wenn sie denken, dass die Gültigkeit von Objekten kontextunabhängig ist, wie es eine isValid-Methode impliziert.

Ich denke, es ist viel nützlicher, sich Validierung als etwas vorzustellen, das an einen Kontext gebunden ist - normalerweise eine Aktion, die Sie ausführen möchten. Ist diese Bestellung gültig um ausgeführt zu werden, ist dieser Kunde gültig um im Hotel einzuchecken. Anstatt Methoden wie isValid zu haben, sollten Sie Methoden wie isValidForCheckIn haben.

Daraus folgt , dass der Konstruktor sollte nicht Validierung tun, außer vielleicht einige sehr grundlegende geistige Gesundheit von allen Kontexten geteilt zu überprüfen.

Nochmals aus dem Artikel:

In About Face befürwortete Alan Cooper, dass wir nicht zulassen sollten, dass unsere Vorstellungen von gültigen Status einen Benutzer daran hindern, unvollständige Informationen einzugeben (und zu speichern). Das hat mich vor ein paar Tagen daran erinnert, als ich einen Entwurf eines Buches las, an dem Jimmy Nilsson arbeitet. Er gab ein Prinzip an, dass Sie ein Objekt immer speichern können sollten, auch wenn es Fehler enthält. Ich bin zwar nicht davon überzeugt, dass dies eine absolute Regel sein sollte, aber ich glaube, die Menschen neigen dazu, das Sparen mehr zu verhindern, als sie sollten. Das Nachdenken über den Kontext für die Validierung kann dies verhindern.

Michael Borgwardt
quelle
Gott sei Dank hat das jemand gesagt. Formulare, die 90% der Daten enthalten, aber nichts speichern, sind unfair gegenüber Benutzern, die oftmals die anderen 10% ausmachen, nur um keine Daten zu verlieren. Die Validierung hat also lediglich dazu geführt, dass das System den Überblick verliert, von denen 10% wurde erfunden. Ähnliche Probleme können im Backend auftreten, beispielsweise beim Datenimport. Ich habe festgestellt, dass es in der Regel besser ist, mit ungültigen Daten ordnungsgemäß zu arbeiten, als zu verhindern, dass sie jemals auftreten.
PSR
2
@psr Benötigen Sie überhaupt eine Back-End-Logik, wenn Ihre Daten nicht persistent sind? Sie können alle Manipulationen auf der Client-Seite belassen, wenn Ihre Daten für Ihr Geschäftsmodell keine Bedeutung haben. Es wäre auch eine Verschwendung von Ressourcen, Nachrichten hin und her zu senden (Client - Server), wenn die Daten bedeutungslos sind. Wir kehren also zu der Idee zurück, "niemals zuzulassen, dass Domain-Objekte in einen ungültigen Zustand versetzt werden!" .
Geo C.
2
Ich frage mich, warum so viele Stimmen für eine so zweideutige Antwort stimmen. Bei der Verwendung von DDD gibt es manchmal einige Regeln, die einfach prüfen, ob einige Daten INT sind oder in einem Bereich liegen. Zum Beispiel, wenn Sie Ihrem App-Benutzer gestatten, bestimmte Einschränkungen für seine Produkte festzulegen (wie oft kann jemand eine Vorschau meines Produkts anzeigen und in welchen Tagen im Abstand von einem Monat). Hier sollten beide Bedingungen int sein und eine davon sollte im Bereich von 0-31 liegen. Dies scheint eine Datenformatüberprüfung zu sein, die in einer Nicht-DDD-Umgebung in einen Dienst oder Controller passt. Aber bei DDD bin ich auf der Seite, die Gültigkeit in der Domäne zu halten (90% davon).
Geo C.
2
Es riecht nach schlechtem Design, wenn die oberen Schichten zu viel über die Domäne wissen, um sie in einem gültigen Zustand zu halten. Die Domain sollte diejenige sein, die garantiert, dass ihr Status gültig ist. Wenn Sie zu viel auf den Schultern der oberen Ebenen bewegen, kann Ihre Domain anämisch werden, und Sie könnten einige wichtige Einschränkungen umgehen, die Ihrem Unternehmen schaden könnten. Was ich jetzt erkenne, wäre eine richtige Verallgemeinerung, Ihre Validierung so nah wie möglich an Ihrer Persistenz zu halten oder so nah wie möglich an Ihrem Datenmanipulationscode (wenn er manipuliert wird, um einen endgültigen Zustand zu erreichen).
Geo C.
PS Ich mische keine Autorisierung (darf etwas tun), Authentifizierung (kam die Nachricht vom richtigen Ort oder wurde vom richtigen Client gesendet, wobei beide durch einen API-Schlüssel / Token / Benutzernamen oder irgendetwas anderes identifiziert wurden) mit Formatüberprüfung oder Geschäftsregeln. Wenn ich 90% sage, meine ich jene Geschäftsregeln, die die meisten von ihnen auch eine Formatvalidierung beinhalten. Natürlich kann die Formatüberprüfung in höheren Ebenen erfolgen, die meisten befinden sich jedoch in der Domäne (sogar das E-Mail-Adressformat, das im Wertobjekt "EmailAddress" überprüft wird).
Geo C.
5

Trotz der Tatsache, dass diese Frage etwas abgestanden ist, möchte ich noch etwas hinzufügen, das sich lohnt:

Ich möchte @MichaelBorgwardt zustimmen und die Testbarkeit erweitern. In "Effektiv mit Legacy-Code arbeiten" spricht Michael Feathers viel über Hindernisse beim Testen, und eines dieser Hindernisse ist "schwer zu konstruierende" Objekte. Das Konstruieren eines ungültigen Objekts sollte möglich sein, und wie Fowler vorschlägt, sollten kontextabhängige Gültigkeitsprüfungen in der Lage sein, diese Bedingungen zu identifizieren. Wenn Sie nicht herausfinden können, wie Sie ein Objekt in einem Testgeschirr konstruieren, können Sie Ihre Klasse nur schwer testen.

Bezüglich der Gültigkeit denke ich gerne an Steuerungssysteme. Steuerungssysteme analysieren ständig den Zustand eines Ausgangs und ergreifen Korrekturmaßnahmen, wenn der Ausgang vom Sollwert abweicht. Dies wird als Regelung bezeichnet. Die Regelung erwartet von sich aus Abweichungen und korrigiert diese. So funktioniert die reale Welt. Deshalb verwenden alle realen Steuerungssysteme in der Regel Regler.

Ich denke, dass die Verwendung von kontextabhängiger Validierung und einfach zu konstruierenden Objekten die Arbeit mit Ihrem System später erleichtert.

Paul
quelle
1
Oft erscheinen Objekte nur schwer zu konstruieren. In diesem Fall können Sie beispielsweise den öffentlichen Konstruktor umgehen, indem Sie eine Wrapper-Klasse erstellen, die von der getesteten Klasse erbt und das Erstellen einer Instanz des Basisobjekts in einem ungültigen Zustand ermöglicht. Hier kommt die Verwendung der richtigen Zugriffsmodifikatoren für Klassen und Konstruktoren ins Spiel, die sich bei unsachgemäßer Verwendung negativ auf das Testen auswirken können. Das Vermeiden von "versiegelten" Klassen und Methoden, sofern dies nicht angemessen ist, trägt wesentlich dazu bei, das Testen eines Codes zu vereinfachen.
P. Roe
4

Wie Sie sicher schon wissen ...

Bei der objektorientierten Programmierung ist ein Konstruktor (manchmal auf ctor abgekürzt) in einer Klasse eine spezielle Art von Unterroutine, die beim Erstellen eines Objekts aufgerufen wird. Es bereitet das neue Objekt für die Verwendung vor und akzeptiert häufig Parameter, die der Konstruktor verwendet, um alle Mitgliedsvariablen festzulegen, die erforderlich sind, wenn das Objekt zum ersten Mal erstellt wird. Es wird als Konstruktor bezeichnet, da es die Werte von Datenelementen der Klasse erstellt.

Die Überprüfung der Gültigkeit der als c'tor-Parameter übergebenen Daten ist im Konstruktor definitiv gültig. Andernfalls können Sie möglicherweise die Erstellung eines ungültigen Objekts zulassen .

Allerdings (und dies ist nur meine Meinung, ich kann derzeit noch keine guten Dokumente finden) - wenn die Datenüberprüfung komplexe Vorgänge erfordert (z. B. asynchrone Vorgänge - möglicherweise serverbasierte Überprüfung bei der Entwicklung einer Desktop-App), ist es besser nullGeben Sie eine Art Initialisierungs- oder explizite Validierungsfunktion ein, und setzen Sie die Elemente in c'tor auf Standardwerte (z. B. ).


Auch nur als Randnotiz, wie Sie es in Ihrem Codebeispiel aufgenommen haben ...

Sofern Sie keine weitere Validierung (oder andere Funktionen) in durchführen AddOrderLine, würde ich die höchstwahrscheinlich List<LineItem>als eine Eigenschaft aussetzen, anstatt als Fassade zuOrder fungieren .

Demian Brecht
quelle
Warum den Behälter aussetzen? Was bedeutet es für die oberen Schichten, was der Container ist? Es ist durchaus sinnvoll, eine AddLineItemMethode zu haben . Tatsächlich ist dies für DDD bevorzugt. Wenn List<LineItem>in ein benutzerdefiniertes Auflistungsobjekt geändert wird, können sich die offen gelegte Eigenschaft und alle von einer List<LineItem>Eigenschaft abhängigen Änderungen, Fehler und Ausnahmen ergeben.
IAbstrakter
4

Die Validierung sollte so bald wie möglich durchgeführt werden.

Die Validierung in jedem Kontext, unabhängig vom Domain-Modell oder einer anderen Art, Software zu schreiben, sollte dem Zweck dienen, WAS Sie validieren möchten und auf welcher Ebene Sie sich gerade befinden.

Aufgrund Ihrer Frage würde die Antwort wohl darin bestehen, die Validierung aufzuteilen.

  1. Die Eigenschaftsüberprüfung prüft, ob der Wert für diese Eigenschaft korrekt ist, z. B. wenn ein Bereich zwischen 1 und 10 erwartet wird.

  2. Die Objektvalidierung garantiert, dass alle Eigenschaften des Objekts in Verbindung miteinander gültig sind. zB BeginDate liegt vor EndDate. Angenommen, Sie lesen einen Wert aus dem Datenspeicher und sowohl BeginDate als auch EndDate werden standardmäßig mit DateTime.Min initialisiert. Beim Festlegen des BeginDate gibt es keinen Grund, die Regel "muss vor dem EndDate sein" durchzusetzen, da dies NOCH nicht zutrifft. Diese Regel sollte überprüft werden, nachdem alle Eigenschaften festgelegt wurden. Dies kann auf der aggregierten Stammebene aufgerufen werden

  3. Die Validierung sollte auch für die aggregierte (oder aggregierte Stamm-) Entität durchgeführt werden. Ein Order-Objekt kann gültige Daten enthalten, ebenso wie seine OrderLines. In einer Geschäftsregel heißt es jedoch, dass kein Auftrag mehr als 1.000 US-Dollar betragen darf. Wie würden Sie diese Regel in einigen Fällen durchsetzen, ist dies zulässig? Sie können nicht einfach eine Eigenschaft "Betrag nicht validieren" hinzufügen, da dies zu Missbrauch führen würde (früher oder später, vielleicht sogar Sie, nur um diese "böse Anfrage" aus dem Weg zu räumen).

  4. Als nächstes erfolgt die Validierung auf der Präsentationsebene. Wollen Sie das Objekt wirklich über das Netzwerk senden, wenn Sie wissen, dass es fehlschlagen wird? Oder ersparen Sie dem Benutzer diesen Aufwand und informieren ihn, sobald er einen ungültigen Wert eingibt. ZB ist Ihre DEV-Umgebung in den meisten Fällen langsamer als die Produktion. Möchten Sie 30 Sekunden warten, bevor Sie über "Sie haben dieses Feld WIEDER während eines weiteren Testlaufs vergessen" informiert werden, insbesondere wenn ein Produktionsfehler behoben werden muss, bei dem Ihr Chef Ihnen den Hals runter atmet?

  5. Die Validierung auf der Persistenzebene sollte der Validierung des Eigenschaftswerts so nahe wie möglich kommen. Dies hilft dabei, Ausnahmen beim Lesen von "Null" - oder "Ungültiger Wert" -Fehlern zu vermeiden, wenn Mapper jeglicher Art oder einfache alte Datenleser verwendet werden. Die Verwendung gespeicherter Prozeduren löst dieses Problem, erfordert jedoch, dass dieselbe Bewertungslogik WIEDER geschrieben und erneut ausgeführt wird. Gespeicherte Prozeduren sind die Domäne des DB-Administrators. Versuchen Sie also nicht, auch SEINE Arbeit zu erledigen (oder stören Sie ihn noch schlimmer mit dieser "netten Auswahl, für die er nicht bezahlt wird").

Um es mit einigen berühmten Worten zu sagen: "es kommt darauf an", aber spätestens jetzt wissen Sie, WARUM es darauf ankommt.

Ich wünschte, ich könnte das alles an einem einzigen Ort unterbringen, aber das ist leider nicht möglich. Dies würde eine Abhängigkeit von einem "Gott-Objekt" erzeugen, das die ALL-Validierung für ALLE Ebenen enthält. Du willst diesen dunklen Weg nicht gehen.

Aus diesem Grund werfe ich nur Validierungsausnahmen einer Eigenschaftsstufe. Alle anderen Ebenen Ich verwende ValidationResult mit einer IsValid-Methode, um alle "defekten Regeln" zu sammeln und sie in einer einzigen AggregateException an den Benutzer zu übergeben.

Bei der Weitergabe des Aufrufstapels sammle ich diese dann erneut in AggregateExceptions, bis ich die Präsentationsebene erreiche. Die Serviceebene kann diese Ausnahme im Fall von WCF als FaultException direkt an den Client senden.

Auf diese Weise kann ich die Ausnahme annehmen und sie entweder aufteilen, um einzelne Fehler bei jedem Eingabesteuerelement anzuzeigen, oder sie reduzieren und in einer einzelnen Liste anzeigen. Es ist deine Entscheidung.

Aus diesem Grund habe ich auch die Validierung der Präsentation erwähnt, um diese so kurz wie möglich zu halten.

Falls Sie sich fragen, warum ich die Validierung auch auf der Aggregationsebene (oder auf der Serviceebene, wenn Sie möchten) habe, liegt dies daran, dass ich keine Kristallkugel habe, die mir sagt, wer meine Services in Zukunft nutzen wird. Sie werden genug Probleme haben, Ihre eigenen Fehler zu finden, um zu verhindern, dass andere Ihre Fehler machen :), indem Sie ungültige Daten eingeben. Beispielsweise verwalten Sie Anwendung A, aber Anwendung B füttert einige Daten mit Ihrem Dienst. Ratet mal, wen sie zuerst fragen, wenn es einen Bug gibt? Der Administrator von Anwendung B teilt dem Benutzer mit, dass kein Fehler aufgetreten ist. Ich gebe nur die Daten ein.

Wesley Kenis
quelle