Gute Verwendung von Try-Catch-Blöcken?

14

Ich ringe immer wieder damit. Ich versuche, die richtige Balance zwischen Versuch / Fang und Code zu finden, und werde nicht zu einem obszönen Durcheinander von Tabs, Klammern und Ausnahmen, die wie eine heiße Kartoffel auf den Call-Stack zurückgeworfen werden. Ich habe zum Beispiel eine App, die ich gerade entwickle und die SQLite verwendet. Ich habe eine Datenbankschnittstelle, die die SQLite-Aufrufe abstrahiert, und ein Modell, das Dinge akzeptiert, die in die / aus der Datenbank gelangen ... Wenn also eine SQLite-Ausnahme auftritt, muss sie an das Modell übergeben werden (von wem sie aufgerufen wurde) ), der es an denjenigen weitergeben muss, der den AddRecord / DeleteRecord / whatever aufgerufen hat ...  

Ich bin ein Fan von Ausnahmen im Gegensatz zur Rückgabe von Fehlercodes, da Fehlercodes ignoriert, vergessen usw. werden können, während eine Ausnahme im Wesentlichen behandelt werden muss (zugegeben, ich könnte sie abfangen und sofort weitermachen ...) Sicher, es muss einen besseren Weg geben als das, was ich gerade vorhabe.

Edit:  Ich hätte das etwas anders formulieren sollen. Ich verstehe es als verschiedene Typen und solche wiederzuwerfen, ich habe das schlecht formuliert und das ist meine eigene Schuld. Meine Frage ist ... wie hält man den Code dabei am besten sauber? Nach einer Weile fühlt es sich für mich sehr unübersichtlich an.

trycatch
quelle
Welche Programmiersprache?
Apalala
2
C # im Moment, aber ich versuche, im Allgemeinen zu denken.
trycatch
C # erzwingt keine Deklaration von Ausnahmen, wodurch es einfacher wird, Ausnahmen dort zu behandeln, wo es sinnvoll ist, und zu vermeiden, dass Programmierer versucht werden, sie zu fangen, ohne sie tatsächlich zu behandeln. Anders Hejlsberg, Designer von C #, spricht sich gegen geprüfte Ausnahmen in diesem Artikel aus artima.com/intv/handcuffs.html
Apalala

Antworten:

13

Stellen Sie sich das als starke Typisierung vor, auch wenn Sie keine stark typisierte Sprache verwenden. Wenn Ihre Methode den erwarteten Typ nicht zurückgeben kann, sollte sie eine Ausnahme auslösen.

Anstatt die SQLException bis zum Modell (oder schlimmer noch bis zur Benutzeroberfläche) auszulösen, sollte jede Ebene bekannte Ausnahmen abfangen und diese durch für diese Ebene geeignete Ausnahmen umbrechen / mutieren / ersetzen:

Layer      Handles Exception
----------------------------
UI         DataNotFoundException
Model      DatabaseRetrievalException
DAO        SQLException

Dies sollte dazu beitragen, die Anzahl der Ausnahmen, nach denen Sie in jeder Ebene suchen, zu begrenzen und ein organisiertes Ausnahmesystem aufrechtzuerhalten.

