GOTO Return

Jetzt versteht jeder, dass die Verwendung des GOTO-Operators nicht nur schlecht, sondern auch schrecklich ist. Die Debatte über seine Verwendung endete in den 80er Jahren des 20. Jahrhunderts und wurde von den meisten modernen Programmiersprachen ausgeschlossen. Aber wie es sich für ein echtes Übel gehört, hat er es geschafft, sich zu verkleiden und im 21. Jahrhundert unter dem Deckmantel von Ausnahmen wiederzubeleben.


Ausnahmen sind zum einen ein recht einfaches Konzept in modernen Programmiersprachen. Andererseits werden sie häufig falsch verwendet. Es gibt eine einfache und bekannte Regel - Ausnahmen gelten nur für den Umgang mit Schäden. Und eine zu lockere Auslegung des Begriffs „Zusammenbruch“ führt zu allen Problemen bei der Verwendung von GOTO.


Theoretisches Beispiel


Der Unterschied zwischen Pannen und negativen Geschäftsszenarien wird im Anmeldefenster anhand eines sehr einfachen Anwendungsfalls deutlich:


  1. Benutzer gibt Login / Passwort ein.
  2. Der Benutzer klickt auf die Schaltfläche "Anmelden".
  3. Die Clientanwendung sendet eine Anforderung an den Server.
  4. Der Server überprüft erfolgreich den Benutzernamen / das Passwort (das Vorhandensein des entsprechenden Paares wird als Erfolg gewertet).
  5. Der Server sendet Informationen an den Client, dass die Authentifizierung erfolgreich war, und einen Link zur Übergangsseite.
  6. Der Client geht zur angegebenen Seite.

Und eine negative Erweiterung:


4.1. Der Server hat das entsprechende Login / Passwort-Paar nicht gefunden und sendet eine entsprechende Benachrichtigung an den Client.


Zu berücksichtigen, dass Szenario 4.1 ein „Problem“ ist und daher mit einer Ausnahme implementiert werden muss, ist ein ziemlich häufiger Fehler. Dies ist eigentlich nicht der Fall. Nicht übereinstimmende Anmeldedaten und Kennwörter gehören zu unserer Standardbenutzererfahrung, die sich aus der Geschäftslogik des Skripts ergibt. Unsere Geschäftskunden erwarten diese Entwicklung. Daher handelt es sich nicht um eine Aufschlüsselung, und Sie können hier keine Ausnahmen verwenden.


Pannen sind: Unterbrechung der Verbindung zwischen Client und Norden, Unzugänglichkeit des DBMS, fehlerhaftes Schema in der Datenbank. Und noch eine Million Gründe, die unsere Anwendungen beschädigen und nichts mit der Geschäftslogik des Benutzers zu tun haben.


In einem der Projekte, an deren Entwicklung ich beteiligt war, gab es eine komplexere Anmeldelogik. Durch dreimaliges Eingeben des falschen Passworts wurde der Benutzer vorübergehend für 15 Minuten gesperrt. Wenn der Benutzer drei Mal hintereinander eine temporäre Sperre erhält, erhält er eine permanente Sperre. Es gab auch zusätzliche Regeln je nach Benutzertyp. Die Implementierung von Ausnahmen hat die Einführung neuer Regeln extrem erschwert.


Es wäre interessant, dieses Beispiel zu betrachten, aber es ist zu groß und nicht sehr visuell. Wie klar und prägnant es wird, Code mit Geschäftslogik in Bezug auf Ausnahmen zu verwechseln, zeige ich Ihnen an einem anderen Beispiel.


Beispiel Laden von Eigenschaften


Versuchen Sie, sich diesen Code anzusehen und klar zu verstehen, was er bewirkt. Die Prozedur ist mit einer ziemlich einfachen Logik nicht groß. Bei einem guten Programmierstil sollte das Verständnis der Essenz nicht länger als 2-3 Minuten dauern (ich kann mich nicht erinnern, wie lange ich gebraucht habe, um diesen Code vollständig zu verstehen, aber definitiv länger als 15 Minuten).


