So gestalten Sie Ausnahmen


11

Ich kämpfe mit einer sehr einfachen Frage:

Ich arbeite jetzt an einer Serveranwendung und muss eine Hierarchie für die Ausnahmen erfinden (einige Ausnahmen existieren bereits, aber ein allgemeines Framework ist erforderlich). Wie fange ich überhaupt damit an?

Ich denke darüber nach, diese Strategie zu verfolgen:

1) Was läuft falsch?

  • Es wird etwas gefragt, was nicht erlaubt ist.
  • Es wird etwas gefragt, es ist erlaubt, aber es funktioniert aufgrund falscher Parameter nicht.
  • Es wird etwas gefragt, es ist erlaubt, aber es funktioniert aufgrund interner Fehler nicht.

2) Wer startet die Anfrage?

  • Die Client-Anwendung
  • Eine andere Serveranwendung

3) Nachrichtenübergabe: Beim Umgang mit einer Serveranwendung dreht sich alles um das Empfangen und Senden von Nachrichten. Was ist, wenn das Senden einer Nachricht schief geht?

Daher erhalten wir möglicherweise folgende Ausnahmetypen:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (falls der Server nicht weiß, woher die Anforderung kommt)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Dies ist ein sehr allgemeiner Ansatz zur Definition der Ausnahmehierarchie, aber ich befürchte, dass mir einige offensichtliche Fälle fehlen könnten. Haben Sie Ideen zu den Bereichen, die ich nicht abdecke, sind Sie sich der Nachteile dieser Methode bewusst oder gibt es einen allgemeineren Ansatz für diese Art von Frage (im letzteren Fall, wo finde ich sie)?

Danke im Voraus


5
Sie haben nicht erwähnt, was Sie mit Ihrer Ausnahmeklassenhierarchie erreichen möchten (und das ist überhaupt nicht offensichtlich). Sinnvolle Protokollierung? Kunden ermöglichen, angemessen auf die verschiedenen Ausnahmen zu reagieren? Oder was?
Ralf Kleberhoff

2
Es ist wahrscheinlich nützlich, zuerst einige Geschichten und Anwendungsfälle durchzuarbeiten und zu sehen, was dabei herauskommt. Beispiel: Clientanfragen X sind nicht zulässig. Client fordert X an , aber die Anfrage ist ungültig. Überlegen Sie, wer mit der Ausnahme umgehen soll, was sie damit tun können (Eingabeaufforderung, Wiederholung, was auch immer) und welche Informationen sie benötigen, um dies gut zu machen. Dann , wenn Sie wissen , was Ihre konkreten Ausnahmen sind und welche Informationen die Handler benötigen , sie zu verarbeiten, können Sie sie in eine schöne Hierarchie bilden.
Nutzlos


1
Ich habe den Wunsch, so viele verschiedene Ausnahmetypen verwenden zu wollen, nie wirklich verstanden. Normalerweise habe catchich für die meisten Blöcke, die ich verwende, nicht viel mehr Verwendung für die Ausnahme als für die darin enthaltene Fehlermeldung. Ich habe nicht wirklich etwas anderes, was ich für eine Ausnahme tun kann, die darin besteht, dass eine Datei nicht gelesen werden kann, da eine Datei beim Lesen nicht zugeordnet werden kann. Daher neige ich dazu, nur std::exceptiondie darin enthaltene Fehlermeldung abzufangen und zu melden, möglicherweise zu dekorieren es mit "Failed to open file: %s", ex.what()einem Stapelpuffer vor dem Drucken.

3
Außerdem kann ich nicht alle Ausnahmetypen vorhersehen, die überhaupt ausgelöst werden. Ich könnte sie jetzt vielleicht vorwegnehmen, aber Kollegen könnten in Zukunft neue einführen, z. B. ist es für mich hoffnungslos, für jetzt und für immer all die verschiedenen Arten von Ausnahmen zu kennen, die im Verlauf eines Prozesses ausgelöst werden könnten Betrieb. Also fange ich generell einfach super mit einem einzigen Fangblock. Ich habe Beispiele von Leuten gesehen, die viele verschiedene catchBlöcke in einer einzelnen Wiederherstellungssite verwenden, aber oft ist es nur, die Nachricht innerhalb der Ausnahme zu ignorieren und eine lokalere Nachricht zu drucken ...

