Retour GOTO

Maintenant, tout le monde comprend que l'utilisation de l'opérateur GOTO n'est pas seulement une mauvaise, mais une terrible pratique. Le débat sur son utilisation a pris fin dans les années 80 du XXe siècle et il a été exclu de la plupart des langages de programmation modernes. Mais, comme il sied à un vrai mal, il a réussi à se déguiser et à ressusciter au 21e siècle sous couvert d'exceptions.


Les exceptions, d'une part, sont un concept assez simple dans les langages de programmation modernes. En revanche, ils sont souvent mal utilisés. Il existe une règle simple et bien connue - les exceptions ne concernent que la gestion des dommages. Et c'est tout simplement une interprétation trop vague du concept de «panne» qui entraîne tous les problèmes liés à l'utilisation de GOTO.


Exemple théorique


La différence entre les pannes et les scénarios commerciaux négatifs est clairement visible sur la fenêtre de connexion avec un cas d'utilisation très simple:


  1. L'utilisateur entre l'identifiant / mot de passe.
  2. L'utilisateur clique sur le bouton "Connexion".
  3. L'application cliente envoie une requête au serveur.
  4. Le serveur vérifie avec succès le nom d'utilisateur / mot de passe (considère la présence de la paire correspondante comme un succès).
  5. Le serveur envoie des informations au client indiquant que l'authentification a réussi et un lien vers la page de transition.
  6. Le client accède à la page spécifiée.

Et une extension négative:


4.1. Le serveur n'a pas trouvé la paire login / mot de passe correspondant et envoie une notification au client à ce sujet.


Considérer que le scénario 4.1 est un «problème» et qu'il doit donc être implémenté à l'aide d'une exception est une erreur assez courante. Ce n'est en fait pas le cas. Les incohérences de connexion et de mot de passe font partie de notre expérience utilisateur standard telle que fournie par la logique métier du script. Nos clients commerciaux attendent cette évolution. Par conséquent, ce n'est pas une panne et vous ne pouvez pas utiliser d'exceptions ici.


Les pannes sont: déconnexion de la connexion entre le client et le nord, inaccessibilité du SGBD, schéma incorrect dans la base de données. Et un million de raisons supplémentaires qui cassent nos applications et n'ont rien à voir avec la logique métier de l'utilisateur.


Dans l'un des projets, au développement duquel j'ai participé, il y avait une logique de connexion plus complexe. En entrant le mauvais mot de passe 3 fois de suite, l'utilisateur a été temporairement bloqué pendant 15 minutes. Obtenu 3 fois de suite dans une serrure temporaire, l'utilisateur a reçu une serrure permanente. Il y avait également des règles supplémentaires selon le type d'utilisateur. La mise en œuvre d'exceptions a rendu extrêmement difficile l'introduction de nouvelles règles.


Il serait intéressant de considérer cet exemple, mais il est trop grand et peu visuel. Comment la confusion du code avec la logique métier sur les exceptions devient claire et concise, je vais vous montrer dans un autre exemple.


Exemple de propriétés de chargement


Essayez de regarder ce code et de comprendre clairement ce qu'il fait. La procédure n'est pas grande avec une logique assez simple. Avec un bon style de programmation, la compréhension de son essence ne devrait pas dépasser 2 à 3 minutes (je ne me souviens pas du temps qu'il m'a fallu pour bien comprendre ce code, mais certainement plus de 15 minutes).


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; } 

Alors, révélons le secret - ce qui se passe ici. Les propriétés de deux fichiers sont WORK_PROPERTIES - les WORK_PROPERTIES requises et les MY_WORK_PROPERTIES supplémentaires, s'ajoutant au magasin de propriétés général. Il y a une nuance - nous ne savons pas exactement où se trouve le fichier de propriétés spécifique - il peut se trouver à la fois dans le répertoire actuel et dans les répertoires ancêtres (jusqu'à trois niveaux plus haut).


Au moins deux choses throwIfNotExists confusion ici: le paramètre throwIfNotExists et le grand bloc logique dans catch FileNotFoundException . Tout cela suggère de manière opaque - les exceptions sont utilisées pour implémenter la logique métier (mais comment expliquer autrement que dans un scénario, lever une exception est un échec, et dans l'autre non?).


Faire le bon contrat


Commençons d'abord par throwIfNotExists . Lorsque vous travaillez avec des exceptions, il est très important de comprendre où exactement il doit être traité en termes de cas d'utilisation. Dans ce cas, il est évident que la méthode readPropertiesFromFile ne peut pas décider quand l'absence d'un fichier est "mauvaise" et quand elle est "bonne". Une telle décision est prise au moment de son appel. Les commentaires montrent que nous décidons si ce fichier doit exister ou non. Mais en fait, nous ne nous intéressons pas au fichier lui-même, mais aux paramètres qu'il contient. Malheureusement, cela ne découle pas du code.


Nous corrigeons ces deux lacunes:


 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 ); 