Nicole
quelle
Ich habe ein paar Änderungen vorgenommen und das Original-Q schlecht formuliert. Meine Frage ist eher, wie man den Code sauber hält, während man mit dem ganzen Ausprobieren und Fangen umgeht und so weiter. Es fühlt sich sehr chaotisch an, wenn überall versuchen, Blöcke zu fangen ...
trycatch
1
@Ed stört es Sie nicht, dass Ihre Benutzeroberfläche oder Ihr Modell "SQLException" abfängt? Für mich ist das nicht sehr relevant. Für jeden seine, denke ich.
Nicole
1
@Ed, in Sprachen, in denen keine Ausnahmen geprüft wurden, ist das vielleicht in Ordnung, aber in Sprachen mit geprüften Ausnahmen ist es wirklich hässlich, throws SQLExceptioneine Methode zu haben , die nicht impliziert, dass SQL überhaupt involviert ist. Und was passiert, wenn Sie entscheiden, dass einige Vorgänge in einen Dateispeicher verschoben werden sollen? Jetzt müssen Sie deklarieren throws SQLException, IOException, etc. Es wird außer Kontrolle geraten.
Mike Daniels
1
@Ed an den Endbenutzer SQLException bedeutet nicht viel, insbesondere wenn Ihr System neben relationalen Datenbanken mehrere Arten von Persistenz unterstützen kann. Als GUI-Programmierer würde ich mich viel lieber mit DataNotFoundException beschäftigen müssen als mit einer ganzen Reihe von Ausnahmen auf niedriger Ebene. Sehr oft ist eine Ausnahme von einer Bibliothek auf niedriger Ebene nur ein Teil des normalen Lebens, in diesem Fall sind sie viel einfacher zu handhaben, wenn ihr Abstraktionsgrad mit dem der Anwendung übereinstimmt.
Newtopian
1
@Newtopian - Ich habe nie gesagt, dem Endbenutzer die unformatierte Ausnahme zu präsentieren. Für Endbenutzer ist eine einfache Meldung "Programm funktioniert nicht mehr" ausreichend, während die Ausnahme an einer Stelle protokolliert wird, die für den Support hilfreich ist. Im Debug-Modus ist es nützlich, die Ausnahme anzuzeigen. Sie sollten sich nicht um jede mögliche Ausnahme kümmern, die eine API auslösen könnte, es sei denn, Sie haben einen speziellen Handler für solche Ausnahmen. Überlassen Sie es einfach Ihrem unbehandelten Ausnahmefänger.
Ed James
9

Ausnahmen ermöglichen es, saubereren Code zu schreiben, da sich der Großteil des Codes um den Normalfall kümmert und die Ausnahmefälle später auch in einem anderen Kontext behandelt werden können.

Die Regel für das Behandeln (Abfangen) von Ausnahmen lautet, dass dies durch einen Kontext erfolgen muss, der tatsächlich etwas dagegen tun kann. Das hat aber eine Ausnahme:

Ausnahmen müssen an Modulgrenzen (insbesondere Ebenengrenzen) abgefangen werden, und selbst wenn sie nur eingeschlossen werden sollen, muss eine Ausnahme höherer Ebene ausgelöst werden, die für den Aufrufer von Bedeutung ist. Jedes Modul und jeder Layer muss seine Implementierungsdetails auch in Bezug auf Ausnahmen verbergen (ein Heap-Modul kann HeapFull auslösen, aber niemals ArrayIndexOutOfBounds).

In Ihrem Beispiel ist es unwahrscheinlich, dass die oberen Schichten etwas gegen eine SQLite-Ausnahme unternehmen können (wenn dies der Fall ist, ist alles so an SQLite gekoppelt, dass Sie die Datenschicht nicht auf etwas anderes umstellen können). Es gibt eine Handvoll vorhersehbarer Gründe, warum Dinge wie Hinzufügen / Löschen / Aktualisieren fehlschlagen, und einige davon (inkompatible Änderungen bei gleichzeitigen Transaktionen) lassen sich selbst in der Daten- / Persistenzschicht nicht wiederherstellen (Verstoß gegen Integritätsregeln, z. B.). Die Persistenzebene sollte die Ausnahmen in aussagekräftige Ausdrücke der Modellebene übersetzen, damit die oberen Ebenen entscheiden können, ob sie es erneut versuchen oder ordnungsgemäß fehlschlagen.

