jueves, 3 de octubre de 2013

Leer un archivo de propiedades (Java)

En este vínculo aparece un código para leer un archivo de propiedades en Java: http://www.drdobbs.com/jvm/readwrite-properties-files-in-java/231000005

El código es este:

private void loadParams() {
  Properties props = new Properties();
  InputStream is = null;
  
  // First try loading from the current directory
  try {
    File f = new File("server.properties");
    is = new FileInputStream( f );
  }
  catch ( Exception e ) { is = null; }
  
  try {
    if ( is == null ) {
      // Try loading from classpath
      is = getClass().getResourceAsStream("server.properties");
    }
  
    // Try loading properties from the file (if found)
    props.load( is );
  }
  catch ( Exception e ) { }

  serverAddr = props.getProperty("ServerAddress", "192.168.0.1");
  serverPort = new Integer(props.getProperty("ServerPort", "8080"));
  threadCnt  = new Integer(props.getProperty("ThreadCount", "5"));
}

Para empezar a limpiar el código, primero vamos a quitar la mezcla entre lógica del código y su manejo de errores:

private void loadParams() throws FileNotFoundException,
    IOException {
  Properties props = new Properties();
  InputStream is = null;
  File f = new File("server.properties");
  is = new FileInputStream( f );
  if ( is == null ) {
    is = getClass().getResourceAsStream("server.properties");
  }
  props.load( is );
  serverAddr = props.getProperty("ServerAddress", "192.168.0.1");
  serverPort = new Integer(props.getProperty("ServerPort", "8080"));
  threadCnt  = new Integer(props.getProperty("ThreadCount", "5"));
}

Noten que estoy lanzando tanto FileNotFoundException como IOException, siendo que podría lanzar solo IOException al ser una super clase de FileNotFoundException. Sin embargo en este punto aún no sé cómo voy a manejarlas. Sólo en caso de que esté seguro que las voy a manejar de la misma manera, debería envolverlas en una misma excepción.

Así como quedó el código, nunca intentará leer el archivo desde el classpath pero por ahora concentrémosnos en hacer que los métodos no hagan más de una cosa.

El método loadParams() debería instanciar un objeto de tipo Properties y leer de él unas propiedades:

private void loadParams() throws FileNotFoundException,
    IOException {
  Properties props = getProperties("server.properties");
  serverAddr = props.getProperty("ServerAddress", "192.168.0.1");
  serverPort = new Integer(props.getProperty("ServerPort", "8080"));
  threadCnt = new Integer(props.getProperty("ThreadCount", "5"));
}

Ahora escribamos el método auxiliar para esto:

private Properties getProperties(String path) 
    throws FileNotFoundException, IOException {
  Properties props = new Properties();
  InputStream is = null;
  File f = new File(path);
  is = new FileInputStream(f);
  if (is == null) {
    is = getClass().getResourceAsStream(path);
  }
  props.load(is);
  return props;
}

En este método también hacemos más de una cosa, así que de nuevo separémoslo:

private Properties getProperties(String path) 
    throws FileNotFoundException, IOException {
  Properties props = new Properties();
  InputStream is = getInputStream(path);
  props.load(is);
  return props;
}
private InputStream getInputStream(String path)
    throws FileNotFoundException {
  InputStream is = null;
  File f = new File(path);
  is = new FileInputStream(f);
  if (is == null) {
    is = getClass().getResourceAsStream(path);
  }
  return is;
}

Nuevamente tenemos un método que hace 2 cosas, pero además la manera en que trata de hacer el "fallback" no es funcional. Por fin podremos corregir eso. Pero primero, una operación familiar:

private InputStream getInputStream(String path)
      throws FileNotFoundException {
  InputStream is = getInputStreamFromFileSystem(path);
  if (is == null) {
    is = getInputStreamFromClasspath(path);
  }
  return is;
}
private InputStream getInputStreamFromFileSystem(String path) 
    throws FileNotFoundException {
  File f = new File(path);
  InputStream is = new FileInputStream(f);
  return is;
}
private InputStream getInputStreamFromClasspath(String path) {
  InputStream is = getClass().getResourceAsStream(path);
  return is;
}

Por fin nuestro código esta compuesto de métodos que hacen una sola cosa. Ahora podemos pensar en el manejo de errores. Por ejemplo ya podemos implementar correctamente el "fallback" para cargar el archivo de propiedades:

private InputStream getInputStream(String path)
      throws FileNotFoundException {
  try {
    return getInputStreamFromFileSystem(path);
  } catch (FileNotFoundException e) {
    return getInputStreamFromClasspath(path);
  }
}
private InputStream getInputStreamFromClasspath(String path)
      throws FileNotFoundException {
  InputStream is = getClass().getResourceAsStream(path);
  if (is == null) {
    throw new FileNotFoundException();
  }
  return is;
}

Con nuestro "fallback" implementado, ahora podemos seguir propagando hasta un punto en que podamos manejarlo correctamente