Antworten:


5

Allgemeine Bemerkungen

(ein bisschen meinungsorientiert)

Normalerweise würde ich mich nicht für eine detaillierte Ausnahmehierarchie entscheiden.

Das Wichtigste: Eine Ausnahme teilt Ihrem Aufrufer mit, dass Ihre Methode ihren Job nicht abgeschlossen hat. Und Ihr Anrufer muss darüber informiert werden, damit er nicht einfach fortfährt. Das funktioniert mit jeder Ausnahme, egal welche Ausnahmeklasse Sie wählen.

Der zweite Aspekt ist die Protokollierung. Sie möchten aussagekräftige Protokolleinträge finden, wenn etwas schief geht. Das erfordert auch keine unterschiedlichen Ausnahmeklassen, nur gut gestaltete Textnachrichten (ich nehme an, Sie benötigen keinen Automaten, um Ihre Fehlerprotokolle zu lesen ...).

Der dritte Aspekt ist die Reaktion Ihres Anrufers. Was kann Ihr Anrufer tun, wenn er eine Ausnahme erhält? Hier kann es sinnvoll sein, unterschiedliche Ausnahmeklassen zu haben, damit der Anrufer entscheiden kann, ob er denselben Aufruf erneut versucht, eine andere Lösung verwendet (z. B. stattdessen eine Fallback-Quelle verwendet) oder aufgibt.

Und vielleicht möchten Sie Ihre Ausnahmen als Grundlage verwenden, um den Endbenutzer über das Problem zu informieren. Das bedeutet, dass neben dem Admin-Text für die Protokolldatei eine benutzerfreundliche Nachricht erstellt wird, jedoch keine unterschiedlichen Ausnahmeklassen erforderlich sind (obwohl dies möglicherweise die Texterstellung erleichtern kann ...).

Ein wichtiger Aspekt für die Protokollierung (und für Benutzerfehlermeldungen) ist die Möglichkeit, die Ausnahme mit Kontextinformationen zu ändern, indem sie auf einer bestimmten Ebene abgefangen, einige Kontextinformationen, z. B. Methodenparameter, hinzugefügt und erneut ausgelöst werden.

Deine Hierarchie

Wer startet die Anfrage? Ich glaube nicht, dass Sie die Informationen benötigen, die die Anfrage gestartet haben. Ich kann mir gar nicht vorstellen, woher du das tief in einem Call-Stack weißt.

Nachrichtenbehandlung : Das ist kein anderer Aspekt, sondern nur zusätzliche Fälle für "Was läuft falsch?".

In einem Kommentar sprechen Sie beim Erstellen einer Ausnahme von einem Flag " Keine Protokollierung ". Ich glaube nicht, dass Sie an dem Ort, an dem Sie eine Ausnahme erstellen und auslösen, eine zuverlässige Entscheidung treffen können, ob Sie diese Ausnahme protokollieren möchten oder nicht.

Die einzige Situation, die ich mir vorstellen kann, ist, dass eine höhere Schicht Ihre API auf eine Weise verwendet, die manchmal Ausnahmen erzeugt, und diese Schicht weiß dann, dass sie keinen Administrator mit der Ausnahme belästigen muss, sodass sie die Ausnahme stillschweigend verschluckt. Aber das ist ein Code-Geruch: Eine erwartete Ausnahme ist ein Widerspruch an sich, ein Hinweis zum Ändern der API. Und es ist die höhere Ebene, die entscheiden sollte, nicht der Code, der Ausnahmen generiert.


Nach meiner Erfahrung funktioniert ein Fehlercode in Kombination mit einem benutzerfreundlichen Text sehr gut. Administratoren können den Fehlercode verwenden, um zusätzliche Informationen zu finden.
Sjoerd

