Was ist eine "gute Anzahl" von Ausnahmen, die für meine Bibliothek implementiert werden müssen?

20

Ich habe mich immer gefragt, wie viele verschiedene Ausnahmeklassen ich für verschiedene Teile meiner Software implementieren und werfen soll. Meine besondere Entwicklung ist normalerweise C ++ / C # / Java-bezogen, aber ich glaube, dass dies eine Frage für alle Sprachen ist.

Ich möchte verstehen, welche Anzahl von Ausnahmen zu werfen ist und was die Entwicklergemeinde von einer guten Bibliothek erwartet.

Die Kompromisse, die ich sehe, umfassen:

  • Weitere Ausnahmeklassen können für API-Benutzer eine sehr genaue Fehlerbehandlung ermöglichen (anfällig für Benutzerkonfigurations- oder Datenfehler oder nicht gefundene Dateien).
  • Weitere Ausnahmeklassen ermöglichen, dass fehlerspezifische Informationen in die Ausnahme eingebettet werden und nicht nur eine Zeichenfolgemeldung oder ein Fehlercode
  • Mehr Ausnahmeklassen können mehr Code-Wartung bedeuten
  • Mehr Ausnahmeklassen können bedeuten, dass die API für Benutzer weniger zugänglich ist

Die Szenarien, in denen ich die Ausnahmeverwendung verstehen möchte, umfassen:

  • Während der Konfigurationsphase, die das Laden von Dateien oder das Einstellen von Parametern umfassen kann
  • Während einer Phase des Typs 'Operation', in der die Bibliothek möglicherweise Aufgaben ausführt und einige Arbeiten ausführt, möglicherweise in einem anderen Thread

Andere Muster der Fehlerberichterstattung ohne Ausnahmen oder mit weniger Ausnahmen (als Vergleich) können Folgendes umfassen:

  • Weniger Ausnahmen, aber Einbettung eines Fehlercodes, der als Suche verwendet werden kann
  • Fehlercodes und Flags direkt von Funktionen zurückgeben (manchmal nicht möglich von Threads)
  • Implementierung eines Ereignis- oder Rückrufsystems bei einem Fehler (vermeidet das Abwickeln des Stapels)

Was sehen Sie als Entwickler am liebsten?

Wenn es VIELE Ausnahmen gibt, stört es Sie trotzdem, diese separat zu behandeln?

Bevorzugen Sie Fehlerbehandlungsarten je nach Betriebsstadium?

Flaum
quelle
1
Vielleicht im Zusammenhang: programmers.stackexchange.com/questions/3713/…
Fuzz
5
"Die API ist für Benutzer weniger zugänglich" - kann durch mehrere Ausnahmeklassen behoben werden, die alle von einer gemeinsamen Basisklasse für die API erben. Dann können Sie gleich zu Beginn sehen, von welcher API die Ausnahme stammt, ohne sich um alle Details kümmern zu müssen. Wenn Sie Ihr erstes Programm mit der API fertigstellen und weitere Informationen zu dem Fehler benötigen, werden Sie sich mit der Bedeutung der verschiedenen Ausnahmen befassen. Natürlich müssen Sie gleichzeitig gut mit der Standard-Ausnahmehierarchie der von Ihnen verwendeten Sprache spielen.
Steve Jessop
Passender Punkt Steve
Fuzz

Antworten:

18

Ich halte es einfach.

Eine Bibliothek hat einen Basis-Ausnahmetyp, der von std ::: runtime_error erweitert wurde (das gilt von C ++ aus entsprechend für andere Sprachen). Diese Ausnahme benötigt eine Nachrichtenzeichenfolge, damit wir protokollieren können. Jeder Wurfpunkt hat eine eindeutige Nachricht (normalerweise mit einer eindeutigen ID).

Das ist alles.

Hinweis 1 : In Situationen, in denen jemand, der die Ausnahme abfängt, die Ausnahmen beheben und die Aktion neu starten kann. Ich werde abgeleitete Ausnahmen für Dinge hinzufügen, die möglicherweise eindeutig an einem entfernten Ort behoben werden können. Dies ist jedoch sehr, sehr selten (Denken Sie daran, dass der Fänger wahrscheinlich nicht in der Nähe des Wurfpunkts ist, sodass die Behebung des Problems schwierig sein wird (aber alles ist situationsabhängig)).

Anmerkung 2 : Manchmal ist die Bibliothek so einfach, dass es sich nicht lohnt, ihr eine eigene Ausnahme zu geben, und std :: runtime_error wird dies tun. Es ist nur wichtig, eine Ausnahme zu haben, wenn die Fähigkeit, sie von std :: runtime_error zu unterscheiden, dem Benutzer genügend Informationen geben kann, um etwas damit zu tun.