Si lo pensamos, si no podemos cargar el archivo properties, la operación de cargar debe lanzar una excepción. ¿Propagamos FileNotFoundException e IOException? Probablemente no, en ambos casos la operación falló y un mensaje amigable al usuario final debería desplegarse. En este caso realmente podemos envolver las excepciones en una sola.

Como deberíamos estar usando i18n para crear los mensajes para el usuario final, lo más conveniente es crear una nueva excepción que me permita poder llevar el código que voy a usar para extraer el mensaje del ResourceBundle. Con esta idea el código se vería así:

private Properties getProperties(String path) 
    throws ApplicationException {
  try {
    return doGetProperties(path);
  } catch (FileNotFoundException e) {
    throw new ApplicationException("message.file_not_found");
  } catch (IOException e) {
    throw new ApplicationException("message.io", e.getMessage());
  }
}
private void loadParams() throws ApplicationException {
  Properties props = getProperties("server.properties");
  serverAddr = props.getProperty("ServerAddress", "192.168.0.1");
  serverPort = new Integer(props.getProperty("ServerPort", "8080"));
  threadCnt = new Integer(props.getProperty("ThreadCount", "5"));
}

Algunos pensaran que es un sacrilegio pasar de un código con un método a uno ¡con 6!

Pero los 6 métodos traen varias ventajas:

  • El código es más fácil de leer y por lo tanto de entender. Por ende es más fácil corregirle bugs o extender sus funcionalidades
  • Por la misma razón es más difícil que al desarrollador se le esconda un bug
  • El código es más fácil de probar: perfectamente podemos aumentar la visibilidad de private a package y hacer pruebas unitarias a los métodos. Como los métodos son cortos, hacer pruebas con buena cobertura es trivial.

Aún con esas ventajas alguien dirá que los 6 métodos hacen que el código sea más lento de ejecutar, lo que podría significar un problema de desempeño.

Como lo sabrá cualquier persona que ha tenido que mejorar el desempeño de una aplicación, la regla de oro es: medir, medir, medir

Así que hagamos una medición, rudimentaria pero que nos daría un buen primer análisis

Voy a hacer el llamado a la función loadParams() dentro del constructor de una clase:

public Foo() throws ApplicationException {
  loadParams();
}

Y ahora voy a ejecutar 10 veces el siguiente código para cada versión del código

public static void main(String[] args) throws ApplicationException {
  long start = System.nanoTime();
  Foo foo = new Foo();
  long end = System.nanoTime();
  System.out.println(end - start);
}

Estos son los resultados:

Versión #1#2#3#4#5#6#7#8#9#10
Original 24977230234489922311079919617614276514012644665123843059209591112643243822878966
Limpia 24960075224947012974672425430114220731852365975025870744263991082626971328054291

Así que claramente la versión limpia no es más lenta que la otra versión y si trae muchos beneficios.

¿Por qué no escribimos más código así en producción?

Creo que principalmente es una cuestión de tiempo: Escribir código limpio toma tiempo. Escribirlo desde el principio no es fácil. Toma un tiempo de experimentación hasta encontrar una versión que sin ser perfecta nos deje satisfechos.

Así que los programadores aducimos que dada la naturaleza afanada de nuestra industria (en que todo hay que entregarlo para ya) el tiempo solo nos alcanza para hacer código funcional.

La razón por la cuál esto es ridículo es porque en teoría si nos tomáramos más tiempo escribiendo código limpio, gastaríamos menos tiempo corrigiendo bugs, lo que implicaría no tener que comer la fecha de lanzamiento.

Esperemos que poco a poco logremos generar esa conciencia

Le versión limpia completa la pueden encontrar en:

https://gist.github.com/gaijinco/6957285