1
Ich mag die Idee hinter dieser Antwort im Allgemeinen. Aus meiner Erfahrung sollten Ausnahmen wirklich niemals zu einem zu komplexen Tier werden. Der Hauptzweck einer Ausnahme besteht darin, dem aufrufenden Code zu ermöglichen, ein bestimmtes Problem zu beheben und die relevanten Debugging- / Wiederholungsinformationen abzurufen, ohne die Antwort der Funktion zu beeinträchtigen.
greggle138

2

Das Wichtigste, das Sie beim Entwerfen eines Fehlerantwortmusters beachten sollten, ist sicherzustellen, dass es für Anrufer nützlich ist. Dies gilt unabhängig davon, ob Sie Ausnahmen oder definierte Fehlercodes verwenden. Wir beschränken uns jedoch darauf, mit Ausnahmen zu veranschaulichen.

  • Wenn Ihre Sprache oder Ihr Framework bereits allgemeine Ausnahmeklassen bereitstellt, verwenden Sie diese dort, wo sie angemessen sind und wo sie vernünftigerweise zu erwarten sind . Definieren Sie keine eigenen Klassen ArgumentNullExceptionoder ArgumentOutOfRangeAusnahmeklassen. Anrufer erwarten nicht, diese zu fangen.

  • Definieren Sie eine MyClientServerAppExceptionBasisklasse, um die Fehler zu erfassen, die im Kontext Ihrer Anwendung eindeutig sind. Wirf niemals eine Instanz der Basisklasse. Eine mehrdeutige Fehlerantwort ist DAS SCHLECHTESTE, was es je gab . Wenn es einen "internen Fehler" gibt, erklären Sie, was dieser Fehler ist.

  • Die Hierarchie unter der Basisklasse sollte größtenteils breit und nicht tief sein. Sie müssen es nur in Situationen vertiefen, in denen es für den Anrufer nützlich ist. Wenn es beispielsweise 5 Gründe gibt, warum eine Nachricht von Client zu Server fehlschlägt, können Sie eine ServerMessageFaultAusnahme definieren und dann für jeden dieser 5 Fehler darunter eine Ausnahmeklasse definieren. Auf diese Weise kann der Anrufer die Oberklasse einfach abfangen, wenn er dies benötigt oder möchte. Versuchen Sie, dies auf bestimmte, vernünftige Fälle zu beschränken.

  • Versuchen Sie nicht, alle Ihre Ausnahmeklassen zu definieren, bevor sie tatsächlich verwendet werden. Sie werden das meiste davon wiederholen. Wenn beim Schreiben von Code ein Fehlerfall auftritt, entscheiden Sie, wie dieser Fehler am besten beschrieben werden soll. Idealerweise sollte es im Kontext dessen ausgedrückt werden, was der Anrufer versucht zu tun.

  • Denken Sie im Zusammenhang mit dem vorherigen Punkt daran, dass nur weil Sie Ausnahmen verwenden, um auf Fehler zu reagieren, dies nicht bedeutet, dass Sie nur Ausnahmen für Fehlerzustände verwenden müssen. Beachten Sie, dass das Auslösen von Ausnahmen normalerweise teuer ist und die Leistungskosten von Sprache zu Sprache variieren können. Bei einigen Sprachen sind die Kosten abhängig von der Tiefe des Aufrufstapels höher. Wenn also ein Fehler tief in einem Aufrufstapel liegt, prüfen Sie, ob Sie keine einfachen primitiven Typen (ganzzahlige Fehlercodes oder boolesche Flags) zum Pushen verwenden können Der Fehler sichert den Stapel, sodass er näher an den Aufruf des Aufrufers herangeführt werden kann.

  • Wenn Sie die Protokollierung als Teil der Fehlerantwort einbeziehen, sollte es für Anrufer einfach sein, Kontextinformationen an ein Ausnahmeobjekt anzuhängen. Beginnen Sie dort, wo die Informationen im Protokollierungscode verwendet werden. Entscheiden Sie, wie viele Informationen benötigt werden, damit das Protokoll nützlich ist (ohne eine riesige Textwand zu sein). Arbeiten Sie dann rückwärts, um sicherzustellen, dass die Ausnahmeklassen problemlos mit diesen Informationen versorgt werden können.

