Warum sollte eine Methode nicht mehrere Typen geprüfter Ausnahmen auslösen?

47

Wir verwenden SonarQube, um unseren Java-Code zu analysieren. Dabei gilt folgende Regel (kritisch):

Öffentliche Methoden sollten höchstens eine aktivierte Ausnahme auslösen

Durch die Verwendung von aktivierten Ausnahmen werden Methodenaufrufer gezwungen, Fehler zu behandeln, indem sie entweder weitergegeben oder behandelt werden. Dies macht diese Ausnahmen vollständig Teil der API der Methode.

Um die Komplexität für die Aufrufer angemessen zu halten, sollten Methoden nicht mehr als eine Art von aktivierter Ausnahme auslösen. "

Ein weiteres Stück in Sonar hat Folgendes :

Öffentliche Methoden sollten höchstens eine aktivierte Ausnahme auslösen

Durch die Verwendung von aktivierten Ausnahmen werden Methodenaufrufer gezwungen, Fehler zu behandeln, indem sie entweder weitergegeben oder behandelt werden. Dies macht diese Ausnahmen vollständig Teil der API der Methode.

Um die Komplexität für Aufrufer angemessen zu halten, sollten Methoden nicht mehr als eine Art von aktivierter Ausnahme auslösen.

Der folgende Code:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

sollte überarbeitet werden in:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

Überschreibende Methoden werden von dieser Regel nicht geprüft und dürfen mehrere geprüfte Ausnahmen auslösen.

Ich bin in meinen Lesungen zur Ausnahmebehandlung nie auf diese Regel / Empfehlung gestoßen und habe versucht, einige Standards, Diskussionen usw. zum Thema zu finden. Das einzige, was ich gefunden habe, ist das von CodeRach: Wie viele Ausnahmen sollte eine Methode höchstens auslösen ?

Ist das ein akzeptierter Standard?

sdoca
quelle
7
Was Sie denken? Die Erklärung, die Sie aus SonarQube zitiert haben, erscheint vernünftig. Haben Sie Grund, daran zu zweifeln?
Robert Harvey
3
Ich habe viel Code geschrieben, der mehr als eine Ausnahme auslöst, und viele Bibliotheken verwendet, die auch mehr als eine Ausnahme auslösen. Außerdem wird in Büchern / Artikeln zur Ausnahmebehandlung das Thema der Begrenzung der Anzahl der geworfenen Ausnahmen normalerweise nicht angesprochen. Viele der Beispiele zeigen jedoch das Werfen / Fangen von Mehrfachen, wodurch die Praxis implizit genehmigt wird. Daher fand ich die Regel überraschend und wollte mehr über die Best Practices / die Philosophie der Ausnahmebehandlung im Vergleich zu den grundlegenden Beispielen für die Vorgehensweise herausfinden.
SDOCA

Antworten:

32

Betrachten wir die Situation, in der Sie den bereitgestellten Code haben:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

Hier besteht die Gefahr, dass der Code, den Sie aufrufen delete(), wie folgt aussieht:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

Das ist auch schlimm. Und es wird mit einer anderen Regel abgefangen, die das Abfangen der Basis-Exception-Klasse kennzeichnet.

Der Schlüssel ist, keinen Code zu schreiben, der Sie dazu bringt, schlechten Code anderswo zu schreiben.

Die Regel, auf die Sie stoßen, ist ziemlich verbreitet. Checkstyle hat es in seinen Designregeln:

ThrowsCount

Beschränkt die Anzahl der Auslöseanweisungen auf einen bestimmten Wert (standardmäßig 1).

Begründung: Ausnahmen sind Teil der Schnittstelle einer Methode. Das Deklarieren einer Methode zum Auslösen zu vieler unterschiedlich verwurzelter Ausnahmen macht die Ausnahmebehandlung lästig und führt zu schlechten Programmierpraktiken wie dem Schreiben von Code wie catch (Ausnahme ex). Diese Prüfung zwingt Entwickler dazu, Ausnahmen in eine Hierarchie zu setzen, sodass im einfachsten Fall nur ein Ausnahmetyp von einem Aufrufer geprüft werden muss, aber bei Bedarf auch alle Unterklassen speziell abgefangen werden können.

Dies beschreibt genau das Problem und was das Problem ist und warum Sie es nicht tun sollten. Es ist ein anerkannter Standard, den viele statische Analysewerkzeuge identifizieren und kennzeichnen.