private WorkspaceProperties(){ Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true); //These mappings will replace any mappings that this hashtable had for any of the //keys currently in the specified map. getProperties().putAll( loadedProperties ); //     loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH, false); if (loadedProperties != null){ getProperties().putAll( loadedProperties ); } System.out.println("Loaded properties:" + getProperties()); } /** *  ,    . * @param filepath * @param throwIfNotFound -  FileNotFoundException,     * @return    null,      !throwIfNotFound * @throws FileNotFoundException throwIfNotFound        * @throws IOException     */ private Properties readPropertiesFromFile(String filepath, boolean throwIfNotExists){ Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); InputStream is = null; InputStreamReader isr = null; try{ int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0){ try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } System.out.println("Found file " + filepath); } catch( FileNotFoundException e) { System.out.println("File not found " + filepath); if (throwIfNotExists) throw new RuntimeException("Can`t load workspace properties." + filepath + " not found", e ); }catch (IOException e){ throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Lassen Sie uns also das Geheimnis enthüllen - was hier passiert. Die Eigenschaften aus zwei Dateien werden WORK_PROPERTIES - die erforderlichen WORK_PROPERTIES und die zusätzlichen MY_WORK_PROPERTIES , die zum allgemeinen Eigenschaftenspeicher hinzugefügt werden. Es gibt eine Nuance: Wir wissen nicht genau, wo sich die spezifische Eigenschaftendatei befindet. Sie kann sowohl im aktuellen Verzeichnis als auch in den Ahnenverzeichnissen liegen (bis zu drei Ebenen höher).


Mindestens zwei Dinge sind hier verwirrend: der throwIfNotExists Parameter und der große catch FileNotFoundException in catch FileNotFoundException . All diese undurchsichtigen Hinweise - Ausnahmen werden zur Implementierung der Geschäftslogik verwendet (aber wie lässt sich sonst erklären, dass das Auslösen einer Ausnahme in einem Szenario ein Fehler ist und in dem anderen nicht?).


Den richtigen Vertrag schließen


Zuerst beschäftigen throwIfNotExists mit throwIfNotExists . Wenn Sie mit Ausnahmen arbeiten, ist es sehr wichtig zu wissen, wo genau diese in Bezug auf Anwendungsfälle verarbeitet werden müssen. In diesem Fall kann die Methode readPropertiesFromFile offensichtlich nicht selbst entscheiden, wann das Fehlen einer Datei „schlecht“ und wann sie „gut“ ist. Eine solche Entscheidung wird zum Zeitpunkt ihres Aufrufs getroffen. Die Kommentare zeigen, dass wir entscheiden, ob diese Datei existieren soll oder nicht. Tatsächlich interessiert uns aber nicht die Datei selbst, sondern die Einstellungen daraus. Dies folgt leider nicht aus dem Code.


Wir beheben diese beiden Mängel:


 Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll( loadedProperties ); 

Jetzt wird die Semantik klar dargestellt -
WORK_PROPERTIES muss angegeben werden, MY_WORK_PROPERTIES nicht angegeben werden. Außerdem habe ich beim Refactoring festgestellt, dass readPropertiesFromFile niemals null und dies beim Lesen von MY_WORK_PROPERTIES .


Wir prüfen ohne zu brechen


Vorheriges Refactoring wirkte sich ebenfalls auf die Implementierung aus, jedoch nicht wesentlich. Ich habe gerade den throwIfNotExists Verarbeitungsblock gelöscht:


 if (throwIfNotExists) throw new RuntimeException(…); 

Nachdem wir die Implementierung genauer untersucht haben, beginnen wir, die Logik des Autors des Codes für die Suche nach einer Datei zu verstehen. Zuerst wird überprüft, ob sich die Datei im aktuellen Verzeichnis befindet. Wenn sie nicht gefunden wird, überprüfen wir sie auf einer höheren Ebene usw. Das heißt Es wird deutlich, dass der Algorithmus das Fehlen einer Datei vorsieht. In diesem Fall erfolgt die Prüfung mit einer Ausnahme. Das heißt Das Prinzip wird verletzt - die Ausnahme wird nicht als „etwas ist kaputt gegangen“, sondern als Teil der Geschäftslogik wahrgenommen.


Es gibt eine Funktion zum Überprüfen der Verfügbarkeit einer Datei zum Lesen von File.canRead() . Mit ihr können Sie die Geschäftslogik in einem catch loswerden catch


  try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } 

Wenn wir den Code ändern, erhalten wir Folgendes:


 private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } } System.out.println("Found file " + filepath); } catch (FileNotFoundException e) { System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Ich habe auch die Ebene der Variablen ( is , isr ) auf das zulässige Minimum reduziert.


Ein solches einfaches Refactoring verbessert die Lesbarkeit des Codes erheblich. Der Code zeigt den Algorithmus direkt an (wenn die Datei vorhanden ist, lesen wir sie, andernfalls reduzieren wir die Anzahl der Versuche und sehen im obigen Verzeichnis nach).


GOTO enthüllen


Überlegen Sie im Detail, was in einer Situation passiert, in der die Datei nicht gefunden wurde:


 } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } 

