
Im Rahmen der jüngsten
DotNext 2018- Konferenz fand BoF on Domain Driven Design statt. Es befasste sich mit der Frage der Arbeit mit Ausnahmen, die eine hitzige Debatte auslösten, jedoch keine ausführliche Diskussion erhielten, da dies nicht das Hauptthema war.
Wenn Sie viele Ressourcen studieren, angefangen von Fragen zum Stackoverflow bis hin zu kostenpflichtigen Architekturkursen, können Sie außerdem feststellen, dass die IT-Community eine mehrdeutige Einstellung zu Ausnahmen und deren Verwendung hat.
Es wird am häufigsten erwähnt, dass es mit Ausnahmen einfach ist, einen Ausführungsthread zu erstellen, der eine
Operatorsemantik aufweist , was die Lesbarkeit des Codes beeinträchtigt.
Es gibt unterschiedliche Meinungen darüber, ob
Sie eigene Arten von Ausnahmen erstellen oder die in .NET bereitgestellten Standardausnahmen verwenden sollen.
Jemand überprüft Ausnahmen, und überall
verwendet jemand
die Ergebnismonade . Das Ergebnis ermöglicht es Ihnen, anhand der Methodensignatur zu verstehen, ob eine erfolgreiche Ausführung möglich ist oder nicht. Es ist jedoch nicht weniger wahr, dass in imperativen Sprachen (einschließlich C #) die weit verbreitete Verwendung von Result zu schlecht lesbarem Code führt, der mit Sprachkonstrukten bedeckt ist, so dass es schwierig ist, das ursprüngliche Skript zu erkennen.
In diesem Artikel werde ich über die Praktiken unseres Teams sprechen (kurz gesagt - wir verwenden alle Ansätze und keiner von ihnen ist ein Dogma).
Wir werden über eine Unternehmensanwendung sprechen, die auf der Basis von ASP.NET MVC + WebAPI erstellt wurde. Die Anwendung basiert auf einer
Zwiebelarchitektur und kommuniziert mit der Datenbank und dem Nachrichtenbroker. Es verwendet eine strukturierte Protokollierung im ELK-Stack und die Überwachung wird mit Grafana konfiguriert.
Wir werden die Arbeit mit Ausnahmen aus drei Perspektiven betrachten:
- Allgemeine Ausnahmeregeln
- Ausnahmen, Fehler und Zwiebelarchitektur
- Sonderfälle für Webanwendungen
Allgemeine Ausnahmeregeln
- Ausnahmen und Fehler sind nicht dasselbe. Für Ausnahmen verwenden wir Ausnahmen, für Fehler - Ergebnis.
- Ausnahmen gelten nur für Ausnahmesituationen, die per Definition nicht viele sein können. Je weniger Ausnahmen, desto besser.
- Die Ausnahmebehandlung sollte so detailliert wie möglich sein. Wie Richter in seinem monumentalen Werk schrieb.
- Wenn der Fehler in seiner ursprünglichen Form an den Benutzer übermittelt werden soll, verwenden Sie Ergebnis.
- Eine Ausnahme sollte die Grenzen des Systems nicht in seiner ursprünglichen Form belassen. Dies ist nicht benutzerfreundlich und gibt einem Angreifer die Möglichkeit, mögliche Schwachstellen im System weiter zu untersuchen.
- Wenn die ausgelöste Ausnahme von unserer Anwendung behandelt wird, verwenden wir nicht die Ausnahme, sondern das Ergebnis. Die Implementierung von Ausnahmen wird vom goto-Operator ausgeblendet. Je schlimmer sie ist, desto weiter entfernt ist der Verarbeitungscode vom Ausnahme-Auslösecode. Das Ergebnis deklariert explizit die Möglichkeit eines Fehlers und erlaubt nur dessen "lineare" Verarbeitung.
Ausnahmen, Fehler und Zwiebelarchitektur
In den folgenden Abschnitten werden die Verantwortlichkeiten und Regeln für das Auslösen / Behandeln von Ausnahmen / Fehlern für die folgenden Ebenen betrachtet:
- Anwendungshosts
- Infrastruktur
- Anwendungsdienste
- Domänenkern
Anwendungshost
Wofür ist verantwortlich?- Kompositionsstamm , der den Betrieb der gesamten Anwendung anpasst.
- Die Grenze der Interaktion mit der Außenwelt sind Benutzer, andere Dienste, geplanter Start.
Da dies recht komplexe Aufgaben sind, lohnt es sich, sich einzuschränken. Die verbleibenden Verantwortlichkeiten übertragen wir auf die inneren Schichten.
Wie man mit Fehlern aus dem Ergebnis umgehtSendungen nach außen, Konvertierung in das entsprechende Format (z. B. in http-Antwort).
Wie das Ergebnis generiert wirdAuf keinen Fall. Diese Schicht enthält keine Logik, daher können nirgendwo Fehler generiert werden.
Umgang mit Ausnahmen- Blendet Details aus und konvertiert sie in ein Format, das zum Senden an die Außenwelt geeignet ist
- Loggt sich ein.
Wie man Ausnahmen wirftAuf keinen Fall ist diese Ebene die externeste und enthält keine Logik - es gibt niemanden, der eine Ausnahme auslöst.
Infrastruktur
Wofür ist verantwortlich?- Adapter an Ports oder einfach zum Implementieren von Domänenschnittstellen, die den Zugriff auf die Infrastruktur ermöglichen - Dienste von Drittanbietern, Datenbanken, Active Directory usw. Diese Schicht sollte so dumm wie möglich sein und so wenig Logik wie möglich enthalten.
- Bei Bedarf kann es als Antikorruptionsschicht fungieren.
Wie man mit Fehlern aus dem Ergebnis umgehtIch kenne die Datenbankanbieter und andere Dienste, die auf der Ergebnismonade ausgeführt werden, nicht. Einige Dienste arbeiten jedoch mit Rückkehrcodes. In diesem Fall konvertieren wir sie in das vom Port geforderte Ergebnisformat.
Wie das Ergebnis generiert wirdIm Allgemeinen enthält diese Schicht keine Logik, was bedeutet, dass keine Fehler generiert werden. Bei Verwendung als Antikorruptionsschicht sind jedoch verschiedene Optionen möglich. Beispiel: Analysieren Sie Ausnahmen von einem Legacy-Service und konvertieren Sie diese Ausnahmen, die einfache Validierungsnachrichten sind, in Ergebnis.
Umgang mit AusnahmenIm allgemeinen Fall wirft es es gegebenenfalls weiter, nachdem es die Details gesichert hat. Wenn der zu implementierende Port die Rückgabe des Ergebnisses im Vertrag zulässt, konvertiert die Infrastruktur die Arten von Ausnahmen, die verarbeitet werden können, in Ergebnis.
Beispielsweise löst der im Projekt verwendete Nachrichtenbroker eine Ausnahme aus, wenn versucht wird, eine Nachricht zu senden, wenn der Broker nicht verfügbar ist. Die Application Services-Schicht ist für diese Situation bereit und kann sie mit einer Wiederholungsrichtlinie, einem Leistungsschalter oder einem manuellen Daten-Rollback verarbeiten.
In diesem Fall deklariert die Application Services-Schicht einen Vertrag, der im Fehlerfall das Ergebnis zurückgibt. Die Infrastrukturschicht implementiert diesen Port und konvertiert die Ausnahme vom Broker in das Ergebnis. Natürlich werden nur bestimmte Arten von Ausnahmen konvertiert und nicht alle hintereinander.
Mit diesem Ansatz erhalten wir zwei Vorteile:
- Erklären Sie ausdrücklich die Möglichkeit von Fehlern im Vertrag.
- Wir beseitigen die Situation, in der der Anwendungsdienst weiß, wie der Fehler zu behandeln ist, aber die Art der Ausnahme nicht kennt, da sie von einem bestimmten Nachrichtenbroker abstrahiert wird. Um einen Catch-Block auf der Basis von System.Exception zu erstellen, müssen alle Arten von Ausnahmen erfasst werden, nicht nur diejenigen, die der Anwendungsdienst verarbeiten kann.
Wie man Ausnahmen wirftHängt von den Besonderheiten des Systems ab.
Beispielsweise lösen die Anweisungen Single und First LINQ eine InvalidOperationException aus, wenn nicht vorhandene Daten angefordert werden. Diese Art von Ausnahme wird jedoch überall in .NET verwendet, sodass eine granulare Verarbeitung nicht möglich ist.
Wir im Team haben die Praxis übernommen, eine benutzerdefinierte ItemNotFoundException zu erstellen und diese aus der Infrastrukturschicht zu werfen, wenn die angeforderten Daten nicht gefunden wurden und nicht den Geschäftsregeln entsprechen sollten.
Wenn die angeforderten Daten nicht gefunden werden und dies zulässig ist, sollten sie im Hafenvertrag ausdrücklich deklariert werden. Zum Beispiel mit der
Vielleicht-Monade .
Anwendungsdienste
Wofür ist verantwortlich?- Validierung der Eingabedaten.
- Orchestrierung und Koordination von Diensten - Beginn und Ende von Transaktionen, Implementierung verteilter Skripte usw.
- Laden Sie Domänenobjekte und externe Daten über Ports in die Infrastruktur herunter und rufen Sie anschließend Befehle in Domain Core auf.
Wie man mit Fehlern aus dem Ergebnis umgehtFehler vom Domänenkern werden unverändert in die Außenwelt übertragen. Fehler aus der Infrastruktur können durch Wiederholungsversuche, Leistungsschalterrichtlinien oder durch externe Übertragung behoben werden.
Wie das Ergebnis generiert wirdKann die Validierung als Ergebnis implementieren.
Kann Benachrichtigungen über den teilweisen Erfolg der Operation generieren. Zum Beispiel Nachrichten an einen Benutzer wie „Ihre Bestellung wurde erfolgreich aufgegeben, aber beim Überprüfen der Lieferadresse ist ein Fehler aufgetreten. Ein Spezialist wird sich in Kürze mit Ihnen in Verbindung setzen, um die Lieferdetails zu klären. “
Umgang mit AusnahmenAngenommen, die Infrastrukturausnahmen, die die Anwendung verarbeiten kann, werden bereits von der Infrastrukturschicht in Ergebnis konvertiert, werden sie jedoch überhaupt nicht verarbeitet.
Wie man Ausnahmen wirftIm Allgemeinen auf keinen Fall. Es gibt jedoch Grenzoptionen, die im letzten Abschnitt des Artikels beschrieben werden.
Domänenkern
Wofür ist verantwortlich?Die Implementierung der Geschäftslogik, der "Kern" des Systems und die Hauptbedeutung seiner Existenz.
Wie man mit Fehlern aus dem Ergebnis umgehtDa die Ebene intern ist und Fehler nur von Objekten in derselben Domäne möglich sind, wird die Verarbeitung entweder auf Geschäftsregeln oder auf die Übersetzung des Fehlers in seiner ursprünglichen Form nach oben reduziert.
Wie das Ergebnis generiert wirdWenn Sie gegen Geschäftsregeln verstoßen, die in Domain Core gekapselt sind und nicht durch die Validierung von Eingabedaten auf Application Services-Ebene abgedeckt sind. Im Allgemeinen wird in dieser Ebene das Ergebnis am häufigsten verwendet.
Umgang mit AusnahmenAuf keinen Fall. Infrastrukturausnahmen wurden bereits von der Infrastrukturschicht verarbeitet, Daten sind dank der Application Services-Schicht bereits strukturiert, vollständig und validiert angekommen. Dementsprechend sind alle Ausnahmen, die herausfliegen können, echte Ausnahmen.
Wie man Ausnahmen wirftNormalerweise funktioniert hier eine allgemeine Regel: Je weniger Ausnahmen - desto besser.
Aber hatten Sie jemals Situationen, in denen Sie Code geschrieben haben und verstanden haben, dass er unter bestimmten Umständen schreckliche Geschäfte machen kann? Zum Beispiel, um Geld zweimal abzuschreiben oder die Daten so sehr zu verderben, dass wir später keine Knochen mehr sammeln.
In der Regel handelt es sich um die Ausführung von Befehlen, die für den aktuellen Status des Objekts nicht akzeptabel sind.
Natürlich sollte die entsprechende Schaltfläche auf der Benutzeroberfläche in diesem Zustand nicht sichtbar sein. In diesem Zustand sollten wir keinen Befehl vom Bus erhalten. All dies gilt, vorausgesetzt, die äußeren Schichten und Systeme haben ihre Funktion
normal ausgeführt . Aber in Domain Core sollten wir nichts über die Existenz externer Schichten wissen und an die Richtigkeit ihrer Arbeit glauben. Wir müssen die Invarianten des Systems schützen.
Einige der Prüfungen können auf Validierungsebene in Application Services platziert werden. Dies kann jedoch zu einer
defensiven Programmierung führen , die im Extremfall zu Folgendem führt:
- Die Einkapselung wird geschwächt, da bestimmte Invarianten auf der äußeren Schicht überprüft werden müssen.
- Das Wissen über den Themenbereich „fließt“ in die äußere Ebene, Überprüfungen können von beiden Ebenen dupliziert werden.
- Das Überprüfen der Ausführung eines Befehls von einer externen Ebene aus kann komplexer und weniger zuverlässig sein als das Überprüfen, ob ein Domänenobjekt einen Befehl in seinem aktuellen Status nicht ausführen kann .
Wenn wir solche Überprüfungen in der Validierungsschicht platzieren, müssen wir dem Benutzer den Grund für den Fehler mitteilen. Da es sich um eine Operation handelt, die unter den gegenwärtigen Bedingungen überhaupt nicht durchgeführt werden kann, laufen wir Gefahr, in einer von zwei Situationen zu sein:
- Wir haben einem normalen Benutzer eine Nachricht gegeben, die er überhaupt nicht verstanden hat und die er trotzdem unterstützen würde, genau wie bei der Meldung "Ein unerwarteter Fehler ist aufgetreten".
- Wir haben den Bösewicht in einer ziemlich verständlichen Form informiert, warum er die Operation, die er ausführen möchte, nicht ausführen kann und nach anderen Problemumgehungen suchen kann.
Aber zurück zum Hauptthema des Artikels. Bei allen Anzeichen ist die diskutierte Situation außergewöhnlich. Es sollte niemals passieren, aber wenn es so ist, wird es schlecht sein.
In dieser Situation ist es am logischsten, eine Ausnahme auszulösen, die erforderlichen Details zu verpfänden, dem Benutzer einen Fehler der allgemeinen Form „Operation ist nicht möglich“ zurückzugeben, die Überwachung für diese Art von Fehlern einzurichten und zu erwarten, dass wir sie niemals sehen werden.
Welche Art oder Arten von Ausnahmen sind in diesem Fall zu verwenden? Logischerweise sollte dies eine separate Art von Ausnahme sein, damit wir sie von anderen unterscheiden können und sie nicht versehentlich von der Ausnahmebehandlung von der äußeren Ebene erfasst wird. Wir brauchen auch keine Hierarchie oder viele Ausnahmen, das Wesentliche ist dasselbe - etwas Unannehmbares ist passiert. In unseren Projekten erstellen wir hierfür einen CorruptedInvariantException-Typ und verwenden ihn in geeigneten Situationen.
Sonderfälle für Webanwendungen
Ein wesentlicher Unterschied zwischen Webanwendungen anderer (Desktop-, Daemons- und Windows-Dienste usw.) ist die Interaktion mit der Außenwelt in Form von kurzfristigen Vorgängen (Verarbeitung von HTTP-Anforderungen), nach denen die Anwendung sofort „vergisst“, was passiert ist.
Nach der Verarbeitung der Anforderung wird immer eine Antwort generiert. Wenn der von unserem Code ausgeführte Vorgang keine Daten zurückgibt, gibt die Plattform weiterhin eine Antwort mit dem Statuscode zurück. Wenn der Vorgang durch eine Ausnahme abgebrochen wurde, gibt die Plattform weiterhin eine Antwort mit dem entsprechenden Statuscode zurück.
Um dieses Verhalten zu implementieren, wird die Anforderungsverarbeitung auf Webplattformen in Form von Pipes erstellt. Zuerst wird die Anforderung nacheinander verarbeitet (Anforderung), und dann wird die Antwort vorbereitet.
Wir können Middleware, Aktionsfilter, http-Handler oder ISAPI-Filter (je nach Plattform) verwenden und jederzeit in diese Pipeline integrieren. In jeder Phase der Anforderungsverarbeitung können wir die Verarbeitung unterbrechen, und die Pipeline bildet eine Antwort.
In der Regel implementieren wir den Geschäftsteil der Anwendung nicht mehr in der Pipeline-Architektur, sondern schreiben Code, der Operationen nacheinander ausführt. Bei diesem Ansatz ist es etwas schwieriger, das Szenario zu implementieren, wenn wir die Ausführung der Anforderung unterbrechen und sofort mit der Erstellung der Antwort fortfahren.
Was hat das alles mit Ausnahmebehandlung zu tun?
Tatsache ist, dass die in den vorherigen Teilen des Artikels beschriebenen Regeln für das Arbeiten mit Ausnahmen nicht gut in dieses Szenario passen.
Ausnahmen sind schlecht zu verwenden, da es sich um eine Semantik handelt.
Die weit verbreitete Verwendung von Result führt dazu, dass wir es (Result) über alle Ebenen der Anwendung ziehen. Wenn wir die Antwort erstellen, müssen wir das Ergebnis irgendwie analysieren, um zu verstehen, welcher Statuscode zurückgegeben werden soll. Es ist auch ratsam, diesen Parsing-Code zu verallgemeinern und in Middleware oder ActionFilter zu übertragen, was zu einem separaten Abenteuer wird. Das heißt, das Ergebnis ist nicht viel besser als Ausnahmen.
Was tun in einer solchen Situation?Baue kein Absolutes. Wir legen die Regeln zu unserem eigenen Vorteil fest und nicht zum Nachteil.
Wenn Sie eine Operation abbrechen möchten, weil ihre Fortsetzung nicht möglich ist, führt das Auslösen einer Ausnahme nicht zur Semantik. Wir leiten die Ausführung zum Exit und nicht zu einem anderen Geschäftscodeblock.
Wenn der Grund für die Unterbrechung wichtig ist, um den gewünschten Statuscode zu ermitteln, können benutzerdefinierte Ausnahmetypen verwendet werden.
Zuvor haben wir zwei benutzerdefinierte Typen erwähnt, die wir verwenden: ItemNotFoundException (Umwandlung in 404) und CorruptedInvariant (Umwandlung in 500).
Wenn Sie die Rechte von Benutzern überprüfen, da diese nicht auf das Vorbild oder die Ansprüche fallen, ist es zulässig, eine benutzerdefinierte ForbiddenException (Statuscode 403) zu erstellen.
Und schließlich die Validierung. Wir können immer noch nichts tun, bis der Benutzer seine Anfrage ändert. Diese Semantik
wird durch Code 422 beschrieben . Also unterbrechen wir den Vorgang und senden die Anfrage direkt an den Exit. Dies kann auch mit Ausnahme erfolgen. Beispielsweise verfügt die
FluentValidation- Bibliothek bereits über einen
integrierten Ausnahmetyp , der alle Details an den Client übergibt, die erforderlich sind, um dem Benutzer klar anzuzeigen, was mit der Anforderung nicht stimmt.
Das ist alles. Wie arbeiten Sie mit Ausnahmen?