Ausnahmen: Warum früh werfen? Warum sich verspäten?

156

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?

shylynx
quelle
104
Die Idee hinter "Fang spät" ist, anstatt so spät wie möglich zu fangen, so früh wie möglich zu fangen, was nützlich ist. Wenn Sie zum Beispiel einen Datei-Parser haben, macht es keinen Sinn, eine nicht gefundene Datei zu behandeln. Was soll man damit machen, wie sieht der Genesungspfad aus? Es gibt keinen, also fang nicht. Gehen Sie zu Ihrem Lexer, was machen Sie dort, wie können Sie sich davon erholen, damit Ihr Programm fortgesetzt werden kann? Die Ausnahme kann nicht passieren. Wie kann Ihr Scanner damit umgehen? Es kann nicht, lass es passieren. Wie kann der aufrufende Code damit umgehen? Es kann entweder einen anderen Dateipfad versuchen oder den Benutzer warnen, also abfangen.
Phoshi
16
Es gibt sehr wenige Fälle, in denen eine NullPointerException (ich nehme an, das ist, was NPE bedeutet) jemals abgefangen werden sollte. wenn möglich, sollte es an erster Stelle vermieden werden. Wenn Sie NullPointerExceptions haben, dann haben Sie einen fehlerhaften Code, der behoben werden muss. Es ist höchstwahrscheinlich auch eine einfache Lösung.
Phil
6
Bevor Sie vorschlagen, diese Frage als Duplikat zu schließen, überprüfen Sie bitte, ob die Antwort auf die andere Frage diese Frage nicht sehr gut beantwortet.
Doc Brown
1
(Citebot) today.java.net/article/2003/11/20/… Wenn dies nicht der Ursprung des Zitats ist, geben Sie bitte die Quelle an, die Sie für das wahrscheinlichste Originalzitat halten.
Rwong
1
Nur eine Erinnerung für alle, die sich mit dieser Frage befassen und Android-Entwicklung betreiben. Unter Android müssen Ausnahmen lokal abgefangen und behandelt werden - in derselben Funktion, in der sie zuerst abgefangen wurden. Dies liegt daran, dass sich Ausnahmen nicht über Nachrichtenhandler ausbreiten. In diesem Fall wird Ihre Anwendung beendet. Daher sollten Sie diesen Rat bei der Android-Entwicklung nicht zitieren.
Rwong

Antworten:

118

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.

Michael Shaw
quelle
Ja, ich weiß bereits und es steht außer Frage, dass ich an der Stelle, an der der Fehler auftritt, Ausnahmen auslösen sollte. Aber warum sollte ich die NPE nicht fangen und stattdessen auf den Stacktrace klettern lassen? Ich würde immer die NPE fangen und sie in eine bedeutungsvolle Ausnahme einwickeln. Ich kann auch keinen Vorteil erkennen, warum ich eine DAO-Exception auf die Service- oder UI-Ebene werfen sollte. Ich habe es immer auf der Service-Ebene abgefangen und es in eine Service-Ausnahme mit zusätzlichen detaillierten Informationen eingeschlossen, warum der Aufruf des Service fehlgeschlagen ist.
Shylynx
8
@shylynx Eine Ausnahme abfangen und dann eine aussagekräftigere Ausnahme erneut auslösen ist eine gute Sache. Was Sie nicht tun sollten, ist, eine Ausnahme zu früh abzufangen und sie dann nicht erneut zu werfen. Der Fehler, vor dem das Sprichwort warnt, besteht darin, dass die Ausnahme zu früh abgefangen wird und dann versucht wird, sie auf der falschen Codeebene zu behandeln.
Simon B
Wenn der Kontext zum Zeitpunkt der Ausnahmebedingung offensichtlich wird, wird das Leben der Entwickler in Ihrem Team erleichtert. Eine NPE erfordert weitere Untersuchungen, um das Problem zu verstehen
Michael Shaw
4
@shylynx Man könnte die Frage stellen: "Warum haben Sie einen Punkt in Ihrem Code, der einen auslösen kannNullPointerException ? Warum nicht früher nach nulleiner Ausnahme (vielleicht einer IllegalArgumentException) suchen und sie auslösen , damit der Anrufer genau weiß, wo die schlechte Nachricht eingegangen ist null?" Ich glaube, das würde der Teil des Sprichworts "Früh werfen" andeuten.
jpmc26
2
@jpmc Ich habe die NPE nur als Beispiel genommen, um die Bedenken in Bezug auf die Ebenen und die Ausnahmen hervorzuheben. Ich könnte es auch durch eine IllegalArgumentException ersetzen.
Shylynx
56

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.