Es ist ersichtlich, dass hier die Ausnahme verwendet wird, um den Ausführungszyklus zu unterbrechen und die GOTO-Funktion tatsächlich auszuführen.


Für Zweifler werden wir eine weitere Änderung vornehmen. Anstatt eine kleine Krücke im Format loadingTryLeft = 0 (Krücke, da ein erfolgreicher Versuch loadingTryLeft = 0 Anzahl der verbleibenden Versuche nicht loadingTryLeft = 0 ), weisen wir ausdrücklich darauf hin, dass das Lesen der Datei zum Verlassen der Funktion führt (ohne zu vergessen, eine Nachricht zu schreiben):


 try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { 

Dies ermöglicht es uns, die while (loadingTryLeft > 0) Bedingung while (loadingTryLeft > 0) durch while(true) zu ersetzen:


 try { int loadingTryLeft = 3; String relativePath = ""; while (true) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); // GOTO: FFN } } } } catch (FileNotFoundException e) { // LABEL: FFN System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } 

Um den offensichtlichen üblen throw new FileNotFoundException , müssen Sie sich an den Funktionsvertrag erinnern. In jedem Fall gibt die Funktion eine Reihe von Eigenschaften zurück. Konnten sie die Datei nicht lesen, geben wir sie leer zurück. Daher gibt es keinen Grund, eine Ausnahme auszulösen und abzufangen. Die übliche while (loadingTryLeft > 0) Bedingung while (loadingTryLeft > 0) reicht aus:


 private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Im Prinzip ist vom Standpunkt der korrekten Arbeit mit Ausnahmen alles hier. Es besteht ein Zweifel daran, dass bei IOException-Problemen eine RuntimeException ausgelöst werden muss, diese wird jedoch aus Gründen der Kompatibilität beibehalten.



Es gibt noch ein paar Kleinigkeiten, mit denen wir den Code noch flexibler und verständlicher machen können:


  • Der readPropertiesFromFile-Methodenname macht seine Implementierung verfügbar (übrigens und löst FileNotFoundException aus). Besser, es neutraler und prägnanter zu nennen - loadProperties (...)
  • Die Methode sucht und liest gleichzeitig. Für mich sind dies zwei unterschiedliche Verantwortlichkeiten, die sich in verschiedene Methoden einteilen lassen.
  • Der Code wurde ursprünglich unter Java 6 geschrieben, wird jetzt jedoch in Java 7 verwendet. Dies ermöglicht die Verwendung von schließbaren Ressourcen.
  • Ich weiß aus Erfahrung, dass es beim Anzeigen von Informationen zu einer gefundenen oder nicht gefundenen Datei besser ist, den vollständigen Pfad zur Datei zu verwenden, als den relativen.
  • if (loadingTryLeft > 0) relativePath += "../"; - Wenn Sie sich den Code genau ansehen, können Sie feststellen, dass diese Prüfung nicht erforderlich ist, da Wenn das Suchlimit erschöpft ist, wird der neue Wert sowieso nicht verwendet. Und wenn der Code etwas Überflüssiges enthält, sollte dieser Müll entfernt werden.

Die endgültige Version des Quellcodes:


 private WorkspaceProperties() { super(new Properties()); if (defaultInstance != null) throw new IllegalStateException(); Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } getProperties().putAll(loadedProperties); loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll(loadedProperties); System.out.println("Loaded properties:" + getProperties()); } private Properties readPropertiesFromFile(String filepath) { System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { return read(file); } else { relativePath += "../"; loadingTryLeft -= 1; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return new Properties(); } private Properties read(File file) throws IOException { try (InputStream is = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(is, "UTF-8")) { Properties loadedProperties = new Properties(); loadedProperties.load(isr); System.out.println("Found file " + file.getAbsolutePath()); return loadedProperties; } } 

Zusammenfassung


Das analysierte Beispiel zeigt deutlich, wozu sorgloser Umgang mit dem Quellcode führt. Anstatt eine Ausnahme zum Behandeln der Aufschlüsselung zu verwenden, wurde beschlossen, sie zum Implementieren der Geschäftslogik zu verwenden. Dies führte sofort zu einer Komplexität der Unterstützung, die sich in der Weiterentwicklung zur Erfüllung neuer Anforderungen und damit in einer Abkehr von den Grundsätzen der strukturellen Programmierung niederschlug. Die Verwendung einer einfachen Regel - Ausnahmen nur für Pannen - hilft Ihnen dabei, nicht in die GOTO-Ära zurückzukehren und Ihren Code sauber, verständlich und erweiterbar zu halten.

Source: https://habr.com/ru/post/de484840/


All Articles