Ich habe über dieses Problem schon eine Weile nachgedacht und finde mich ständig mit Vorbehalten und Widersprüchen konfrontiert. Deshalb hoffe ich, dass jemand zu folgenden Schlussfolgerungen kommen kann:
Bevorzugen Sie Ausnahmen gegenüber Fehlercodes
Soweit mir bekannt ist, sollten Sie nach vierjähriger Tätigkeit in der Branche beim Lesen von Büchern, Blogs usw. Ausnahmen auslösen und keine Fehlercodes zurückgeben (nicht unbedingt ein Fehlercode, sondern ein Fehlercode) Typ, der einen Fehler darstellt).
Aber - mir scheint das zu widersprechen ...
Codierung auf Schnittstellen, keine Implementierungen
Wir codieren in Schnittstellen oder Abstraktionen, um die Kopplung zu verringern. Wir kennen oder wollen den spezifischen Typ und die Implementierung einer Schnittstelle nicht kennen. Wie können wir also möglicherweise wissen, nach welchen Ausnahmen wir suchen sollten? Die Implementierung kann 10 verschiedene Ausnahmen auslösen oder keine. Wenn wir eine Ausnahme feststellen, treffen wir sicherlich Annahmen über die Implementierung?
Es sei denn - die Schnittstelle hat ...
Ausnahmespezifikationen
In einigen Sprachen können Entwickler angeben, dass bestimmte Methoden bestimmte Ausnahmen auslösen (Java verwendet beispielsweise das throws
Schlüsselwort.) Aus Sicht des aufrufenden Codes scheint dies in Ordnung zu sein - wir wissen explizit, welche Ausnahmen möglicherweise abgefangen werden müssen.
Aber - das scheint darauf hinzudeuten ...
Undichte Abstraktion
Warum sollte eine Schnittstelle angeben, welche Ausnahmen ausgelöst werden können? Was ist, wenn die Implementierung keine Ausnahme oder andere Ausnahmen auslösen muss? Auf Schnittstellenebene ist es unmöglich zu wissen, welche Ausnahmen eine Implementierung auslösen möchte.
Damit...
Schlussfolgern
Warum werden Ausnahmen bevorzugt, wenn sie (in meinen Augen) den Best Practices für Software zu widersprechen scheinen? Und wenn die Fehlercodes so schlecht sind (und ich nicht unbedingt die Fehlercodes verkaufen muss), gibt es eine andere Alternative? Was ist der aktuelle (oder zukünftige) Stand der Technik für die Fehlerbehandlung, der die Anforderungen der oben beschriebenen Best Practices erfüllt, sich jedoch nicht auf den Aufruf von Code zur Überprüfung des Rückgabewerts von Fehlercodes stützt?
Antworten:
Zuallererst würde ich dieser Aussage nicht zustimmen:
Dies ist nicht immer der Fall: Schauen Sie sich beispielsweise Objective-C (mit dem Foundation-Framework) an. Dort ist der NSError die bevorzugte Methode, um Fehler zu behandeln, obwohl ein Java-Entwickler wahre Ausnahmen nennt: @try, @catch, @throw, NSException class, etc.
Es ist jedoch wahr, dass viele Schnittstellen ihre Abstraktionen mit den geworfenen Ausnahmen verlieren. Ich bin der Meinung, dass dies nicht an der "Ausnahmebedingung" der Fehlerausbreitung / -behandlung liegt. Im Allgemeinen glaube ich, dass der beste Rat zur Fehlerbehandlung der folgende ist:
Behandeln Sie den Fehler / die Ausnahme auf der niedrigstmöglichen Ebene
Ich denke, wenn man sich an diese Faustregel hält, kann die Menge an "Leckagen" aus Abstraktionen sehr begrenzt und begrenzt sein.
Ich bin der Meinung, dass Ausnahmen, die von einer Methode ausgelöst werden, Teil ihrer Deklaration sein sollten: Sie sind Teil des Vertrags , der durch diese Schnittstelle definiert wird: Diese Methode führt A aus oder schlägt mit B oder C fehl.
Wenn es sich bei einer Klasse beispielsweise um einen XML-Parser handelt, sollte ein Teil des Entwurfs anzeigen, dass die bereitgestellte XML-Datei einfach falsch ist. In Java tun Sie dies normalerweise, indem Sie die erwarteten Ausnahmen deklarieren und sie zum
throws
Teil der Deklaration der Methode hinzufügen . Wenn andererseits einer der Parsing-Algorithmen fehlschlägt, gibt es keinen Grund, diese Ausnahme über unbehandelt zu bestehen.Auf eines kommt es an: Gutes Interface-Design. Wenn Sie Ihre Benutzeroberfläche gut genug gestalten, sollten Sie nicht von einer Reihe von Ausnahmen heimgesucht werden. Ansonsten stören Sie nicht nur Ausnahmen.
Ich denke auch, dass die Entwickler von Java sehr starke Sicherheitsgründe hatten, um Ausnahmen von einer Methodendeklaration / -definition aufzunehmen.
Eine letzte Sache: Einige Sprachen, zum Beispiel Eiffel, haben andere Mechanismen zur Fehlerbehandlung und enthalten einfach keine Wurfmöglichkeiten. Dort wird automatisch eine Art 'Ausnahme' ausgelöst, wenn eine Nachbedingung für eine Routine nicht erfüllt ist.
quelle
goto
sind sehr unterschiedlich. Beispielsweise gehen Ausnahmen immer in die gleiche Richtung - den Aufrufstapel hinunter. Und zweitens ist der Schutz vor Überraschungsausnahmen genau die gleiche Vorgehensweise wie bei DRY. Wenn Sie beispielsweise in C ++ RAII verwenden, um die Bereinigung sicherzustellen, wird in allen Fällen die Bereinigung sichergestellt , nicht nur bei Ausnahmen, sondern auch bei allen normalen Kontrollabläufen. Dies ist unendlich zuverlässiger.try/finally
etwas Ähnliches vollbringt. Wenn Sie die Bereinigung ordnungsgemäß garantieren, müssen Sie Ausnahmen nicht als Sonderfall betrachten.Ich möchte nur darauf hinweisen, dass Ausnahmen und Fehlercodes nicht die einzige Möglichkeit sind, mit Fehlern und alternativen Codepfaden umzugehen.
Aus dem Kopf können Sie einen Ansatz wie den von Haskell wählen, bei dem Fehler über abstrakte Datentypen mit mehreren Konstruktoren signalisiert werden können (denken Sie an diskriminierte Enums oder Null-Zeiger, aber typsicher und mit der Möglichkeit, Syntax hinzuzufügen Zucker- oder Hilfsfunktionen, die den Code fließen lassen).
operationThatMightfail ist eine Funktion, die einen in Vielleicht eingeschlossenen Wert zurückgibt. Es funktioniert wie ein Zeiger, der auf null gesetzt werden kann, aber die do-Notation garantiert, dass das Ganze auf null gesetzt wird, wenn eines von a, b oder c fehlschlägt. (und der Compiler schützt Sie vor versehentlicher NullPointerException)
Eine andere Möglichkeit ist die Übergabe eines Fehlerbehandlungsobjekts als zusätzliches Argument an jede aufgerufene Funktion. Dieser Fehlerbehandler verfügt über eine Methode für jede mögliche "Ausnahme", die von der Funktion signalisiert werden kann, an die Sie ihn übergeben, und die von dieser Funktion verwendet werden kann, um die Ausnahmen dort zu behandeln, wo sie auftreten, ohne dass der Stapel notwendigerweise über Ausnahmen zurückgespult werden muss.
Common LISP tut dies und macht es möglich, indem syntaktische Unterstützung (implizite Argumente) und die eingebauten Funktionen diesem Protokoll folgen.
quelle
Maybe
ist.Ja, Ausnahmen können undichte Abstraktionen verursachen. Aber sind Fehlercodes in dieser Hinsicht nicht noch schlimmer?
Eine Möglichkeit, dieses Problem zu lösen, besteht darin, dass die Schnittstelle genau angibt, welche Ausnahmen unter welchen Umständen ausgelöst werden können, und dass Implementierungen ihr internes Ausnahmemodell dieser Spezifikation zuordnen müssen, indem Ausnahmen abgefangen, konvertiert und gegebenenfalls erneut ausgelöst werden. Wenn Sie eine "Präfekt" -Schnittstelle möchten, ist dies der richtige Weg.
In der Praxis ist es normalerweise ausreichend, Ausnahmen anzugeben, die logisch Teil der Schnittstelle sind und die ein Client möglicherweise abfangen und bearbeiten möchte. Es ist allgemein bekannt, dass es andere Ausnahmen geben kann, wenn Fehler auf niedriger Ebene auftreten oder ein Fehler auftritt, und die ein Client im Allgemeinen nur behandeln kann, indem er eine Fehlermeldung anzeigt und / oder die Anwendung herunterfährt. Zumindest kann die Ausnahme noch Informationen enthalten, die bei der Diagnose des Problems hilfreich sind.
Tatsächlich geschieht mit Fehlercodes so ziemlich das Gleiche, nur auf implizitere Weise und mit einer weitaus größeren Wahrscheinlichkeit, dass Informationen verloren gehen und die App in einem inkonsistenten Zustand endet.
quelle
getSQLState
(generische) undgetErrorCode
(herstellerspezifische). Wenn es nur richtige Unterklassen hätte ...Viele gute Sachen hier, ich möchte nur hinzufügen, dass wir alle vorsichtig mit Code sein sollten, der Ausnahmen als Teil des normalen Kontrollflusses verwendet. Manchmal geraten Menschen in eine Falle, in der alles, was nicht der übliche Fall ist, zur Ausnahme wird. Ich habe sogar eine Ausnahme gesehen, die als Bedingung für die Beendigung einer Schleife verwendet wurde.
Ausnahmen bedeuten: "Etwas, mit dem ich hier nicht umgehen kann, ist passiert. Ich muss zu jemand anderem gehen, um herauszufinden, was zu tun ist." Ein Benutzer, der eine ungültige Eingabe eingibt, ist keine Ausnahme (dies sollte von der Eingabe lokal behandelt werden, indem er erneut fragt usw.).
Ein weiterer entarteter Fall von Ausnahmeverwendung, den ich gesehen habe, sind Menschen, deren erste Antwort "eine Ausnahme auslösen" lautet. Dies geschieht fast immer ohne das Schreiben des catch (Faustregel: Schreiben Sie zuerst den catch, dann die throw-Anweisung). In großen Anwendungen wird dies problematisch, wenn eine nicht abgefangene Ausnahme aus den Unterregionen auftaucht und das Programm in die Luft sprengt.
Ich bin kein Anti-Ausnahmefall, aber sie scheinen Singletons von vor ein paar Jahren zu sein: Sie werden viel zu häufig und unangemessen verwendet. Sie sind perfekt für den beabsichtigten Gebrauch, aber dieser Fall ist nicht so umfassend, wie manche meinen.
quelle
Nee. Ausnahmespezifikationen befinden sich im selben Bereich wie Rückgabe- und Argumenttypen - sie sind Teil der Schnittstelle. Wenn Sie diese Spezifikation nicht einhalten können, implementieren Sie die Schnittstelle nicht. Wenn Sie nie werfen, ist das in Ordnung. Die Angabe von Ausnahmen in einer Schnittstelle ist nicht undicht.
Fehlercodes sind mehr als schlecht. Sie sind schrecklich. Sie müssen manuell daran denken, sie jedes Mal für jeden Anruf zu überprüfen und weiterzugeben. Dies verstößt zunächst gegen DRY und sprengt massiv Ihren Fehlerbehandlungscode. Diese Wiederholung ist ein weitaus größeres Problem als alle Ausnahmen. Sie können eine Ausnahme niemals stillschweigend ignorieren, aber die Leute können und tun es stillschweigend, um Rückkehrcodes zu ignorieren - definitiv eine schlechte Sache.
quelle
Die Ausnahmebehandlung kann über eine eigene Schnittstellenimplementierung verfügen. Führen Sie je nach Art der Ausnahmebedingung die gewünschten Schritte aus.
Die Lösung für Ihr Entwurfsproblem besteht darin, zwei Schnittstellen- / Abstraktionsimplementierungen zu haben. Eine für die Funktionalität und die andere für die Ausnahmebehandlung. Rufen Sie abhängig vom Typ der erfassten Ausnahme die entsprechende Ausnahmetypklasse auf.
Die Implementierung von Fehlercodes ist eine orthodoxe Methode zur Behandlung von Ausnahmen. Es ist wie die Verwendung von String vs. String Builder.
quelle
IM-very-HO-Ausnahmen sind von Fall zu Fall zu beurteilen, da sie durch eine Unterbrechung des Kontrollflusses die tatsächliche und wahrgenommene Komplexität Ihres Codes in vielen Fällen unnötig erhöhen. Wenn Sie die Diskussion über das Auslösen von Ausnahmen in Ihren Funktionen beiseite lassen - was Ihren Steuerungsfluss tatsächlich verbessern kann -, sollten Sie Folgendes berücksichtigen, um Ausnahmen über Aufrufgrenzen hinweg auszulösen:
Das Zulassen, dass ein Angerufener Ihren Kontrollfluss unterbricht, bietet möglicherweise keinen wirklichen Vorteil, und es gibt möglicherweise keine sinnvolle Möglichkeit, mit der Ausnahme umzugehen. Ein direktes Beispiel: Wenn Sie das Observable-Muster implementieren (in einer Sprache wie C #, in der überall Ereignisse vorliegen und die
throws
in der Definition nicht explizit angegeben ist ), gibt es keinen wirklichen Grund dafür, dass der Observer Ihren Kontrollfluss unterbricht, wenn er abstürzt kein sinnvoller umgang mit ihren sachen (natürlich sollte ein guter nachbar beim beobachten nicht werfen, aber niemand ist perfekt).Die obige Beobachtung kann auf jede lose gekoppelte Schnittstelle ausgedehnt werden (wie Sie betont haben); Ich denke, es ist eigentlich eine Norm, dass nach dem Erstellen von 3-6 Stack-Frames eine nicht erfasste Ausnahme wahrscheinlich in einem Codeabschnitt endet, der entweder:
In Anbetracht des oben Gesagten ist das Dekorieren von Schnittstellen mit
throws
Semantik nur ein geringfügiger Funktionsgewinn, da sich viele Anrufer über Schnittstellenverträge nur darum kümmern würden, wenn Sie scheitern, und nicht darum, warum.Ich würde sagen, es wird dann eine Frage des Geschmacks und der Zweckmäßigkeit: Ihr Hauptaugenmerk liegt darauf, nach einer "Ausnahme" Ihren Zustand sowohl beim Anrufer als auch beim Angerufenen ordnungsgemäß wiederherzustellen, wenn Sie viel Erfahrung mit dem Verschieben von Fehlercodes haben aus einem C-Hintergrund) oder wenn Sie in einer Umgebung arbeiten, in der Ausnahmen böse werden können (C ++), glaube ich nicht, dass das Herumwerfen von Dingen für eine nette, saubere OOP so wichtig ist, dass Sie sich nicht auf Ihre alte verlassen können Muster, wenn Sie mit ihm unangenehm sind. Vor allem, wenn es zu einem Bruch des SoC kommt.
Aus theoretischer Sicht kann eine SoC-koschere Art der Behandlung von Ausnahmen direkt aus der Beobachtung abgeleitet werden, dass der direkte Anrufer sich in den meisten Fällen nur darum kümmert, dass Sie gescheitert sind, und nicht darum, warum. Der Angerufene wirft, jemand ganz in der Nähe (2-3 Frames) fängt eine aufgestaute Version ab und die eigentliche Ausnahme wird immer an einen spezialisierten Fehlerbehandler (auch wenn nur die Verfolgung) gesenkt - hier wäre AOP nützlich, weil diese Behandler sind wahrscheinlich horizontal.
quelle
Beide sollten koexistieren.
Gibt einen Fehlercode zurück, wenn Sie ein bestimmtes Verhalten erwarten.
Ausnahmebedingung zurückgeben, wenn Sie nicht mit einem Verhalten gerechnet haben.
Fehlercodes werden normalerweise mit einer einzelnen Nachricht verknüpft, wenn der Ausnahmetyp erhalten bleibt. Eine Nachricht kann jedoch variieren
Die Ausnahme hat eine Stapelverfolgung, wenn der Fehlercode dies nicht tut. Ich verwende keine Fehlercodes, um ein defektes System zu debuggen.
Dies mag spezifisch für JAVA sein, aber wenn ich meine Schnittstellen deklariere, gebe ich nicht an, welche Ausnahmen von einer Implementierung dieser Schnittstelle ausgelöst werden könnten, es macht einfach keinen Sinn.
Dies liegt ganz bei Ihnen. Sie können versuchen, eine ganz bestimmte Art von Ausnahme abzufangen und dann eine allgemeinere abzufangen
Exception
. Warum nicht Exception den Stack verbreiten lassen und dann damit umgehen? Alternativ können Sie sich die Aspektprogrammierung ansehen, bei der die Ausnahmebehandlung zu einem "steckbaren" Aspekt wird.Ich verstehe nicht, warum es ein Problem für Sie ist. Ja, Sie haben möglicherweise eine Implementierung, die niemals fehlschlägt oder Ausnahmen auslöst, und Sie haben möglicherweise eine andere Implementierung, die ständig fehlschlägt und Ausnahmen auslöst. Wenn dies der Fall ist, geben Sie in der Schnittstelle keine Ausnahmen an, und Ihr Problem ist behoben.
Würde es etwas ändern, wenn Ihre Implementierung anstelle einer Ausnahme ein Ergebnisobjekt zurückgeben würde? Dieses Objekt enthält das Ergebnis Ihrer Aktion sowie etwaige Fehler / Ausfälle. Sie können dieses Objekt dann abfragen.
quelle
Nach meiner Erfahrung würde der Code, der den Fehler empfängt (sei es über eine Ausnahme, einen Fehlercode oder etwas anderes), die genaue Fehlerursache normalerweise nicht berücksichtigen - er würde auf die gleiche Weise auf einen Fehler reagieren, mit Ausnahme einer möglichen Meldung des Fehlers Fehler (sei es ein Fehlerdialog oder eine Art Protokoll); und diese Berichterstattung würde orthogonal zu dem Code erfolgen, der die fehlerhafte Prozedur aufgerufen hat. Beispielsweise könnte dieser Code den Fehler an einen anderen Teil des Codes weiterleiten, der bestimmte Fehler zu melden weiß (z. B. Formatieren einer Nachrichtenzeichenfolge) und möglicherweise Kontextinformationen anfügt.
Natürlich ist es in einigen Fällen wird benötigt spezifische Semantik , um Fehler zu befestigen und zu unterschiedlich , auf dem react basierter Fehler ist aufgetreten. Solche Fälle sollten in der Schnittstellenspezifikation dokumentiert werden. Die Schnittstelle behält sich jedoch möglicherweise das Recht vor, andere Ausnahmen ohne besondere Bedeutung auszulösen.
quelle
Ich finde, dass Ausnahmen es ermöglichen, einen strukturierteren und präziseren Code für das Melden und Behandeln von Fehlern zu schreiben: Um Fehlercodes zu verwenden, müssen die Rückgabewerte nach jedem Aufruf überprüft und entschieden werden, was im Falle eines unerwarteten Ergebnisses zu tun ist.
Andererseits stimme ich zu, dass Ausnahmen Implementierungsdetails enthalten, die für den Code, der eine Schnittstelle aufruft, verborgen sein sollten. Da nicht von vornherein bekannt ist, welcher Code welche Ausnahmen auslösen kann (es sei denn, sie sind in der Methodensignatur wie in Java deklariert), führen wir mithilfe von Ausnahmen sehr komplexe implizite Abhängigkeiten zwischen verschiedenen Teilen des Codes ein gegen das Prinzip der Minimierung von Abhängigkeiten.
Zusammenfassend:
quelle
catch Exception
oder sogarThrowable
oder gleichwertig).Entweder nach rechts voreingenommen.
Es kann nicht ignoriert werden, es muss gehandhabt werden, es ist völlig transparent. Und WENN Sie den richtigen Linkshänder-Fehlertyp verwenden, werden die gleichen Informationen wie bei einer Java-Ausnahme übertragen.
Nachteil? Code mit korrekter Fehlerbehandlung sieht widerlich aus (gilt für alle Mechanismen).
quelle