Doval
quelle
1
+1 für eine klare Erklärung, warum die verschiedenen Ebenen wichtig sind. Exzellentes Beispiel für den Dateisystemfehler.
Juan Carlos Coto
24

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:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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:

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Alternativ können Sie den OOP-Stil wie folgt steuern:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Wie Sie sehen, hat der Anrufercode die Last der Vorabprüfung, führt jedoch keine Ausnahmebehandlung mehr durch. Kommt ArithmeticExceptionjemals ein Aufruf von divideoder eval, dann bist DU es, der die Ausnahmebehandlung durchführen und deinen Code korrigieren muss, weil du den vergessen hast check(). Aus den gleichen Gründen NullPointerExceptionist 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:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

Inzwischen woanders die konkreten Umsetzungen ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

Und schließlich der Verkabelungscode ... Wer behandelt konkrete Ressourcenausnahmen? Derjenige, der über sie Bescheid weiß!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

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!

Daniel Dinnyes
quelle
Sie haben Recht, dass der Code, der die Schnittstelle bereitstellt, mehr über mögliche Fehler weiß als der Code, der die Schnittstelle verwendet. Angenommen, eine Methode verwendet zwei Ressourcen desselben Schnittstellentyps und Fehler müssen unterschiedlich behandelt werden. Oder verwendet eine dieser Ressourcen intern als Implementierungsdetail, das dem Ersteller nicht bekannt ist, andere verschachtelte Ressourcen desselben Typs? Die Business-Schicht werfen WrappedFirstResourceExceptionoder WrappedSecondResourceExceptionund die "Verkabelung" Schicht, um in dieser Ausnahme zu suchen, um die Ursache des Problems zu sehen ...
Supercat
... mag icky sein, aber es scheint besser zu sein, als anzunehmen, dass eine FailingInputResourceAusnahme das Ergebnis einer Operation mit ist in1. 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äftsschicht catcheinzufügen, das dann die handleExceptionMethode 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.
Supercat
@supercat Ich verstehe, was du sagst. Ich würde sagen, dass eine konkrete Ressourcenimplementierung dafür verantwortlich ist, die Ausnahmen zu kennen, die sie auslösen kann. Es muss nicht alles spezifizieren (es gibt so genanntes undefiniertes Verhalten ), aber es muss sicherstellen, dass es keine Mehrdeutigkeiten gibt. Auch ungeprüfte Laufzeitausnahmen sind zu dokumentieren. Wenn es der Dokumentation widerspricht, ist es ein Fehler. Wenn zu erwarten ist, dass der Aufrufercode bei einer Ausnahme irgendetwas Sinnvolles tut, besteht das absolute Minimum darin, dass die Ressource sie in einige einschließt UnrecoverableInternalException, ähnlich einem HTTP 500-Fehlercode.
Daniel Dinnyes
@supercat Über Ihren Vorschlag zu konfigurierbaren Fehlerbehandlern: genau! In meinem letzten Beispiel ist die Fehlerbehandlungslogik fest codiert und ruft die statische doMyBusinessMethode auf. Dies war der Kürze halber und es ist durchaus möglich, es dynamischer zu gestalten. Eine solche HandlerKlasse würde mit einigen Eingabe- / Ausgaberessourcen instanziiert werden und eine handleMethode haben, die eine Klasse empfängt, die eine implementiert ReusableBusinessLogicInterface. Sie können dann kombinieren / konfigurieren, um verschiedene Handler-, Ressourcen- und Geschäftslogikimplementierungen in der Verdrahtungsschicht irgendwo darüber zu verwenden.
Daniel Dinnyes
10

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:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Dieser Code kann offensichtlich eine Nullreferenzausnahme auslösen, wenn PropertyBnull ist. Es gibt zwei Dinge, die wir in diesem Fall tun könnten, um diese Situation zu "korrigieren". Wir könnten:

  • Erstellt automatisch PropertyB, wenn wir es nicht haben. oder
  • Lassen Sie die Ausnahme zur aufrufenden Methode aufsteigen.

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.

