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;
try {
File f = new File("server.properties");
is = new FileInputStream( f );
}
catch ( Exception e ) { is = null; }
try {
if ( is == null ) {
is = getClass().getResourceAsStream("server.properties");
}
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 | 24977230 | 23448992 | 23110799 | 19617614 | 27651401 | 26446651 | 23843059 | 20959111 | 26432438 | 22878966 |
Limpia | 24960075 | 22494701 | 29746724 | 25430114 | 22073185 | 23659750 | 25870744 | 26399108 | 26269713 | 28054291 |
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