miércoles, 26 de febrero de 2014

La maldición de la Perla Negra: dos por el precio de una.

Yo tiendo a decir de mí mismo que soy un "desarrollador científico". Realmente no existe tal cosa, al menos de momento. De algún modo, intento aplicar el método científico, inaugurado por el gran Galileo (que por muy pocos días no nació exactamente 400 años antes que yo), y otras características del trabajo científico, tan bien visto y del que tan poco se sabe en este país, al desarrollo de software. De esta manera, cuando tengo que tocar código que no es mío, siempre dedico un rato a echar un vistazo a lo que hay alrededor, buscando cosas que se puedan mejorar. Es entonces cuando me encuentro cosas que son inmejorables porque, al verlas, sufro alguna especie de colapso cerebral que me impide pensar en alguna mejora hasta que se me pasa el susto. Luego, con más calma, las vuelvo a mirar y me vuelve a dar otro susto.

Soy consciente de que hay veces que un desarrollador inexperto tiene que hacer algo, y lo hace como sabe y puede. Esto se me ha dicho muchas veces, y lo sé. Pero hay cosas que van más allá de la experiencia, en este caso, con Java. Yo creo que, realmente, reflejan algún tipo de condición psicológica que puede ser que esté bastante extendida y que merecería un estudio científico.

Veamos dos ejemplos. Y son dos porque, aunque son cosas hechas por gente que no tiene nada que ver entre sí en el espacio-tiempo, al verlas da la sensación de que, al menos, son del mismo pueblo.

En una grande e importante consultora internacional alguien se enfrentó a una compleja tarea relacionada con el sexo (¿Eh?). Se trataba de recibir un código, con posibles valores 1 y 2, y devolver respectivamente "H" y "M" (¡Ah!).

En otro momento y lugar (muy conocida compañía, por cierto), otro se enfrentó a la terrible experiencia de crear un método que recibiese un valor numérico, del 1 al 12, y devolver el nombre del mes que le corresponde.

Empecemos por el sexo (claro). Sin pensarlo mucho, se me ocurren dos soluciones que me valdrían.
public String getSexo(int codSex)  {
    if(codSex==1)
        return "H";
    if(codSex==2)
        return "M";
    throw new IllegalArgumentException("El valor "+codSex+" no es valido para codSex");
}
Dejaremos de lado consideraciones acerca de que el método podría ser static y, probablemente, final. Esta solución hace lo que debe, incluyendo el chequeo del argumento, porque puedo deducir que hay una precondición que me dice que el código solo puede ser 1 o 2. Por supuesto, podría sustituir los if por un switch:
public String getSexo(int codSex)  {
    switch(codSex) {
        case 1:
            return "H";
        case 2:
            return "M";
        default:
            throw new IllegalArgumentException("El valor "+codSex+" no es valido para codSex");
    }
}
La otra solución es apoyarme en un array:
private static final String[] sexValues={"H", "M"};

public String getSexo(int codSex)  {
    return sexValues[codSex-1];
}
Se podría decir que me falta el chequeo de la precondición, pero el método me lanzará un ArrayIndexOutOfBoundsException si le paso algo raro. Si eso me vale, pues ya está, aunque no tendrá la misma información que en los ejemplos de antes. No pasa nada, le añado el chequeo con el típico if/throw.

Este procedimiento también me vale para el segundo caso:
private static final String[] monthNames{
    "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
    "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
};

public String getMonthName(int monthNumber)  {
    return monthNames[monthNumber-1];
}
Podría rizar el rizo y usar DateFormat para sacar el nombre del mes, capitalizando (toma barbarismo) la salida, pero en términos de complejidad y proceso sería un exceso innecesario. Por cierto, en esta solución también tendría que decrementar el número de mes. Es sorprendente la cantidad de expertos en Java que no saben que los Calendar cuentan los meses de 0 a 11. Al menos, hasta Java 8.

Parece sencillo, ¿no? Pues hay quien no lo ve así de claro. Veamos la solución que a alguien se le ocurrió al tema del sexo.
/**
 * Method getSexo.
 * @param codSex
 * @return String
 */