5 comentarios:

  1. Fenomenal... para alcanzar este nivel de destreza en producción sin sacrificar tiempos, se requiere bastante entrenamiento personal... tiene razón. Es importante dedicar tiempo para autoentrenarse y aumentar las capacidades personales en programación, ya que sería fabuloso si todos codificamos de esta manera. Todo el que haya tenido que hacerle carpintería a un código funcional lo sabe...

    ResponderEliminar
    Respuestas
    1. Es cierto que hay que dedicar tiempo a mejorar la manera en que se programa.

      Pero escribir Clean Code es más una actitud que una habilidad. Hay que decidir preocuparse por el código. Algo que no es muy común!

      Eliminar
  2. Carlos,

    Buenísima la iniciativa. Ojalá todos leyeran el libro porque realmente cambia la forma en que uno escribe código.

    Una sugerencia es siempre preguntarse al principio ¿cuál es la intención del código? En el ejemplo, la intención es inicializar unas variables de instancia. Pero inicializarlas desde un archivo de propiedades en la misma clase incrementa el acoplamiento: la clase se vuelve difícil de cambiar y probar. El nombre del archivo está quemado en la clase, por ejemplo.

    La otra sugerencia es que si estás pensando en cambiar la visibilidad de los métodos para probarlos, generalmente existe una mejor solución.

    Por último, el método getProperties(String path) hace algo inesperado: busca también en el classpath. Si uno no ve el código, no hay forma de saber que eso también lo hace. Es mejor poner el nombre completo (la mejor solución) o documentar el método (la segunda mejor solución).

    Mi propuesta entonces es la siguiente (seguro tiene errores porque la escribí de rapidez ahí mismo pero lo importante es la idea): https://gist.github.com/gaijinco/6957285#comment-928918

    Fíjate que no utilice el ApplicationException, me parece que ahí no es el lugar para envolver esas excepciones.

    ResponderEliminar
    Respuestas
    1. Una de las intenciones del blog es tratar de hacer el mismo código que estaba en alguna parte, pero limpio más allá de las diferencias que yo pueda sentir con el diseño. Así que trate de respetar el espíritu del código. Pero las discusiones sobre problemas del código original también son bienvenidas.

      Eso si, estoy de acuerdo que la lectura de un archivo de propiedades debería estar encapsulado en su propia clase. Eso es definitivamente algo del diseño original que se podría mejorar.

      Posiblemente la clase Foo debería usar métodos estáticos (como propones) y proveer getters para los valores que lee del archivo. Pero como dije, mi intención es sólo limpiar el código "as is"

      Soy consciente que hay temas que son "controversiales" simplemente porque no hay un consenso sobre cuál es la mejor práctica. Si yo tengo un montón de métodos que son private (y de nuevo esto es una extensión del código original) no tengo ningún problema en aumentarles la visibilidad, solo lo suficiente para que las pruebas unitarias los vean.

      Para mi el concepto de visibilidad por paquete es similar al concepto de clases amigas en C++, y me parece que la clase con pruebas unitarias es la definición exacta de lo que una clase amiga debe ser. (posiblemente el concepto podría ser mejor implementado como se hace en C++, pero esa es otra discusión)

      No sería la única manera, muchas veces siento que un método privado queda mejor representado haciendo una clase auxiliar con ese método público y reemplazar el método privado con una referencia privada a esa clase. Así las pruebas unitarias se pueden hacer sobre la clase auxiliar. Supongo que es cuestión de gustos.

      ¿Qué otra opción verías para hacer pruebas unitarias a nuestra clase hipotética, Foo?

      Estoy de acuerdo en que es importante documentar el comportamiento de un método para tener claras las expectativas. Como en este caso el método es private la mejor documentación es la implementación del código. La manera simple en la que está escrita más el nombre descriptivo de los métodos hacen que las expectativas sean claras: primero trata de leerlo como una ruta del "file system" y luego en el classpath.

      El nombre que propones, loadPropertiesFromFileSystemOrClassPath(), en lo personal no me gusta porque si cambio la implementación tengo que cambiarle el nombre y eso podría romper código (en este caso de los métodos privados puede no ser terrible pero en tu código de métodos públicos estáticos podría serlo) Cosa de gustos.

      A mi me gusta que un código tenga diferentes niveles de abstracciones. A alguien que le empiece a leer el código verá que loadParams() obtiene un objeto de tipo Properties (pero no sabe cómo) y saca unos valores de él. Eso podría ser suficiente para él. Si necesita más información, puede ir dentro de método getProperties() y ver cómo lo está haciendo, y así sucesivamente. Si uno bota la complejidad desde el principio puede enredar a alguien que solo quiera hacer un pequeño ajuste al código.

      El caso de ApplicationException yo diría, si la clase es utilitaria como la propones, yo mantendría FileNotFoundException e IOException. En el caso de una clase Foo que pertenece a una aplicación (dado que conoce exactamente unos valores a sacar de un properties files) ya no las lanzaría y las envolvería en una excepción general que use la aplicación, especialmente si voy a usar i18n para mostrar un mensaje amigable al usuario.

      Antes de Java 7, la gente se acostumbraba a atrapar Exception bajo la excusa que se lanzaban "muchas" excepciones que iban a ser manejadas de la misma manera, lo que traía el problema de atrapar incluso excepciones no esperadas y volverlas silenciosas.

      Muchas gracias por los comentarios, ciertamente algo como este blog se vuelve mucho más interesante si se vuelve un espacio para confrontar ideas y que los lectores juzguen los argumentos y tomen sus decisiones.

      Espero seguir contando con tus comentarios.

      Eliminar
  3. ¿Por qué cambiarías la implementación de PropertiesLoader.loadPropertiesFromFileSystemOrClassPath()? Yo cambiaría el código a más alto nivel si fuera necesario, pero ¿cambiar ese método utilitario?

    Entiendo que lo de aumentar la visibilidad para hacer pruebas unitarias es muy discutible, pero en mi experiencia, siempre que hago un refactor para evitar cambiar la visibilidad de un método, la solución queda mucho más elegante y legible.

    Si quieres código para hacerle refactor, puedes mirar mis repositorios en github (https://github.com/germanescobar), seguro que ahí hay bastante código por mejorar ;)

    ResponderEliminar