Versuchen Sie nicht, diese oder andere katastrophale Laufzeitausnahmen zu behandeln , es sei denn, Ihre Anwendung kann einen Fehler aufgrund von Speichermangel absolut ordnungsgemäß beheben. Lassen Sie einfach das OS mit ihr umgehen, denn in Wirklichkeit, das ist alles Sie können tun.


2
Mit Ausnahme Ihrer vorletzten Kugel (über die Kosten für Ausnahmen) ist die Antwort gut. Diese Aufzählung über die Kosten von Ausnahmen ist irreführend, da bei allen gängigen Methoden zum Implementieren von Ausnahmen auf niedriger Ebene die Kosten für das Auslösen einer Ausnahme völlig unabhängig von der Tiefe des Aufrufstapels sind. Alternative Methoden zur Fehlerberichterstattung sind möglicherweise besser, wenn Sie wissen, dass der Fehler vom unmittelbaren Aufrufer behandelt wird, aber nicht einige Funktionen vom Aufrufstapel entfernt werden, bevor eine Ausnahme ausgelöst wird.
Bart van Ingen Schenau

@BartvanIngenSchenau: Ich habe versucht, dies nicht an eine bestimmte Sprache zu binden. In einigen Sprachen ( z. B. Java ) wirkt sich die Tiefe des Aufrufstapels auf die Kosten der Instanziierung aus. Ich werde bearbeiten, um zu reflektieren, dass es nicht so geschnitten und getrocknet ist.
Mark Benningfield

0

Nun, ich würde Ihnen empfehlen, in erster Linie eine Basisklasse Exceptionfür alle geprüften Ausnahmen zu erstellen, die von Ihrer Anwendung ausgelöst werden können. Wenn Ihre Anwendung aufgerufen wurde DuckType, erstellen Sie eine DuckTypeExceptionBasisklasse.

Auf diese Weise können Sie jede Ausnahme Ihrer Basisklasse DuckTypeExceptionfür die Behandlung abfangen . Von hier aus sollten Ihre Ausnahmen mit beschreibenden Namen verzweigen, die die Art des Problems besser hervorheben können. Zum Beispiel "DatabaseConnectionException".

Lassen Sie mich klar sein, dies sollten alle Ausnahmen für Situationen überprüft werden, die auftreten könnten, die Sie wahrscheinlich in Ihrem Programm ordnungsgemäß behandeln möchten. Mit anderen Worten, Sie können keine Verbindung zur Datenbank herstellen, sodass eine DatabaseConnectionExceptionausgelöst wird, die Sie abfangen können, um nach einer bestimmten Zeit zu warten und es erneut zu versuchen.

Sie würden keine geprüfte Ausnahme für ein sehr unerwartetes Problem wie eine ungültige SQL-Abfrage oder eine Nullzeiger-Ausnahme sehen, und ich würde Sie ermutigen, diese Ausnahmen die meisten catch-Klauseln überschreiten zu lassen (oder bei Bedarf abzufangen und erneut zu werfen), bis Sie zum Hauptziel gelangen Controller selbst, der dann RuntimeExceptionrein zu Protokollierungszwecken fangen kann .

Meine persönliche Präferenz ist es, eine ungeprüfte RuntimeExceptionAusnahme nicht als weitere Ausnahme erneut auszulösen , da Sie aufgrund der Natur einer nicht aktivierten Ausnahme dies nicht erwarten würden und daher das erneute Auslösen unter einer anderen Ausnahme Informationen verbirgt. Wenn dies jedoch Ihre Präferenz ist, können Sie immer noch ein fangen RuntimeExceptionund ein werfen, DuckTypeInternalExceptiondas DuckTypeExceptionsich von dem unterscheidet RuntimeExceptionund daher nicht markiert ist.

