GOTO Volver

Ahora todos entienden que usar el operador GOTO no es solo una mala práctica, sino una práctica terrible. El debate sobre su uso terminó en los años 80 del siglo XX y fue excluido de la mayoría de los lenguajes de programación modernos. Pero, como corresponde a un verdadero mal, logró disfrazarse y resucitar en el siglo XXI bajo la apariencia de excepciones.


Las excepciones, por un lado, son un concepto bastante simple en los lenguajes de programación modernos. Por otro lado, a menudo se usan incorrectamente. Existe una regla simple y bien conocida: las excepciones son solo para el manejo de daños. Y es demasiado flojo interpretar el concepto de "desglose" que lleva a todos los problemas del uso de GOTO.


Ejemplo teórico


La diferencia entre los desgloses y los escenarios comerciales negativos es claramente visible en la ventana de inicio de sesión con un caso de uso muy simple:


  1. El usuario ingresa nombre de usuario / contraseña.
  2. El usuario hace clic en el botón "Iniciar sesión".
  3. La aplicación cliente envía una solicitud al servidor.
  4. El servidor verifica con éxito el nombre de usuario / contraseña (considera la presencia del par correspondiente como correcto).
  5. El servidor envía información al cliente de que la autenticación fue exitosa y un enlace a la página de transición.
  6. El cliente va a la página especificada.

Y una extensión negativa:


4.1. El servidor no encontró el par de inicio de sesión / contraseña correspondiente y envía una notificación al cliente sobre esto.


Considerar que el escenario 4.1 es un "problema" y, por lo tanto, debe implementarse utilizando una excepción es un error bastante común. Este no es realmente el caso. Los desajustes de inicio de sesión y contraseña son parte de nuestra experiencia de usuario estándar según lo dispuesto por la lógica empresarial del script. Nuestros clientes comerciales esperan este desarrollo. Por lo tanto, esto no es un desglose y no puede usar excepciones aquí.


Las averías son: desconexión de la conexión entre el cliente y el norte, inaccesibilidad del DBMS, esquema incorrecto en la base de datos. Y un millón de razones más que rompen nuestras aplicaciones y no tienen nada que ver con la lógica empresarial del usuario.


En uno de los proyectos, en el desarrollo del cual participé, había una lógica de inicio de sesión más compleja. Al ingresar la contraseña incorrecta 3 veces seguidas, el usuario fue bloqueado temporalmente durante 15 minutos. Obteniendo 3 veces seguidas en un bloqueo temporal, el usuario recibió un bloqueo permanente. También había reglas adicionales según el tipo de usuario. La implementación de excepciones ha hecho que sea extremadamente difícil introducir nuevas reglas.


Sería interesante considerar este ejemplo, pero es demasiado grande y no muy visual. Cómo el código confuso con la lógica empresarial en las excepciones se vuelve claro y conciso, te mostraré en otro ejemplo.


Ejemplo de propiedades de carga


Intenta mirar este código y entender claramente lo que hace. El procedimiento no es grande con una lógica bastante simple. Con un buen estilo de programación, la comprensión de su esencia no debe exceder más de 2-3 minutos (no recuerdo cuánto tiempo me llevó comprender completamente este código, pero definitivamente más de 15 minutos).


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

Entonces, revelemos el secreto: lo que está sucediendo aquí. Se WORK_PROPERTIES las propiedades de dos archivos: las WORK_PROPERTIES requeridas y las MY_WORK_PROPERTIES adicionales, que se agregan al almacén de propiedades generales. Hay un matiz, no sabemos exactamente dónde se encuentra el archivo de propiedades específico, puede encontrarse tanto en el directorio actual como en los directorios de antepasados ​​(hasta tres niveles hacia arriba).


Al menos dos cosas son confusas aquí: el parámetro throwIfNotExists y el bloque lógico grande en catch FileNotFoundException . Todo esto insinúa opacamente: las excepciones se utilizan para implementar la lógica de negocios (pero ¿de qué otra manera explicar eso en un escenario, lanzar una excepción es un fracaso y en el otro no?).


Hacer el contrato correcto


Primero, throwIfNotExists con throwIfNotExists . Cuando se trabaja con excepciones, es muy importante comprender dónde se debe procesar exactamente en términos de casos de uso. En este caso, es obvio que el método readPropertiesFromFile no puede decidir cuándo la ausencia de un archivo es "mala" y cuándo es "buena". Dicha decisión se toma en el momento de su convocatoria. Los comentarios muestran que decidimos si este archivo debe existir o no. Pero, de hecho, no nos interesa el archivo en sí, sino la configuración del mismo. Desafortunadamente, esto no se sigue del código.