Maintenant, la sémantique est clairement montrée -
WORK_PROPERTIES doit être spécifié, mais MY_WORK_PROPERTIES ne MY_WORK_PROPERTIES pas l'être. De plus, lors de la refactorisation, j'ai remarqué que readPropertiesFromFile ne pouvait jamais retourner null et en ai profité lors de la lecture de MY_WORK_PROPERTIES .


Nous vérifions sans casser


Le refactoring précédent a également affecté la mise en œuvre, mais pas de manière significative. Je viens de supprimer le throwIfNotExists traitement throwIfNotExists :


 if (throwIfNotExists) throw new RuntimeException(…); 

Après avoir examiné l'implémentation de plus près, nous commençons à comprendre la logique de l'auteur du code pour rechercher un fichier. Tout d'abord, il est vérifié que le fichier se trouve dans le répertoire courant, s'il n'est pas trouvé, nous vérifions à un niveau supérieur, etc. C'est-à-dire il devient clair que l'algorithme prévoit l'absence de fichier. Dans ce cas, la vérification est effectuée à l'aide d'une exception. C'est-à-dire le principe est violé - l'exception n'est pas perçue comme «quelque chose a cassé», mais comme faisant partie de la logique métier.


Il existe une fonction pour vérifier la disponibilité d'un fichier pour lire File.canRead() . En l'utilisant, vous pouvez vous débarrasser de la logique métier dans un 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(); } } 

En changeant le code, nous obtenons ce qui suit:


 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; } 

J'ai également réduit le niveau des variables ( is , isr ) au minimum autorisé.


Une telle refactorisation simple améliore considérablement la lisibilité du code. Le code affiche directement l'algorithme (si le fichier existe, alors nous le lisons, sinon nous réduisons le nombre de tentatives et regardons dans le répertoire ci-dessus).


Révéler GOTO


Considérez en détail ce qui se passe dans une situation si le fichier est introuvable:


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

On peut voir qu'ici, l'exception est utilisée pour interrompre le cycle d'exécution et réellement exécuter la fonction GOTO.


Pour les sceptiques, nous ferons un autre changement. Au lieu d'utiliser une petite béquille sous la forme loadingTryLeft = 0 (béquille, car en fait une tentative réussie ne loadingTryLeft = 0 pas loadingTryLeft = 0 nombre de tentatives restantes), nous indiquons explicitement que la lecture du fichier conduit à la sortie de la fonction (sans oublier d'écrire un message):


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

Cela nous permet de remplacer la condition while (loadingTryLeft > 0) par while(true) :


 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); } 

Afin de se débarrasser de l'odeur nauséabonde évidente, throw new FileNotFoundException , vous devez vous rappeler le contrat de fonction. Dans tous les cas, la fonction renvoie un ensemble de propriétés, si elles ne pouvaient pas lire le fichier, nous le renvoyons vide. Par conséquent, il n'y a aucune raison de lever une exception et de l'attraper. La condition while (loadingTryLeft > 0) habituelle while (loadingTryLeft > 0) suffit:


 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; } 

En principe, du point de vue du travail correct avec des exceptions, tout est là. Il y a un doute sur la nécessité de lancer une RuntimeException en cas de problèmes IOException, mais nous la laisserons telle quelle pour des raisons de compatibilité.



Il reste quelques petites choses que nous pouvons rendre le code encore plus flexible et compréhensible:


  • Le nom de la méthode readPropertiesFromFile expose son implémentation (en passant, et lève FileNotFoundException). Mieux vaut l'appeler plus neutre et concis - loadProperties (...)
  • La méthode recherche et lit simultanément. Pour moi, ce sont deux responsabilités différentes qui peuvent être divisées en différentes méthodes.
  • Le code a été écrit à l'origine sous Java 6, mais est maintenant utilisé dans Java 7. Cela permet l'utilisation de ressources pouvant être fermées.
  • Je sais par expérience que lors de l'affichage d'informations sur un fichier trouvé ou introuvable, il est préférable d'utiliser le chemin d'accès complet au fichier plutôt que relatif.
  • if (loadingTryLeft > 0) relativePath += "../"; - si vous regardez attentivement le code, vous pouvez voir - cette vérification est inutile, car si la limite de recherche est épuisée, la nouvelle valeur ne sera pas utilisée de toute façon. Et s'il y a quelque chose de superflu dans le code, ce sont des ordures qui devraient être supprimées.

La version finale du code source:


 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; } } 

Résumé


L'exemple analysé illustre clairement à quoi conduit une manipulation imprudente du code source. Au lieu d'utiliser une exception pour gérer la panne, il a été décidé de l'utiliser pour implémenter la logique métier. Cela a immédiatement conduit à la complexité de son soutien, qui s'est reflété dans son développement ultérieur pour répondre aux nouvelles exigences et, par conséquent, à une dérogation aux principes de la programmation structurelle. L'utilisation d'une règle simple - des exceptions uniquement pour les pannes - vous aidera à éviter de retourner dans l'ère GOTO et à garder votre code propre, compréhensible et extensible.

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


All Articles