Wenn Sie es vorziehen, können Sie Ihre Ausnahmen für organisatorische Zwecke, z. B. DatabaseExceptionfür datenbankbezogene Zwecke , unterkategorisieren. Ich würde diese Unterausnahmen jedoch weiterhin dazu ermutigen, sich von Ihrer Basisausnahme abzuleiten DuckTypeExceptionund abstrakt zu sein und daher mit expliziten beschreibenden Namen abgeleitet zu werden.

In der Regel sollten Ihre Try-Catches immer allgemeiner werden, wenn Sie den Callstack von Anrufern für die Behandlung von Ausnahmen nach oben verschieben. In Ihrem Hauptcontroller würden Sie wiederum nicht eine DatabaseConnectionException, sondern die einfache Behandlung behandeln , DuckTypeExceptionvon der alle Ihre überprüften Ausnahmen abgeleitet sind.


2
Beachten Sie, dass die Frage mit "C ++" gekennzeichnet ist.
Martin Ba

0

Versuchen Sie das zu vereinfachen.

Das erste, was Ihnen hilft, über eine andere Strategie nachzudenken, ist: Viele Ausnahmen sind der Verwendung von geprüften Ausnahmen aus Java sehr ähnlich (sorry, ich bin kein C ++ - Entwickler). Dies ist aus vielen Gründen nicht gut, deshalb versuche ich immer, sie nicht zu verwenden, und Ihre Hierarchie-Ausnahmestrategie erinnert mich sehr daran.

Daher empfehle ich Ihnen eine andere und flexible Strategie: Verwenden Sie ungeprüfte Ausnahmen und Codefehler.

Siehe beispielsweise diesen Java-Code:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

Und Ihre einzigartige Ausnahme:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Diese Strategie habe ich auf diesem Link gefunden und Sie können die Java-Implementierung hier finden , wo Sie weitere Details dazu sehen können, da der obige Code vereinfacht ist.

Da Sie die verschiedenen Ausnahmen zwischen "Client" - und "einem anderen Server" -Anwendungen trennen müssen, können mehrere Fehlercodeklassen die Schnittstelle ErrorCode implementieren.


2
Beachten Sie, dass die Frage mit "C ++" gekennzeichnet ist.
Sjoerd

Ich habe bearbeitet, um die Idee anzupassen.
Dherik

0

Ausnahmen sind uneingeschränkte Gotos und müssen mit Vorsicht verwendet werden. Die beste Strategie für sie ist es, sie einzuschränken. Die aufrufende Funktion muss alle Ausnahmen behandeln, die von den aufgerufenen Funktionen ausgelöst werden, oder das Programm stirbt ab. Nur die aufrufende Funktion hat den richtigen Kontext für die Behandlung der Ausnahme. Wenn Funktionen weiter oben im Aufrufbaum behandelt werden, handelt es sich um uneingeschränkte Aufgaben.

Ausnahmen sind keine Fehler. Sie treten auf, wenn die Umstände das Programm daran hindern, einen Zweig des Codes zu vervollständigen, und einen anderen Zweig angeben, dem er folgen soll.

Ausnahmen müssen im Kontext der aufgerufenen Funktion stehen. Zum Beispiel: eine Funktion, die quadratische Gleichungen löst . Es müssen zwei Ausnahmen behandelt werden: Division_by_zero und square_root_of_negative_number. Diese Ausnahmen sind jedoch für Funktionen, die diesen quadratischen Gleichungslöser aufrufen, nicht sinnvoll. Sie treten aufgrund der Methode auf, die zum Lösen von Gleichungen verwendet wird, und durch einfaches erneutes Werfen werden die Interna freigelegt und die Kapselung unterbrochen. Stattdessen sollte Division_by_zero als not_quadratic und square_root_of_negative_number und no_real_roots neu geworfen werden.

Es ist keine Hierarchie von Ausnahmen erforderlich. Eine Aufzählung der Ausnahmen (die sie identifizieren), die von einer Funktion ausgelöst werden, ist ausreichend, da die aufrufende Funktion sie verarbeiten muss. Das Zulassen, dass sie den Anrufbaum verarbeiten, ist ein nicht kontextbezogener (uneingeschränkter) Schritt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.