Und während Sie können es nach Sprache Design tun, und es kann vorkommen, dass es das Richtige ist, zu tun , ist es etwas , das Sie sehen sollen und sofort gehen „um, warum mache ich das?“ Es mag für internen Code akzeptabel sein, bei dem jeder diszipliniert genug ist, um nie catch (Exception e) {}etwas zu tun , aber in den meisten Fällen habe ich Leute gesehen, die vor allem in internen Situationen Abstriche gemacht haben.

Lassen Sie die Teilnehmer Ihrer Klasse keinen schlechten Code schreiben.


Ich möchte darauf hinweisen, dass dies mit Java SE 7 und höher weniger wichtig ist, da mit einer einzelnen catch-Anweisung mehrere Ausnahmen abgefangen werden können (Abfangen mehrerer Ausnahmetypen und erneutes Auslösen von Ausnahmen mit verbesserter Typprüfung von Oracle).

Mit Java 6 und früher hatten Sie Code, der so aussah:

public void delete() throws IOException, SQLException {
  /* ... */
}

und

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

oder

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

Keine dieser Optionen mit Java 6 ist ideal. Der erste Ansatz verletzt DRY . Mehrere Blöcke tun immer wieder dasselbe - einmal für jede Ausnahme. Sie möchten die Ausnahme protokollieren und erneut auslösen? Okay. Dieselben Codezeilen für jede Ausnahme.

Die zweite Option ist aus mehreren Gründen schlechter. Erstens bedeutet dies, dass Sie alle Ausnahmen abfangen. Der Nullzeiger wird dort abgefangen (und sollte es auch nicht). Darüber hinaus werfen Sie einen erneut aus, Exceptionwas bedeutet, dass die Methodensignatur deleteSomething() throws Exceptioneine weitere Verwirrung verursacht, da Benutzer Ihres Codes nun gezwungen sind, diesen zu verwenden catch(Exception e).

In Java 7 ist dies nicht so wichtig, da Sie stattdessen Folgendes tun können:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Darüber hinaus ist die Art zu überprüfen , ob man nicht die Typen der Ausnahmen fängt geworfen werden:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

Die Typprüfung erkennt, dass es sich emöglicherweise nur um Typen IOExceptionoder handelt SQLException. Ich bin immer noch nicht sonderlich begeistert von der Verwendung dieses Stils, aber es verursacht nicht so schlechten Code wie unter Java 6 (wo es Sie zwingen würde, die Methodensignatur als die Superklasse zu haben, die die Ausnahmen erweitern).

Trotz all dieser Änderungen erzwingen viele statische Analysetools (Sonar, PMD, Checkstyle) immer noch Java 6-Stilrichtlinien. Es ist keine schlechte Sache. Ich bin mit einer Warnung einverstanden, dass diese noch durchgesetzt werden sollen, aber Sie können die Priorität für sie in Dur oder Moll ändern, je nachdem, wie Ihr Team sie priorisiert.

Wenn Ausnahmen aktiviert oder deaktiviert werden sollen ... das ist eine Frage der g r e eine t Debatte , dass man leicht unzählige Blog - Beiträge Aufnahme jede Seite des Arguments finden. Wenn Sie jedoch mit aktivierten Ausnahmen arbeiten, sollten Sie wahrscheinlich vermeiden, mehrere Typen zu werfen, zumindest unter Java 6.

other_paul
quelle
Akzeptiere dies als die Antwort, da es meine Frage beantwortet, ob es ein gut akzeptierter Standard ist. Ich lese jedoch immer noch die verschiedenen Pro / Contra-Diskussionen, beginnend mit dem Link Panzercrisis, der in seiner Antwort angegeben ist, um festzustellen, wie mein Standard aussehen wird.
SDOCA
"Die Typprüfung erkennt, dass e möglicherweise nur vom Typ IOException oder SQLException ist.": Was bedeutet das? Was passiert, wenn eine Ausnahme eines anderen Typs ausgelöst wird foo.delete()? Wird es noch gefangen und erneut geworfen?
Giorgio
@Giorgio Es ist ein Fehler beim Kompilieren, wenn deletein diesem Beispiel eine andere aktivierte Ausnahme als IOException oder SQLException ausgelöst wird. Der entscheidende Punkt, den ich machen wollte, ist, dass eine Methode, die rethrowException aufruft, immer noch den Typ der Exception in Java 7 erhält. In Java 6 wird all dies auf den allgemeinen ExceptionTyp zusammengeführt, was statische Analysen und andere Codierer traurig macht.
Aha. Es scheint mir ein bisschen verworren zu sein. Ich würde es intuitiver finden, das zu verbieten catch (Exception e)und es entweder catch (IOException e)oder catch (SQLException e)stattdessen zu erzwingen .
Giorgio
@Giorgio Es ist ein schrittweiser Fortschritt von Java 6, um das Schreiben von gutem Code zu vereinfachen. Leider wird die Option, schlechten Code zu schreiben, für lange Zeit bei uns bleiben. Denken Sie daran, dass Java 7 dies catch(IOException|SQLException ex)stattdessen tun kann . Wenn Sie jedoch nur die Ausnahme erneut auslösen möchten, ist es keine schlechte Sache, der Typprüfung zu erlauben, den tatsächlichen Typ der Ausnahme zu verbreiten, während der Code vereinfacht wird.
22