Apalala
quelle
Ich stimme zu und habe meine Frage bearbeitet, um zu reflektieren, wo ich sie falsch formuliert hatte. Ich wollte, dass dies eher eine Frage ist, wie man das Ausprobieren und Auffangen von Unordnung verhindern kann.
trycatch
Sie können die Prinzipien von @Ed James und mir innerhalb einer Ebene oder eines Moduls anwenden . Anstatt SQLite direkt von überall aufzurufen, sollten Sie einige Methoden / Funktionen verwenden, die mit SQLite kommunizieren und entweder die Ausnahmen beheben oder sie in allgemeinere übersetzen. Wenn eine Transaktion mehrere Abfragen und Aktualisierungen umfasst, müssen Sie nicht jede mögliche Ausnahme in jeder behandeln: Ein einzelner äußerer Try-Catch kann die Ausnahmen übersetzen, und ein innerer kann Teilaktualisierungen mit einem Rollback durchführen. Sie können die Aktualisierungen auch in eine eigene Funktion verschieben, um die Ausnahmebehandlung zu vereinfachen.
Apalala
1
dieses Material leichter sein würde , mit Code - Beispiele zu verstehen ..
Klicken Upvote
Dies war wahrscheinlich eine Frage, die von Anfang an besser für den Stapelüberlauf geeignet war.
Apalala
5

In der Regel sollten Sie nur bestimmte Ausnahmen (z. B. IOException) abfangen und nur dann, wenn Sie nach dem Abfangen der Ausnahme etwas Bestimmtes zu tun haben.

Andernfalls ist es oft am besten, Ausnahmen an die Oberfläche sprudeln zu lassen, damit sie freigelegt und behandelt werden können. Einige Leute nennen das Fail-Fast.

Sie sollten eine Art Handler im Stammverzeichnis Ihrer Anwendung haben, um unbehandelte Ausnahmen zu erfassen, die von unten in die Luft gesprudelt sind. Auf diese Weise haben Sie die Möglichkeit, die Ausnahme in geeigneter Weise darzustellen, zu melden oder zu verwalten.

Das Umbrechen von Ausnahmen ist nützlich, wenn Sie eine Ausnahme auf einem verteilten System auslösen müssen und der Client nicht über die Definition des serverseitigen Fehlers verfügt.

Ed James
quelle
Ausnahmen abfangen und zum Schweigen bringen ist eine schreckliche Sache. Wenn das Programm kaputt ist, sollte es mit einer Ausnahme und einem Traceback abstürzen. Es sollte durcheinander sein, Dinge stillschweigend protokollieren, aber die Daten durcheinander bringen.
S.Lott
1
@ S.lott Ich bin damit einverstanden, dass man Ausnahmen nicht zum Schweigen bringt, weil sie ärgerlich sind, aber nur ein Absturz einer Anwendung ist ein bisschen extrem. Es gibt viele Fälle, in denen es möglich ist, die Ausnahme abzufangen und das System in einem bekannten Zustand zurückzusetzen. In solchen Fällen ist der globale Handler für alles sehr nützlich. Wenn der Rücksetzvorgang jedoch wiederum auf eine Weise fehlschlägt, die nicht sicher behandelt werden kann, ist es weitaus besser, ihn abstürzen zu lassen, als den Kopf in den Sand zu stecken.
Newtopian
2
wieder hängt alles davon ab, was Sie bauen, aber ich bin im Allgemeinen nicht einverstanden damit, sie nur in die Luft sprudeln zu lassen, da dies zu einem Abstraktionsleck führt. Wenn ich eine API verwende, möchte ich lieber sehen, dass sie Ausnahmen enthält, die mit dem API-Modell übereinstimmen. Ich hasse auch Überraschungen, also hasse ich es, wenn eine API eine Ausnahme in Bezug auf die Implementierung unangekündigt durchschaut. Wenn ich keine Ahnung habe, was auf mich zukommt, wie kann ich dann reagieren? Ich verwende zu oft viel breitere Fangnetze, um zu verhindern, dass Überraschungen meine App unangekündigt zum Absturz bringen.
Newtopian
@Newtopian: "Wrapping" -Ausnahmen und "Rewriting" reduzieren den Verlust von Abstraktionen. Sie sprudeln immer noch angemessen. "Wenn ich keine Ahnung habe, was auf mich zukommt, wie kann ich dann reagieren? Ich verwende zu oft viel breitere Fangnetze." Sie müssen nicht alles fangen. In 80% der Fälle ist es richtig, nichts zu fangen. In 20% der Fälle gibt es eine aussagekräftige Antwort.
S.Lott
1
@Newtopian, ich denke, wir müssen unterscheiden zwischen Ausnahmen, die normalerweise von einem Objekt geworfen werden und daher umbrochen werden sollten, und Ausnahmen, die aufgrund von Fehlern im Code des Objekts auftreten und nicht umbrochen werden sollten.
Winston Ewert
3