public final static String getSexo(String codSex)  {

    StringBuffer nomreturn = new StringBuffer();

    if (codSex != null) {

        if (codSex.equals("1"))
        {
            nomreturn.append("H");
        }
        else if (codSex.equals("2"))
        {
            nomreturn.append("M");
        }
    }
    if (nomreturn.length() == 0) {
        nomreturn.append("");
    }

    return nomreturn.toString();
}
Visto esto, se podría suponer que el autor tiene problemas con el sexo, pero yo creo que la cosa va más allá. No sé cómo hacer comentarios sobre este método. No sé bajo los efectos de qué decidió hacer esto con un StringBuffer. No sé qué tipo de deterioro sináptico le hace decidir que el código venga como String. Ahorrarse el chequeo de null invirtiendo los equals (o sea, como "1".equals(codSex)) podría tomarse como algo más "avanzado", pero el comprobar si el buffer está vacío para añadirle, en ese caso, una cadena vacía, escapa a mi comprensión. Cadena vacía que, por otro lado, es lo que devuelve el método si no le paso el valor adecuado (Supongo que eso englobaría a los hermafroditas y eunucos, o algo así). El inteligente javadoc tampoco me da muchas pistas. Es deprimente.

Para lo de los meses, al desarrollador de turno se le vino lo que sigue. Lo del "vino" va con segundas. Lo advierto: da un poco de miedo. Las personas sensibles deberían dejar de leer.
public class NombreMes implements Serializable {

    public static final NombreMes ENERO =
        new NombreMes(new Long(1), "Enero");
    public static final NombreMes FEBRERO =
        new NombreMes(new Long(2), "Febrero");
    public static final NombreMes MARZO =
        new NombreMes(new Long(3), "Marzo");
    public static final NombreMes ABRIL =
        new NombreMes(new Long(4), "Abril");
    public static final NombreMes MAYO =
        new NombreMes(new Long(5), "Mayo");
    public static final NombreMes JUNIO =
        new NombreMes(new Long(6), "Junio");
    public static final NombreMes JULIO =
        new NombreMes(new Long(7), "Julio");
    public static final NombreMes AGOSTO =
        new NombreMes(new Long(8), "Agosto");
    public static final NombreMes SEPTIEMBRE =
        new NombreMes(new Long(9), "Septiembre");
    public static final NombreMes OCTUBRE =
        new NombreMes(new Long(10), "Octubre");
    public static final NombreMes NOVIEMBRE =
        new NombreMes(new Long(11), "Noviembre");
    public static final NombreMes DICIEMBRE =
        new NombreMes(new Long(12), "Diciembre");


    public static final NombreMes[] MESES = { ENERO, FEBRERO, MARZO, ABRIL, MAYO, JUNIO, JULIO, AGOSTO, SEPTIEMBRE, OCTUBRE, NOVIEMBRE, DICIEMBRE};

    public static NombreMes find(Long posicion) {
        for(int i=0; i<MESES.length; i++) {
            if(posicion.equals(MESES[i].getPosicion())) {
               return MESES[i];
            }
        }
        return null;
    }

    private Long posicion;
    private String descripcion;

    private NombreMes() {
    }


    public NombreMes(Long posicion, String descripcion) {
        this.posicion = posicion;
        this.descripcion = descripcion;
    }

    public Long getPosicion() {
        return posicion;
    }

    public String getDescripcion() {
        return descripcion;
    }

}
Es lo que hay. A ver cómo analizamos esto. Voy a intentar ponerme en la piel de este tan hábil desarrollador. Lo mismo hasta encuentro justificaciones para esto.

Antes, voy a dar una información que no se ve en la muestra de código. Todos los usos que se hacían de esto tenían la forma NombreMes.find(new Long(algo)).getDescripcion(), donde "algo" solía tener la forma otroAlgo.intValue(), porque ese "otroAlgo" solía ser un Integer.

Vamos allá. Para empezar, he decidido resolver esto con una enumeración. Usando un enum resulta una solución perfectamente válida, aunque alguien podría opinar que innecesariamente compleja, utilizando algo como values()[posicion-1].getDescripcion() para obtener el nombre de un mes. Pero estoy desarrollando con una versión de Java que aún tiene enum. O a lo mejor sí, pero no lo sé. Por eso implemento un patrón Typesafe Enum (aproximadamente). Bueno, lo mismo me he enterado de que esto se parece al patrón ese más adelante, no sé. Tengo entendido que el autor de este blog tiene pensado tratar este tema más adelante. Ahora sí, me dicen, la solución resulta innecesariamente compleja. Bueno, pero mola mucho y parece como que sé un montón.