Der Grund, warum Sie im Idealfall nur einen Ausnahmetyp auslösen möchten, liegt darin, dass dies ansonsten wahrscheinlich gegen die Prinzipien der Einzelverantwortung und der Abhängigkeitsumkehr verstößt . Lassen Sie uns anhand eines Beispiels demonstrieren.

Angenommen, wir haben eine Methode, die Daten aus der Persistenz abruft, und diese Persistenz besteht aus einer Reihe von Dateien. Da es sich um Dateien handelt, können wir Folgendes haben FileNotFoundException:

public String getData(int id) throws FileNotFoundException

Jetzt haben wir eine Änderung der Anforderungen und unsere Daten stammen aus einer Datenbank. Anstelle von a FileNotFoundException(da es sich nicht um Dateien handelt) werfen wir jetzt ein SQLException:

public String getData(int id) throws SQLException

Wir müssten jetzt den gesamten Code durchgehen, der unsere Methode verwendet, und die Ausnahme ändern, auf die wir prüfen müssen, sonst wird der Code nicht kompiliert. Wenn unsere Methode weit und breit aufgerufen wird, kann das viel bewirken, dass sich andere ändern. Es braucht viel Zeit und die Leute werden nicht glücklich sein.

Die Abhängigkeitsinversion besagt, dass wir keine dieser Ausnahmen auslösen sollten, da sie interne Implementierungsdetails enthüllen, an deren Verkapselung wir arbeiten. Das Aufrufen von Code muss wissen, welche Art von Persistenz wir verwenden, wenn es wirklich nur darum gehen sollte, ob der Datensatz abgerufen werden kann. Stattdessen sollten wir eine Ausnahme auslösen, die den Fehler auf derselben Abstraktionsebene übermittelt, die wir über unsere API verfügbar machen:

public String getData(int id) throws InvalidRecordException

Wenn wir nun die interne Implementierung ändern, können wir diese Ausnahme einfach in eine einbinden InvalidRecordExceptionund weitergeben (oder sie nicht einbinden und einfach eine neue auslösen InvalidRecordException). Externer Code weiß oder kümmert sich nicht darum, welche Art von Persistenz verwendet wird. Es ist alles eingekapselt.


In Bezug auf die Einzelverantwortung müssen wir uns mit Code befassen, der mehrere, nicht zusammenhängende Ausnahmen auslöst. Angenommen, wir haben die folgende Methode:

public Record parseFile(String filename) throws IOException, ParseException

Was können wir über diese Methode sagen? Wir können nur an der Signatur erkennen, dass sie eine Datei öffnet und analysiert. Wenn wir eine Konjunktion wie "und" oder "oder" in der Beschreibung einer Methode sehen, wissen wir, dass sie mehr als eine Sache tut; es hat mehr als eine verantwortung . Methoden mit mehr als einer Verantwortung sind schwer zu verwalten, da sie sich ändern können, wenn sich eine der Verantwortlichkeiten ändert. Stattdessen sollten wir die Methoden aufteilen, damit sie eine einzige Verantwortung haben:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

Wir haben die Verantwortung für das Lesen der Datei aus der Verantwortung für das Parsen der Daten herausgenommen. Ein Nebeneffekt davon ist, dass wir jetzt beliebige Zeichenfolgendaten an die Analysedaten aus beliebigen Quellen übergeben können: In-Memory, Datei, Netzwerk usw. Wir können parsejetzt auch einfacher testen, da wir keine Datei auf der Festplatte benötigen Tests durchführen gegen.


Manchmal gibt es tatsächlich zwei (oder mehr) Ausnahmen, die wir von einer Methode auslösen können, aber wenn wir uns an SRP und DIP halten, werden die Zeiten, in denen wir auf diese Situation stoßen, seltener.