Stellen Sie sich vor, Sie schreiben eine Stapelklasse. Sie fügen keinen Code für die Ausnahmebehandlung in die Klasse ein. Dies kann zu den folgenden Ausnahmen führen.

  1. ArrayIndexError - Wird ausgelöst, wenn der Benutzer versucht, einen Popup vom leeren Stapel auszuführen
  2. NullPtrException - Wird ausgelöst, weil ein Fehler in der Implementierung versucht hat, auf eine Nullreferenz zu verweisen

Bei einem vereinfachten Ansatz zum Umschließen von Ausnahmen werden möglicherweise beide Ausnahmen in eine StackError-Ausnahmeklasse eingeschlossen. Hier geht es jedoch nicht darum, Ausnahmen zu verpacken. Wenn ein Objekt eine Low-Level-Ausnahme auslöst, bedeutet dies, dass das Objekt beschädigt ist. Es gibt jedoch einen Fall, in dem dies akzeptabel ist: Wenn das Objekt tatsächlich beschädigt ist.

Der Punkt beim Umschließen von Ausnahmen ist, dass das Objekt ordnungsgemäße Ausnahmen für normale Fehler gibt. Der Stack sollte StackEmpty und nicht ArrayIndexError auslösen, wenn Sie von einem leeren Stack abspringen. Es ist nicht beabsichtigt , andere Ausnahmen auszulösen, wenn das Objekt oder der Code beschädigt ist.

Was wir wirklich vermeiden wollen, ist das Abfangen von Ausnahmen auf niedriger Ebene, die durch Objekte auf hoher Ebene geleitet wurden. Eine Stapelklasse, die einen ArrayIndexError auslöst, wenn sie von einem leeren Stapel abruft, ist ein geringfügiges Problem. Wenn Sie diesen ArrayIndexError tatsächlich abfangen, haben wir ein ernstes Problem. Die Ausbreitung von Fehlern auf niedriger Ebene ist eine weitaus weniger schwerwiegende Sünde, als sie zu fangen.

Um dies auf Ihr Beispiel einer SQLException zurückzuführen: Warum erhalten Sie SQLExceptions? Ein Grund ist, dass Sie ungültige Abfragen übergeben. Wenn Ihre Datenzugriffsebene jedoch fehlerhafte Abfragen generiert, ist sie beschädigt. Es sollte nicht versucht werden, seine Brokenness in eine DataAccessFailure-Ausnahme umzubrechen.

Eine SQLException kann jedoch auch aufgrund eines Verbindungsverlusts zur Datenbank auftreten. Meine Strategie in diesem Punkt besteht darin, die Ausnahme an der letzten Verteidigungslinie abzufangen, dem Benutzer zu melden, dass die Datenbankkonnektivität unterbrochen und heruntergefahren wurde. Da die Anwendung keinen Zugriff auf die Datenbank hat, kann nicht viel mehr getan werden.

Ich weiß nicht, wie Ihr Code aussieht. Aber es hört sich so an, als würden Sie alle Ausnahmen blind in übergeordnete Ausnahmen übersetzen. Sie sollten dies nur in relativ wenigen Fällen tun. Die meisten Ausnahmen auf niedrigeren Ebenen weisen auf Fehler in Ihrem Code hin. Das Fangen und Umwickeln ist kontraproduktiv.

Winston Ewert
quelle