Anmerkung 3 : Innerhalb einer Klasse bevorzuge ich normalerweise Fehlercodes (diese werden jedoch in der öffentlichen API meiner Klasse niemals ausgeblendet).

Betrachten Sie Ihre Kompromisse:

Die Kompromisse, die ich sehe, umfassen:

Weitere Ausnahmeklassen können für API-Benutzer eine sehr genaue Fehlerbehandlung ermöglichen (anfällig für Benutzerkonfigurations- oder Datenfehler oder nicht gefundene Dateien).

Ermöglichen Ihnen mehr Ausnahmen wirklich eine feinere Getreidesteuerung? Die Frage wird, ob der abfangende Code den Fehler wirklich anhand der Ausnahme beheben kann. Ich bin sicher, dass es solche Situationen gibt und in diesen Fällen sollten Sie eine weitere Ausnahme haben. Alle Ausnahmen, die Sie oben aufgeführt haben, sind jedoch die Generierung einer großen Warnung und das Stoppen der Anwendung.

Weitere Ausnahmeklassen ermöglichen, dass fehlerspezifische Informationen in die Ausnahme eingebettet werden und nicht nur eine Zeichenfolgemeldung oder ein Fehlercode

Dies ist ein guter Grund für die Verwendung von Ausnahmen. Die Informationen müssen jedoch für die Person nützlich sein, die sie zwischenspeichert. Können sie die Informationen verwenden, um Korrekturmaßnahmen durchzuführen? Wenn sich das Objekt in Ihrer Bibliothek befindet und nicht zur Beeinflussung der API verwendet werden kann, sind die Informationen unbrauchbar. Sie müssen sehr genau festlegen, dass die ausgelesenen Informationen für die Person, die sie abfangen kann, einen nützlichen Wert haben. Die Person, die es abfängt, befindet sich normalerweise außerhalb Ihrer öffentlichen API. Passen Sie daher Ihre Informationen so an, dass sie mit den Dingen in Ihrer öffentlichen API verwendet werden können.

Wenn sie nur die Ausnahme protokollieren können, ist es am besten, statt vieler Daten nur eine Fehlermeldung auszugeben. Da der Fänger in der Regel eine Fehlermeldung mit den Daten aufbaut. Wenn Sie die Fehlermeldung erstellen, ist sie für alle Catcher konsistent. Wenn Sie dem Catcher ermöglichen, die Fehlermeldung zu erstellen, wird derselbe Fehler möglicherweise unterschiedlich gemeldet, je nachdem, wer anruft und abfängt.

Weniger Ausnahmen, aber Einbettung eines Fehlercodes, der als Suche verwendet werden kann

Sie müssen feststellen, ob der Fehlercode sinnvoll verwendet werden kann. Wenn dies möglich ist, sollten Sie eine eigene Ausnahme haben. Andernfalls müssen Ihre Benutzer jetzt switch-Anweisungen in catch implementieren (was den ganzen Punkt zunichte macht, wenn catch automatisch mit Dingen umgeht).

Wenn dies nicht möglich ist, verwenden Sie in der Ausnahme eine Fehlermeldung (Sie müssen den Code nicht aufteilen, und die Meldung macht es schwierig, nachzuschlagen).

Fehlercodes und Flags direkt von Funktionen zurückgeben (manchmal nicht möglich von Threads)

Das Zurückgeben von Fehlercodes ist intern großartig. Es ermöglicht Ihnen, Fehler dort und dann zu beheben, und Sie müssen sicherstellen, dass Sie alle Fehlercodes beheben und diese berücksichtigen. Es ist jedoch eine schlechte Idee, sie über Ihre öffentliche API zu verbreiten. Das Problem ist, dass Programmierer häufig vergessen, nach Fehlerzuständen zu suchen (zumindest mit einer Ausnahme, wird ein ungeprüfter Fehler die Anwendung zwingen, einen nicht behandelten Fehler zu beenden, wodurch im Allgemeinen alle Ihre Daten beschädigt werden).

Implementierung eines Ereignis- oder Rückrufsystems bei einem Fehler (vermeidet das Abwickeln des Stapels)

Diese Methode wird häufig in Verbindung mit anderen Fehlerbehandlungsmechanismen verwendet (nicht als Alternative). Denken Sie an Ihr Windows-Programm. Ein Benutzer leitet eine Aktion ein, indem er einen Menüpunkt auswählt. Dadurch wird eine Aktion in der Ereigniswarteschlange generiert. Die Ereigniswarteschlange weist schließlich einen Thread zu, der die Aktion handhabt. Der Thread soll die Aktion ausführen und schließlich zum Thread-Pool zurückkehren und auf eine weitere Aufgabe warten. Hier muss eine Ausnahme an der Basis von dem mit dem Job beauftragten Thread abgefangen werden. Das Ergebnis des Erfassens der Ausnahme führt normalerweise dazu, dass ein Ereignis für die Hauptschleife generiert wird, das schließlich dazu führt, dass dem Benutzer eine Fehlermeldung angezeigt wird.