Arreglamos ambas deficiencias:


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

Ahora la semántica se muestra claramente:
WORK_PROPERTIES debe especificarse, pero MY_WORK_PROPERTIES no MY_WORK_PROPERTIES especificarse. También durante la refactorización, noté que readPropertiesFromFile nunca podría devolver null y aproveché esto al leer MY_WORK_PROPERTIES .


Comprobamos sin romper


La refactorización previa también afectó la implementación, pero no significativamente. Acabo de eliminar el throwIfNotExists procesamiento throwIfNotExists :


 if (throwIfNotExists) throw new RuntimeException(…); 

Habiendo examinado la implementación más de cerca, comenzamos a comprender la lógica del autor del código para buscar un archivo. Primero, se verifica que el archivo esté en el directorio actual; si no se encuentra, lo verificamos en un nivel superior, etc. Es decir queda claro que el algoritmo prevé la ausencia de un archivo. En este caso, la verificación se realiza utilizando una excepción. Es decir se viola el principio: la excepción se percibe no como "algo se ha roto", sino como parte de la lógica empresarial.


Hay una función para verificar la disponibilidad de un archivo para leer File.canRead() . Al usarlo, puede deshacerse de la lógica de negocios en 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(); } } 

Al cambiar el código, obtenemos lo siguiente:


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

También reduje el nivel de variables ( is , isr ) al mínimo permitido.


Tal refactorización simple mejora enormemente la legibilidad del código. El código muestra directamente el algoritmo (si el archivo existe, entonces lo leemos, de lo contrario reducimos el número de intentos y buscamos en el directorio anterior).


Revelando GOTO


Considere en detalle lo que sucede en una situación si no se encuentra el archivo:


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

Se puede ver que aquí la excepción se usa para interrumpir el ciclo de ejecución y realmente realizar la función GOTO.


Para los que dudan, haremos otro cambio. En lugar de usar una pequeña muleta en el formulario loadingTryLeft = 0 (muleta, porque de hecho un intento exitoso no loadingTryLeft = 0 número de intentos restantes), indicamos explícitamente que leer el archivo conduce a la salida de la función (sin olvidar escribir un mensaje):


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

Esto nos permite reemplazar la condición while (loadingTryLeft > 0) con 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); } 

Para deshacerse del obvio mal olor que throw new FileNotFoundException , debe recordar el contrato de la función. En cualquier caso, la función devuelve un conjunto de propiedades, si no pueden leer el archivo, lo devolvemos vacío. Por lo tanto, no hay razón para lanzar una excepción y atraparla. La condición while (loadingTryLeft > 0) habitual while (loadingTryLeft > 0) suficiente:


 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 principio, desde el punto de vista del trabajo correcto con excepciones, todo está aquí. Existe una duda sobre la necesidad de lanzar una RuntimeException en caso de problemas de IOException, pero la dejaremos como está por razones de compatibilidad.



Quedan algunas cositas que podemos hacer que el código sea aún más flexible y comprensible:


  • El nombre del método readPropertiesFromFile expone su implementación (por cierto, así como arroja FileNotFoundException). Es mejor llamarlo más neutral y conciso - loadProperties (...)
  • El método busca y lee simultáneamente. Para mí, estas son dos responsabilidades diferentes que se pueden dividir en diferentes métodos.
  • El código se escribió originalmente en Java 6, pero ahora se usa en Java 7. Esto permite el uso de recursos que se pueden cerrar.
  • Sé por experiencia que al mostrar información sobre un archivo encontrado o no encontrado, es mejor usar la ruta completa al archivo, en lugar de relativa.
  • if (loadingTryLeft > 0) relativePath += "../"; - si observa cuidadosamente el código, puede ver - esta verificación es innecesaria, porque Si el límite de búsqueda se agota, el nuevo valor no se utilizará de todos modos. Y si hay algo superfluo en el código, esto es basura que debería eliminarse.

La versión final del código fuente:


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

Resumen


El ejemplo analizado ilustra claramente a qué conduce el manejo descuidado del código fuente. En lugar de usar una excepción para manejar el desglose, se decidió usarlo para implementar la lógica empresarial. Esto condujo inmediatamente a la complejidad de su apoyo, lo que se reflejó en su mayor desarrollo para cumplir con los nuevos requisitos y, como resultado, una desviación de los principios de la programación estructural. El uso de una regla simple (excepciones solo para averías) lo ayudará a evitar volver a la era GOTO y a mantener su código limpio, comprensible y extensible.

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


All Articles