cbojar
quelle
Ich bin vollkommen damit einverstanden, Ausnahmen auf niedrigerer Ebene gemäß Ihrem Beispiel zu verpacken. Wir machen das regelmäßig und werfen Abweichungen von MyAppExceptions. Eines der Beispiele, bei denen ich mehrere Ausnahmen auslöse, ist der Versuch, einen Datensatz in einer Datenbank zu aktualisieren. Diese Methode löst eine RecordNotFoundException aus. Der Datensatz kann jedoch nur aktualisiert werden, wenn er sich in einem bestimmten Status befindet, sodass die Methode auch eine InvalidRecordStateException auslöst. Ich halte das für gültig und versorge den Anrufer mit wertvollen Infos.
SDOCA
@sdoca Wenn Ihre updateMethode so atomar ist, wie Sie es machen können, und die Ausnahmen auf der richtigen Abstraktionsebene sind, dann klingt es so, als müssten Sie zwei verschiedene Arten von Ausnahmen auslösen, da es zwei Ausnahmefälle gibt. Das sollte das Maß dafür sein, wie viele Ausnahmen geworfen werden können, und nicht diese (manchmal willkürlichen) Interferenzregeln.
1.
2
Wenn ich jedoch eine Methode habe, mit der Daten aus einem Stream gelesen und analysiert werden, kann ich diese beiden Funktionen nicht aufteilen, ohne den gesamten Stream in einen Puffer zu lesen, was unhandlich sein kann. Außerdem kann der Code, der entscheidet, wie die Ausnahmen richtig behandelt werden, von dem Code, der das Lesen und Parsen ausführt, getrennt sein. Wenn ich den Lese- und Parsing-Code schreibe, weiß ich nicht, wie der Code, der meinen Code aufruft, mit beiden Ausnahmetypen umgehen soll, daher muss ich beide durchlassen.
User3294068
+1: Diese Antwort gefällt mir sehr gut, vor allem, weil sie das Problem vom Standpunkt der Modellierung aus angeht. Oft ist es nicht notwendig, noch eine andere Redewendung (wie catch (IOException | SQLException ex)) zu verwenden, da das eigentliche Problem im Programmmodell / -design liegt.
Giorgio
3

Ich erinnere mich, dass ich ein bisschen damit herumgespielt habe, als ich vor einiger Zeit mit Java gespielt habe, aber ich war mir der Unterscheidung zwischen aktiviert und deaktiviert nicht wirklich bewusst, bis ich Ihre Frage gelesen habe. Ich habe diesen Artikel ziemlich schnell bei Google gefunden und er geht auf einige der offensichtlichen Kontroversen ein:

http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html

Abgesehen davon ist eines der Probleme, die dieser Typ mit geprüften Ausnahmen angesprochen hat, dass (und ich persönlich bin von Anfang an auf dieses Problem mit Java gestoßen), wenn Sie den throwsKlauseln in Ihren Methodendeklarationen nicht nur eine Reihe von geprüften Ausnahmen hinzufügen Müssen Sie viel mehr Boilerplate-Code eingeben, um dies zu unterstützen, wenn Sie zu Methoden auf höherer Ebene wechseln? Es führt jedoch auch zu einem größeren Durcheinander und bricht die Kompatibilität, wenn Sie versuchen, den Methoden auf niedrigerer Ebene mehr Ausnahmetypen hinzuzufügen. Wenn Sie einer untergeordneten Methode einen aktivierten Ausnahmetyp hinzufügen, müssen Sie Ihren Code erneut durchgehen und mehrere andere Methodendeklarationen anpassen.

Ein in dem Artikel erwähnter Milderungspunkt - und der Autor mochte das persönlich nicht - war, eine Basisklassenausnahme zu erstellen, Ihre throwsKlauseln darauf zu beschränken , sie nur zu verwenden und dann nur Unterklassen davon intern zu erhöhen. Auf diese Weise können Sie neue geprüfte Ausnahmetypen erstellen, ohne den gesamten Code erneut durchlaufen zu müssen.

Dem Autor dieses Artikels hat es vielleicht nicht so gut gefallen, aber es ist meiner persönlichen Erfahrung nach absolut sinnvoll (vor allem, wenn Sie nachsehen können, was alle Unterklassen sind), und ich wette, aus diesem Grund erhalten Sie den folgenden Rat begrenzen Sie alles auf jeweils einen aktivierten Ausnahmetyp. Was mehr ist, dass der von Ihnen erwähnte Rat mehrere geprüfte Ausnahmetypen in nicht öffentlichen Methoden erlaubt, was absolut sinnvoll ist, wenn dies ihr Motiv ist (oder auch auf andere Weise). Wenn es ohnehin nur eine private Methode oder ähnliches ist, haben Sie nicht die Hälfte Ihrer Codebasis durchlaufen, wenn Sie eine Kleinigkeit ändern.