Aber es sei denn, Sie können trotz der Ausnahme weitermachen, der Stapel wird sich abwickeln (zumindest für den Thread).

Martin York
quelle
+1; Tho "Andernfalls müssen Ihre Benutzer jetzt switch-Anweisungen in catch implementieren (was den ganzen Punkt zunichte macht, wenn catch automatisch mit Dingen umgeht)." - Das Abwickeln des Aufrufstapels (wenn Sie ihn noch verwenden können) und die erzwungene Fehlerbehandlung sind nach wie vor große Vorteile. Aber es wird sicherlich die Fähigkeit beeinträchtigen, Call-Stack-Abwicklungen durchzuführen, wenn Sie es in der Mitte fangen und erneut werfen müssen.
Merlyn Morgan-Graham
9

Normalerweise beginne ich mit:

  1. Eine Ausnahmeklasse für Argumentfehler . ZB "Argument darf nicht null sein", "Argument muss positiv sein" usw. Java und C # haben vordefinierte Klassen für diese; In C ++ erstelle ich normalerweise nur eine Klasse, die von std :: exception abgeleitet ist.
  2. Eine Ausnahmeklasse für Vorbedingungsfehler . Diese sind für komplexere Tests wie "Index muss kleiner als die Größe sein".
  3. Eine Ausnahmeklasse für Assertion Bugs . Diese dienen zur Überprüfung des Zustands auf Konstanz auf halbem Weg. Wenn Sie beispielsweise eine Liste durchlaufen, um Elemente zu zählen, die negativ, null oder positiv sind, sollten sich diese drei Elemente zur Größe addieren.
  4. Eine Basisausnahmeklasse für die Bibliothek selbst. Zuerst werfen Sie einfach diese Klasse. Fügen Sie erst dann Unterklassen hinzu, wenn dies erforderlich ist.
  5. Ich ziehe es vor, Ausnahmen nicht einzuschließen, aber ich weiß, dass die Meinungen in diesem Punkt unterschiedlich sind (und sehr stark mit der verwendeten Sprache korrelieren). Wenn Sie einen Zeilenumbruch durchführen, benötigen Sie zusätzliche Zeilenumbruch-Ausnahmeklassen .

Da es sich bei den Klassen für die ersten drei Fälle um Debugging-Hilfen handelt, ist nicht vorgesehen, dass sie vom Code behandelt werden. Stattdessen sollten sie nur von einem Top-Level-Handler abgefangen werden, der die Informationen so anzeigt, dass der Benutzer sie dem Entwickler kopieren und einfügen kann (oder noch besser: Klicken Sie auf die Schaltfläche "Bericht senden"). Fügen Sie also Informationen hinzu, die für den Entwickler nützlich sind: Datei, Funktion, Zeilennummer und eine Meldung, die eindeutig angibt, welche Prüfung fehlgeschlagen ist.

Da die ersten drei Fälle für jedes Projekt gleich sind, kopiere ich sie in C ++ normalerweise nur aus dem vorherigen Projekt. Da viele genau dasselbe tun, haben die Designer von C # und Java der Standardbibliothek Standardklassen für diese Fälle hinzugefügt. [UPDATE:] Für die faulen Programmierer: Eine Klasse könnte ausreichen, und mit etwas Glück hat Ihre Standardbibliothek bereits eine geeignete Ausnahmeklasse. Ich bevorzuge es, Informationen wie Dateiname und Leinennummer hinzuzufügen, die die Standardklassen in C ++ nicht bereitstellen. [Update beenden]

Abhängig von der Bibliothek könnte der vierte Fall nur eine Klasse haben oder eine Handvoll Klassen werden. Ich ziehe es vor, dass der agile Ansatz einfach beginnt und Unterklassen hinzufügt, wenn dies erforderlich ist.

Eine ausführliche Argumentation zu meinem vierten Fall finden Sie in der Antwort von Loki Astari . Ich stimme seiner detaillierten Antwort voll und ganz zu.

Sjoerd
quelle
+1; Es gibt einen deutlichen Unterschied zwischen dem Schreiben von Schreibausnahmen in einem Framework und dem Schreiben von Ausnahmen in einer Anwendung. Die Unterscheidung ist ein wenig unscharf, aber Sie erwähnen es (mit Verweis auf Loki für den Bibliothekskoffer).
Merlyn Morgan-Graham