Por supuesto, hago la clase Serializable. Realmente no me hace falta para nada, pero así parece como más importante. Se dicen cosas por ahí acerca de este tipo de enumeraciones cuando son serializables, pero no importa porque nadie va a serializar esto, la verdad.

Bueno, cada elemento de la enumeración contiene el nombre del mes en un String. En esto no he recibido crítica ninguna, en poner el nombre en un String. El número lo coloco en un Long. ¿Por qué un Long y no un long? Pues porque si el tipo lleva mayúsculas es más chulo. ¿Por qué un long y no un int? Bueno, tal como están las cosas de inseguras, vete a saber si no acabaremos teniendo años con más de 2.147.483.647 meses. Nota del autor (del post, no del código): alguien podría afinar más y decir que, en realidad vale con un byte, y que eso sería más ajustado que un int. Es cierto, pero como internamente la unidad mínima que va a manejar Java es un int, tampoco hay diferencia, y nos ahorramos conversiones implícitas.

En la misma línea que el dilema long/int, he decidido poner un constructor público, que me permite ir añadiendo más meses según el año vaya creciendo. Podríamos hacer, por ejemplo, new NombreMes(13L, "Onciembre") o new NombreMes(14L, "Dociembre"). Bueno, éste último puede ser confuso. Propongo oficialmente un NombreMes(14L, "Duodeciembre"). En cuanto al constructor privado, evidentemente está de adorno, porque me pagan por líneas, o lo mismo no tengo claro cómo funcionan los constructores en Java.

Por cierto. Ya puestos a usar el Long, y tal como acabo de escribir, lo suyo sería usar el autobox/autounbox. Pero no sé qué es eso, o sigo usando una versión de Java que, como con el enum, no tiene esta facilidad (me quedé en Java 1.4). Como Long es inmutable (todo Number lo es) podría usar Long.valueOf(), ahorrándome instancias inútiles, porque tira de caché. Pero como hay que justificar las ampliaciones de memoria de los equipos, lo dejo así. Además, vete a saber si algún día 11==11 es false. Hay que ser precavido.

Poder obtener con getPosicion() el número del mes que acabo de obtener con el número del mes (¿lo he escrito bien?... Sí) puede ser útil por si al procesador le surge algo por medio, le llaman por teléfono, o a una reunión, o algo así, y se le olvida.

Toda esta manera de hacer las cosas me obliga, claro, a buscar cada vez recorriendo la lista donde he puesto todos los meses después de crearlos individualmente, con un bucle for (el foreach tampoco lo tengo). Ya veremos qué hago con el índice cuando el año tenga más de esos dos mil y pico millones de meses. Podría hacer creado la lista directamente sin las instancias públicas de los meses, que son públicas precisamente por... algo, pero se trata de un tema de autodocumentación: si pongo en la lista ENERO, sé que me refiero al mes de enero, a no ser que me haya equivocado, pero eso no puede ser; en cambio, si pusiera new NombreMes(new Long(1), "Enero") pues... estooo... !Caramba, qué coincidencia!

En lugar de una lista podría haber usado un HashMap, pero eso sería Java avanzado. Además, eso me privaría de demostrar mi habilidad usando el equals de Long, que también puede ser la razón de usar Long en lugar de long, que lo resolvería con un operador ==, más simplón y feo.

Por último, si el valor que paso a find es null, cosa que puede ocurrir al usar Long en lugar de long, esto se la va a pegar, pero no digo nada, ni hago la comprobación nada más empezar. A todo el mundo le gustan las sorpresas. Eso sí: si el número de mes que me pasan no está entre 1 y 12 incluidos, es porque me piden un mes que no existe, así que devuelvo null. Hay que fijarse bien en esto: no es que me hayan pasado un número de mes mal. No, no. El número de mes 35, por ejemplo, es bueno. No es que haya algo mal en un sitio donde conste un número de mes 35, si no que, simplemente, es un mes que no existe. Así que devuelvo null en lugar de lanzar algo como IllegalArgumentException, que lo mismo le resulta ofensivo al que use esto, si no le resulta ofensivo el simple hecho de usarlo.

Bueno. Estar en esta piel ha sido anímicamente duro. Lo dejaré aquí. Pero, aviso, ya tengo más perlas a la vista.

No hay comentarios:

Publicar un comentario