Es gibt viele bekannte Best Practices zur Ausnahmebehandlung für sich. Ich kenne die "Do's and Don'ts" gut genug, aber die Dinge werden kompliziert, wenn es um Best Practices oder Muster in größeren Umgebungen geht. "Früh werfen, spät fangen" - habe ich schon oft gehört und es verwirrt mich immer noch.
Warum sollte ich früh werfen und spät fangen, wenn auf einer niedrigen Ebene eine Nullzeigerausnahme ausgelöst wird? Warum sollte ich es auf einer höheren Ebene fangen? Es macht für mich keinen Sinn, eine Ausnahme auf niedriger Ebene auf einer höheren Ebene abzufangen, z. B. eine Geschäftsschicht. Es scheint die Bedenken jeder Schicht zu verletzen.
Stellen Sie sich folgende Situation vor:
Ich habe einen Dienst, der eine Zahl berechnet. Zur Berechnung der Zahl greift der Service auf ein Repository zu, um Rohdaten abzurufen, und auf einige andere Services, um die Berechnung vorzubereiten. Wenn auf der Datenabrufebene ein Fehler aufgetreten ist, warum sollte ich eine DataRetrievalException auf eine höhere Ebene verschieben? Im Gegensatz dazu würde ich es vorziehen, die Ausnahme in eine sinnvolle Ausnahme, zum Beispiel eine CalculationServiceException, zu verpacken.
Warum früh werfen, warum spät fangen?
quelle
Antworten:
Meiner Erfahrung nach ist es am besten, Ausnahmen an dem Punkt auszulösen, an dem die Fehler auftreten. Sie tun dies, weil Sie genau wissen, warum die Ausnahme ausgelöst wurde.
Da die Ausnahme die Ebenen rückgängig macht, ist das Auffangen und Zurückwerfen eine gute Möglichkeit, der Ausnahme zusätzlichen Kontext hinzuzufügen. Dies kann bedeuten, dass eine andere Art von Ausnahme ausgelöst wird, schließt jedoch die ursprüngliche Ausnahme ein, wenn Sie dies tun.
Schließlich wird die Ausnahme eine Ebene erreichen, auf der Sie Entscheidungen über den Codefluss treffen können (z. B. eine Aufforderung an den Benutzer zum Handeln). Dies ist der Punkt, an dem Sie die Ausnahme endlich behandeln und die normale Ausführung fortsetzen sollten.
Durch Übung und Erfahrung mit Ihrer Codebasis ist es ziemlich einfach zu beurteilen, wann Sie den Fehlern zusätzlichen Kontext hinzufügen und wo es am sinnvollsten ist, die Fehler tatsächlich endgültig zu behandeln.
Fang → Nachwerfen
Hier können Sie sinnvollerweise weitere Informationen hinzufügen, die es einem Entwickler ersparen würden, alle Ebenen zu durcharbeiten, um das Problem zu verstehen.
Fang → Griff
Hier können Sie endgültige Entscheidungen darüber treffen, was für eine angemessene, aber unterschiedliche Ausführung die Software durchläuft.
Catch → Error Return
In bestimmten Situationen sollten Ausnahmen abgefangen und ein Fehlerwert an den Aufrufer zurückgegeben werden, um in eine Catch → Rethrow-Implementierung umzugestalten.
quelle
NullPointerException
? Warum nicht früher nachnull
einer Ausnahme (vielleicht einerIllegalArgumentException
) suchen und sie auslösen , damit der Anrufer genau weiß, wo die schlechte Nachricht eingegangen istnull
?" Ich glaube, das würde der Teil des Sprichworts "Früh werfen" andeuten.Sie möchten so schnell wie möglich eine Ausnahme auslösen, da dies das Auffinden der Ursache erleichtert. Betrachten Sie beispielsweise eine Methode, die mit bestimmten Argumenten fehlschlagen kann. Wenn Sie die Argumente validieren und gleich zu Beginn der Methode fehlschlagen, wissen Sie sofort, dass der Fehler im aufrufenden Code enthalten ist. Wenn Sie warten, bis die Argumente benötigt werden, bevor Sie fehlschlagen, müssen Sie die Ausführung verfolgen und herausfinden, ob der Fehler im aufrufenden Code liegt (falsches Argument) oder ob die Methode einen Fehler aufweist. Je früher Sie die Ausnahme auslösen, desto näher ist sie an der zugrunde liegenden Ursache und desto einfacher ist es herauszufinden, wo Fehler aufgetreten sind.
Der Grund, warum Ausnahmen auf höheren Ebenen behandelt werden, liegt darin, dass die niedrigeren Ebenen nicht wissen, wie mit dem Fehler umgegangen werden soll. Tatsächlich kann es je nach dem aufrufenden Code mehrere geeignete Möglichkeiten geben, um denselben Fehler zu behandeln. Nehmen wir zum Beispiel das Öffnen einer Datei. Wenn Sie versuchen, eine Konfigurationsdatei zu öffnen, die nicht vorhanden ist, kann es eine angemessene Reaktion sein, die Ausnahme zu ignorieren und mit der Standardkonfiguration fortzufahren. Wenn Sie eine private Datei öffnen, die für die Ausführung des Programms von entscheidender Bedeutung ist und irgendwie fehlt, besteht Ihre einzige Option wahrscheinlich darin, das Programm zu schließen.
Das Einschließen der Ausnahmen in die richtigen Typen ist eine rein orthogonale Angelegenheit.
quelle
Andere haben recht gut zusammengefasst, warum man früh wirft . Lassen Sie mich stattdessen auf das Warum konzentrieren, um den späten Teil zu fangen , für den ich keine befriedigende Erklärung für meinen Geschmack gesehen habe.
WARUM AUSNAHMEN?
Es scheint ziemlich verwirrend zu sein, warum es überhaupt Ausnahmen gibt. Lassen Sie mich das große Geheimnis hier teilen: Der Grund für Ausnahmen und die Behandlung von Ausnahmen ist ... ABSTRAKTION .
Haben Sie Code wie diesen gesehen:
So sollten Ausnahmen nicht verwendet werden. Code wie der obige gibt es im wirklichen Leben, aber sie sind eher eine Aberration und sind wirklich die Ausnahme (Wortspiel). Die Definition der Division ist beispielsweise auch in der reinen Mathematik bedingt: Es ist immer der "Aufrufercode", der den Ausnahmefall Null behandeln muss, um den Eingabebereich einzuschränken. Es ist hässlich. Es tut dem Anrufer immer weh. Dennoch ist in solchen Situationen das Check-Then-Do- Muster der natürliche Weg:
Alternativ können Sie den OOP-Stil wie folgt steuern:
Wie Sie sehen, hat der Anrufercode die Last der Vorabprüfung, führt jedoch keine Ausnahmebehandlung mehr durch. Kommt
ArithmeticException
jemals ein Aufruf vondivide
odereval
, dann bist DU es, der die Ausnahmebehandlung durchführen und deinen Code korrigieren muss, weil du den vergessen hastcheck()
. Aus den gleichen GründenNullPointerException
ist es fast immer falsch, eine zu fangen .Nun gibt es einige Leute , die sagen , dass sie die Ausnahmefälle in der Methode / Funktion Unterschrift sehen wollen, also explizit die Ausgabe zu erweitern Domäne . Sie sind diejenigen, die geprüfte Ausnahmen bevorzugen . Natürlich sollte das Ändern der Ausgabedomäne eine Anpassung des Codes eines direkten Anrufers erzwingen, und dies würde in der Tat mit überprüften Ausnahmen erreicht. Aber dafür braucht man keine Ausnahmen! Aus diesem Grund gibt es
Nullable<T>
generische Klassen , Fallklassen , algebraische Datentypen und Vereinigungstypen . Einige OO-Leute ziehen es vielleicht sogar vor,null
für einfache Fehlerfälle wie diesen zurückzukehren:Technisch Ausnahmen können für Zwecke wie die oben verwendet werden, aber hier ist der Punkt: Ausnahmen existieren nicht für eine solche Nutzung . Ausnahmen sind pro Abstraktion. Ausnahme sind etwa Indirektionen. Ausnahmen ermöglichen es, die "Ergebnis" -Domäne zu erweitern, ohne direkte Kundenverträge zu brechen , und die Fehlerbehandlung auf "irgendwo anders" zu verschieben. Wenn Ihr Code Ausnahmen auslöst , die von Direktaufrufern desselben Codes behandelt werden, ohne dazwischen liegende Abstraktionsebenen, dann tun Sie dies FALSCH
WIE SPÄTEN?
So hier sind wir. Ich habe mich durchgearbeitet, um zu zeigen, dass die Verwendung von Ausnahmen in den obigen Szenarien nicht die Verwendung von Ausnahmen bedeutet. Es gibt jedoch einen echten Anwendungsfall, bei dem die durch die Ausnahmebehandlung gebotene Abstraktion und Indirektion unverzichtbar ist. Das Verstehen einer solchen Verwendung hilft auch dabei, die Empfehlung für einen späten Fang zu verstehen .
Dieser Anwendungsfall lautet: Programmieren gegen Ressourcenabstraktionen ...
Ja, Geschäftslogik sollte gegen Abstraktionen programmiert werden , nicht gegen konkrete Implementierungen. Übergeordneter IOC- Verkabelungscode instanziiert die konkreten Implementierungen der Ressourcenabstraktionen und leitet sie an die Geschäftslogik weiter. Hier gibt es nichts Neues. Die konkreten Implementierungen dieser Ressourcenabstraktionen können jedoch möglicherweise ihre eigenen implementierungsspezifischen Ausnahmen auslösen , nicht wahr?
Wer kann mit diesen implementierungsspezifischen Ausnahmen umgehen? Ist es dann möglich, irgendwelche ressourcenspezifischen Ausnahmen in der Geschäftslogik zu behandeln? Nein, das ist es nicht. Die Geschäftslogik ist gegen Abstraktionen programmiert, was die Kenntnis dieser implementierungsspezifischen Ausnahmedetails ausschließt.
"Aha!", Könnte man sagen: "Aber deshalb können wir Ausnahmen unterordnen und Ausnahmehierarchien erstellen" (siehe Mr. Spring !). Lass mich dir sagen, das ist ein Trugschluss. Erstens sagt jedes vernünftige Buch über OOP, dass konkrete Vererbung schlecht ist, aber irgendwie ist diese Kernkomponente von JVM, die Ausnahmebehandlung, eng mit konkreter Vererbung verbunden. Ironischerweise hätte Joshua Bloch sein Effective Java-Buch nicht schreiben können, bevor er die Erfahrung mit einer funktionierenden JVM sammeln konnte, oder? Es ist eher ein Lehrbuch für die nächste Generation. Zweitens, und was noch wichtiger ist, wenn Sie eine Ausnahme auf hoher Ebene feststellen, wie gehen Sie dann damit um?
PatientNeedsImmediateAttentionException
: Müssen wir ihr eine Pille geben oder ihre Beine amputieren? Wie wäre es mit einer switch-Anweisung über alle möglichen Unterklassen? Da ist dein Polymorphismus, da ist die Abstraktion. Du hast es erfasst.Wer kann mit den ressourcenspezifischen Ausnahmen umgehen? Es muss derjenige sein, der die Konkretionen kennt! Derjenige, der die Ressource instanziiert hat! Der "Verkabelungs" Code natürlich! Überprüfen Sie dies heraus:
Geschäftslogik, die gegen Abstraktionen codiert ist ... KEINE BEHANDLUNG VON BETONRESSOURCENFEHLERN!
Inzwischen woanders die konkreten Umsetzungen ...
Und schließlich der Verkabelungscode ... Wer behandelt konkrete Ressourcenausnahmen? Derjenige, der über sie Bescheid weiß!
Jetzt ertrage mit mit mir. Der obige Code ist einfach. Sie könnten sagen, Sie haben eine Unternehmensanwendung / einen Webcontainer mit mehreren Bereichen für von IOC-Containern verwaltete Ressourcen, und Sie benötigen automatische Wiederholungsversuche und Neuinitialisierung von Ressourcen für Sitzungs- oder Anforderungsbereiche usw. Der Verkabelungslogik auf den Bereichen der unteren Ebene können abstrakte Fabriken zugewiesen werden Erstellen Sie Ressourcen, ohne die genauen Implementierungen zu kennen. Nur übergeordnete Bereiche würden wirklich wissen, welche Ausnahmen diese untergeordneten Ressourcen auslösen können. Jetzt warte!
Leider erlauben Ausnahmen nur die Indirektion über den Aufrufstapel, und verschiedene Bereiche mit ihren unterschiedlichen Kardinalitäten werden normalerweise in mehreren verschiedenen Threads ausgeführt. Keine Möglichkeit, damit mit Ausnahmen zu kommunizieren. Wir brauchen hier etwas Mächtigeres. Antwort: asynchrone Nachrichtenübermittlung . Fangen Sie jede Ausnahme an der Wurzel des Bereichs der unteren Ebene ab. Ignoriere nichts, lass nichts durch. Dadurch werden alle im Aufrufstapel des aktuellen Bereichs erstellten Ressourcen geschlossen und freigegeben. Geben Sie dann Fehlermeldungen an Bereiche weiter oben weiter, indem Sie Nachrichtenwarteschlangen / -kanäle in der Ausnahmebehandlungsroutine verwenden, bis Sie die Ebene erreichen, auf der die Konkretionen bekannt sind. Das ist der Typ, der weiß, wie man damit umgeht.
SUMMA SUMMARUM
Nach meiner Interpretation bedeutet " Spät fangen" also, Ausnahmen an der günstigsten Stelle zu fangen, an der Sie die ABSTRAKTION NICHT MEHR UNTERBRECHEN . Fang nicht zu früh! Fangen Sie Ausnahmen auf der Ebene ab, auf der Sie die konkrete Ausnahme erstellen, die Instanzen der Ressourcenabstraktionen auslöst. Diese Ebene kennt die Konkretionen der Abstraktionen. Die "Verdrahtungs" -Schicht.
HTH. Viel Spaß beim Codieren!
quelle
WrappedFirstResourceException
oderWrappedSecondResourceException
und die "Verkabelung" Schicht, um in dieser Ausnahme zu suchen, um die Ursache des Problems zu sehen ...FailingInputResource
Ausnahme das Ergebnis einer Operation mit istin1
. Tatsächlich denke ich, dass in vielen Fällen der richtige Ansatz darin besteht, die Verdrahtungsschicht ein Objekt zur Ausnahmebehandlung passieren zu lassen und die Geschäftsschichtcatch
einzufügen, das dann diehandleException
Methode dieses Objekts aufruft . Diese Methode kann die Standarddaten erneut werfen oder bereitstellen oder eine "Abort / Retry / Fail" -Aufforderung anzeigen und den Bediener entscheiden lassen, was zu tun ist, usw. abhängig von den Anforderungen der Anwendung.UnrecoverableInternalException
, ähnlich einem HTTP 500-Fehlercode.doMyBusiness
Methode auf. Dies war der Kürze halber und es ist durchaus möglich, es dynamischer zu gestalten. Eine solcheHandler
Klasse würde mit einigen Eingabe- / Ausgaberessourcen instanziiert werden und einehandle
Methode haben, die eine Klasse empfängt, die eine implementiertReusableBusinessLogicInterface
. Sie können dann kombinieren / konfigurieren, um verschiedene Handler-, Ressourcen- und Geschäftslogikimplementierungen in der Verdrahtungsschicht irgendwo darüber zu verwenden.Um diese Frage richtig zu beantworten, gehen wir einen Schritt zurück und stellen eine noch grundlegendere Frage.
Warum haben wir überhaupt Ausnahmen?
Wir lösen Ausnahmen aus, um dem Aufrufer unserer Methode mitzuteilen, dass wir nicht das tun konnten, was wir tun sollten. Die Art der Ausnahme erklärt, warum wir nicht das tun konnten, was wir wollten.
Schauen wir uns einen Code an:
Dieser Code kann offensichtlich eine Nullreferenzausnahme auslösen, wenn
PropertyB
null ist. Es gibt zwei Dinge, die wir in diesem Fall tun könnten, um diese Situation zu "korrigieren". Wir könnten:Das Erstellen von PropertyB hier kann sehr gefährlich sein. Welchen Grund hat diese Methode, PropertyB zu erstellen? Dies würde mit Sicherheit das Prinzip der einheitlichen Verantwortung verletzen. Wenn PropertyB hier nicht vorhanden ist, weist dies aller Wahrscheinlichkeit nach darauf hin, dass ein Fehler aufgetreten ist. Die Methode wird für ein teilweise konstruiertes Objekt aufgerufen oder PropertyB wurde falsch auf null gesetzt. Durch das Erstellen von PropertyB hier könnten wir einen viel größeren Fehler verbergen, der uns später beißen könnte, wie z. B. einen Fehler, der zu Datenkorruption führt.
Wenn wir stattdessen die Nullreferenz in die Luft sprudeln lassen, lassen wir den Entwickler, der diese Methode aufgerufen hat, so bald wie möglich wissen, dass ein Fehler aufgetreten ist. Eine wichtige Voraussetzung für den Aufruf dieser Methode wurde übersehen.
In der Tat werfen wir früh, weil es unsere Bedenken viel besser trennt. Sobald ein Fehler aufgetreten ist, informieren wir die vorgelagerten Entwickler darüber.
Warum wir uns "verspäten", ist eine andere Geschichte. Wir wollen nicht wirklich zu spät kommen, wir wollen wirklich zu früh kommen, wenn wir wissen, wie wir mit dem Problem richtig umgehen sollen. Manchmal werden dies später fünfzehn Abstraktionsebenen sein, und manchmal wird dies der Punkt der Schöpfung sein.
Der Punkt ist, dass wir die Ausnahme auf der Abstraktionsebene abfangen möchten, die es uns ermöglicht, die Ausnahme an dem Punkt zu behandeln, an dem wir alle Informationen haben, die wir benötigen, um die Ausnahme richtig zu behandeln.
quelle
if(PropertyB == null) return 0;
Werfen Sie, sobald Sie etwas sehen, für das es sich zu werfen lohnt, um zu vermeiden, dass Objekte in einen ungültigen Zustand versetzt werden. Das bedeutet, dass Sie, wenn ein Nullzeiger übergeben wurde, früh danach suchen und eine NPE auslösen, bevor die Chance besteht, dass er auf den niedrigen Pegel abfällt.
Fangen Sie an, sobald Sie wissen, was Sie tun müssen, um den Fehler zu beheben (dies ist im Allgemeinen nicht der Ort, an dem Sie werfen, sonst könnten Sie einfach ein if-else verwenden). Wenn ein ungültiger Parameter übergeben wurde, sollte der Layer, der den Parameter bereitgestellt hat, die Konsequenzen behandeln .
quelle
Eine gültige Geschäftsregel lautet: "Wenn die untergeordnete Software keinen Wert berechnet, dann ...".
Dies kann nur auf der höheren Ebene ausgedrückt werden, andernfalls versucht die Software der niedrigeren Ebene, ihr Verhalten auf der Grundlage ihrer eigenen Korrektheit zu ändern, was nur zu einem Knoten führen wird.
quelle
Ausnahmen gelten vor allem für Ausnahmesituationen. In Ihrem Beispiel kann keine Zahl berechnet werden, wenn die Rohdaten nicht vorhanden sind, da sie nicht geladen werden konnten.
Nach meiner Erfahrung ist es empfehlenswert, Ausnahmen zu abstrahieren, während Sie den Stapel hinaufgehen. Die Punkte, an denen Sie dies tun möchten, befinden sich normalerweise immer dann, wenn eine Ausnahme die Grenze zwischen zwei Ebenen überschreitet.
Wenn beim Erfassen Ihrer Rohdaten in der Datenschicht ein Fehler auftritt, lösen Sie eine Ausnahme aus, um den Benutzer zu benachrichtigen, der die Daten angefordert hat. Versuchen Sie nicht, dieses Problem hier unten zu umgehen. Die Komplexität des Handhabungscodes kann sehr hoch sein. Außerdem ist die Datenschicht nur für die Anforderung von Daten verantwortlich, nicht für die Behandlung von Fehlern, die dabei auftreten. Dies ist, was mit "früh werfen" gemeint ist .
In Ihrem Beispiel ist die Fangschicht die Serviceschicht. Der Dienst selbst ist eine neue Schicht, die über der Datenzugriffsschicht liegt. Sie möchten also die Ausnahme dort abfangen. Möglicherweise verfügt Ihr Dienst über eine Failover-Infrastruktur und versucht, die Daten von einem anderen Repository anzufordern. Wenn dies ebenfalls fehlschlägt, schließen Sie die Ausnahme in etwas ein, das der Aufrufer des Dienstes versteht (wenn es sich um einen Webdienst handelt, kann dies ein SOAP-Fehler sein). Stellen Sie die ursprüngliche Ausnahme als innere Ausnahme ein, damit die späteren Ebenen genau protokollieren können, was falsch gelaufen ist.
Der Dienstfehler kann von der Schicht abgefangen werden, die den Dienst aufruft (z. B. der Benutzeroberfläche). Und das ist es, was mit "spät fangen" gemeint ist . Wenn Sie die Ausnahme in einer niedrigeren Ebene nicht verarbeiten können, werfen Sie sie erneut. Wenn die oberste Ebene die Ausnahme nicht verarbeiten kann, behandeln Sie sie! Dies kann das Protokollieren oder Präsentieren umfassen.
Der Grund, warum Sie Ausnahmen erneut auslösen sollten (wie oben beschrieben, indem Sie sie in allgemeinere Ausnahmen einschließen), ist, dass der Benutzer sehr wahrscheinlich nicht verstehen kann, dass ein Fehler aufgetreten ist, weil beispielsweise ein Zeiger auf ungültigen Speicher verwiesen hat. Und es ist ihm egal. Er achtet nur darauf, dass die Zahl nicht vom Dienst berechnet werden kann und dies sind die Informationen, die ihm angezeigt werden sollten.
Geht weitere können Sie (in einer idealen Welt) vollständig auslassen
try
/catch
Code aus dem UI. Verwenden Sie stattdessen einen globalen Ausnahmebehandler, der die möglicherweise von den unteren Ebenen ausgelösten Ausnahmen versteht, sie in ein Protokoll schreibt und sie in Fehlerobjekte umschließt, die aussagekräftige (und möglicherweise lokalisierte) Informationen zum Fehler enthalten. Diese Objekte können dem Benutzer auf einfache Weise in einer von Ihnen gewünschten Form (Nachrichtenfelder, Benachrichtigungen, Nachrichten-Toasts usw.) präsentiert werden.quelle
Im Allgemeinen empfiehlt es sich, Ausnahmen frühzeitig auszulösen, da gebrochene Verträge nicht weiter als nötig durch den Code fließen sollen. Wenn Sie beispielsweise erwarten, dass ein bestimmter Funktionsparameter eine positive Ganzzahl ist, sollten Sie diese Einschränkung zum Zeitpunkt des Funktionsaufrufs erzwingen, anstatt zu warten, bis diese Variable an einer anderen Stelle im Codestapel verwendet wird.
Ich kann mich nicht wirklich dazu äußern, weil ich meine eigenen Regeln habe und sie sich von Projekt zu Projekt ändern. Ich versuche jedoch, die Ausnahmen in zwei Gruppen zu unterteilen. Eine ist nur für den internen Gebrauch und die andere nur für den externen Gebrauch. Interne Ausnahmen werden von meinem eigenen Code abgefangen und behandelt, und externe Ausnahmen werden von jedem Code behandelt, der mich aufruft. Dies ist im Grunde eine Form, Dinge später einzufangen, aber nicht ganz, weil es mir die Flexibilität bietet, von der Regel abzuweichen, wenn dies im internen Code erforderlich ist.
quelle