Stephen
quelle
Ich denke, Sie verwenden Upstream-Entwickler im falschen Sinne. Sie sagten auch, es verstoße gegen das Prinzip der Einzelverantwortung, aber in Wirklichkeit werden viele On-Demand-Initialisierungen und das Zwischenspeichern von Werten auf diese Weise implementiert (natürlich mit angemessenen Kontrollen der Parallelität)
Daniel Dinnyes,
In Ihrem gegebenen Beispiel was für null vor der Subtraktion Überprüfung, wie dieseif(PropertyB == null) return 0;
user1451111
1
Können Sie auch auf Ihren letzten Absatz näher eingehen, insbesondere auf die Frage, was Sie unter " Abstraktionsebene " verstehen ?
user1451111
Wenn wir IO-Arbeiten ausführen, ist die Abstraktionsebene, auf der eine IO-Ausnahmebedingung abgefangen wird, die Ebene, auf der wir die Arbeit ausführen. Zu diesem Zeitpunkt haben wir alle Informationen, die wir benötigen, um zu entscheiden, ob wir es erneut versuchen oder ein Meldungsfeld an den Benutzer senden oder ein Objekt mit einer Reihe von Standardeinstellungen erstellen möchten.
Stephen
"Wie sieht es in Ihrem Beispiel mit der Überprüfung auf Null vor der Subtraktionsoperation aus, wenn (PropertyB == Null) 0 zurückgibt?" Yuck. Das würde der aufrufenden Methode sagen, dass ich gültige Dinge zu und von subtrahieren muss. Natürlich ist dies kontextbezogen, aber es wäre in den meisten Fällen eine schlechte Praxis, hier meine Fehler zu überprüfen.
Stephen
6

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 .

Ratschenfreak
quelle
1
Du hast geschrieben: Wirf bald, ... Fang bald ...! Warum? Das ist ein völlig entgegengesetzter Ansatz im Gegensatz zu "Früh werfen, spät fangen".
Shylynx
1
@shylynx Ich weiß nicht, woher "Früh werfen, spät fangen" kommt, aber sein Wert ist fraglich. Was genau bedeutet "spät" fangen? Wo es sinnvoll ist, eine Ausnahme abzufangen (wenn überhaupt), hängt vom Problem ab. Das einzige, was klar ist, ist, dass Sie Probleme so früh wie möglich erkennen (und beseitigen) möchten.
Doval
2
Ich gehe davon aus, dass "Spät abfangen" dazu gedacht ist, der Praxis des Abfangens entgegenzuwirken, bevor Sie wissen, wie Sie den Fehler beheben können - z.
@Hurkyl: Ein Problem mit "catch late" ist, dass es für Code, der möglicherweise in der Lage ist, etwas gegen die Situation zu unternehmen, schwierig sein kann, zu wissen, dass die Dinge wirklich so sind, wie sie sind, wenn eine Ausnahme durch Ebenen auftaucht, die nichts davon wissen erwartet. Nehmen wir als einfaches Beispiel an, dass ein Parser für eine Benutzerdokumentdatei einen CODEC von der Festplatte laden muss und ein Festplattenfehler auftritt, während dieser gelesen wird. Code, der den Parser aufruft, verhält sich möglicherweise unangemessen, wenn er denkt, dass beim Lesen des Benutzers ein Festplattenfehler aufgetreten ist dokumentieren.
Supercat
4

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.

Soru
quelle
2

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/ catchCode 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.

Aschratt
quelle
1

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.

davidk01
quelle