Sie haben größtenteils gefragt, ob dies ein akzeptierter Standard ist, aber zwischen Ihrer Recherche, die Sie erwähnt haben, diesem vernünftig durchdachten Artikel und der Erfahrung mit persönlicher Programmierung scheint dies in keiner Weise aufzufallen.

Panzerkrise
quelle
2
Warum nicht einfach Würfe deklarieren Throwableund damit fertig werden, anstatt eine eigene Hierarchie zu erfinden?
Deduplizierer
@ Deduplicator Das ist auch eine Art Grund, warum der Autor die Idee nicht mochte. Er dachte nur, Sie könnten genauso gut alle ungeprüften verwenden, wenn Sie das tun werden. Aber wenn jemand, der die API verwendet (möglicherweise Ihr Kollege), eine Liste aller Unterklassen-Ausnahmen hat, die von Ihrer Basisklasse stammen, kann ich ein bisschen davon profitieren, wenn er zumindest weiß, dass alle erwarteten Ausnahmen vorliegen innerhalb einer bestimmten Menge von Unterklassen; dann, wenn sie das Gefühl haben, dass einer von ihnen "handhabbarer" ist als die anderen, wären sie nicht so geneigt, auf einen für ihn spezifischen Handler zu verzichten.
Panzercrisis
Der Grund, warum geprüfte Ausnahmen im Allgemeinen schlechtes Karma sind, ist einfach: Sie sind viral und infizieren jeden Benutzer. Das Angeben einer absichtlich möglichst breiten Spezifikation ist nur eine Möglichkeit zu sagen, dass alle Ausnahmen deaktiviert sind, und somit das Durcheinander zu vermeiden. Ja, es ist eine gute Idee, zu dokumentieren, was Sie möglicherweise tun möchten. Für die Dokumentation gilt : Nur zu wissen, welche Ausnahmen von einer Funktion ausgehen können, ist von streng begrenztem Wert (abgesehen von keiner / vielleicht einer, aber Java lässt dies sowieso nicht zu). .
Deduplizierer
1
@Deduplicator Ich unterstütze die Idee nicht und auch nicht unbedingt. Ich spreche nur über die Richtung, aus der der Rat des OP stammen könnte.
Panzercrisis
1
Danke für den Link. Es war ein großartiger Ausgangspunkt, um über dieses Thema zu lesen.
SDOCA
-1

Das Auslösen mehrerer aktivierter Ausnahmen ist sinnvoll, wenn mehrere sinnvolle Dinge zu tun sind.

Nehmen wir zum Beispiel an, Sie haben eine Methode

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

Dies verstößt gegen eine Ausnahmeregel, ist aber sinnvoll.

Leider passieren in der Regel Methoden wie

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

Hier besteht für den Anrufer nur eine geringe Chance, basierend auf dem Ausnahmetyp etwas Bestimmtes zu tun. Also, wenn wir ihn dazu bringen wollen, zu erkennen, dass diese Methoden KÖNNEN und manchmal WOLLEN, dann ist es genug und besser, nur SomethingMightGoWrongException auszulösen.

Daher gilt höchstens eine geprüfte Ausnahme.

Wenn Ihr Projekt jedoch ein Design verwendet, bei dem mehrere wichtige überprüfte Ausnahmen vorliegen, sollte diese Regel nicht gelten.

Nebenbemerkung: Irgendetwas kann eigentlich fast überall schief gehen, man kann also überlegen, mit? erweitert RuntimeException, aber es gibt einen Unterschied zwischen "wir alle machen Fehler" und "dies spricht externes System und es wird manchmal nicht funktionieren, damit umgehen".

user470365
quelle
1
" Mehrere vernünftige Dinge zu tun" entsprechen kaum srp - dieser Punkt wurde in der vorherigen Antwort
gnat
Es tut. Sie können ein Problem in einer Funktion (Behandlung eines Aspekts) und ein anderes Problem in einer anderen (auch Behandlung eines Aspekts) ermitteln, das von einer Funktion aufgerufen wird, die eine Sache behandelt, nämlich das Aufrufen dieser beiden Funktionen. Und der Anrufer kann in einer Schicht von try catch (in einer Funktion) ein Problem behandeln und ein anderes Problem nach oben weiterleiten und dieses Problem auch alleine behandeln.
user470365