Rename, para entender mejor

por Fran Iglesias

El código debería expresar su intención. ¿Qué hacer cuando no es así?

Código que no dice lo que hace

Considera el siguiente ejemplo:

public static class C {
    String u;
    String p;
    String s;
    String e;

    public C(String u, String p, String s, String e) {
        this.u = u;
        this.p = p;
        this.s = s;
        this.e = e;
    }

    public String cx() {
        return String.format("%s:%s@%s/%s", u, p, s, e);
    }
}

¿Qué hace esta clase? Casi nada en ella nos da una pista de cuál es su intención. Puede que la plantilla de formato que tenemos nos resulte familiar y nos indique por donde van los tiros, pero no deja de ser una apuesta.

Para entender el código siempre nos hará falta contexto del dominio en el que opera. Podemos obtener este contexto de muchas formas. Hay un contexto implícito porque trabajamos en un proyecto determinado, dentro de un equipo de desarrollo en una empresa de cierto sector. Quizá estamos mirando este código desde la perspectiva de estar realizando una tarea específica. Esta información nos proporciona, muchas veces, suficiente contexto para entender la responsabilidad de este código.

Pero, ¿si no es suficiente?

Podemos recurrir al contexto de uso:

public class NoAbbreviations {

    public static void main(String[] args) {
        C c = new C("admin", "secret", "localhost", "prod");
        System.out.println("Connection: " + c.cx());
    }

    public static class C {
        String u;
        String p;
        String s;
        String e;

        public C(String u, String p, String s, String e) {
            this.u = u;
            this.p = p;
            this.s = s;
            this.e = e;
        }

        public String cx() {
            return String.format("%s:%s@%s/%s", u, p, s, e);
        }
    }
}

Bien, sigue siendo críptico, pero ahora tenemos señales que nos dicen que C es posiblemente una clase encargada de gestionar una configuración de conexión a algún servicio. Todavía no sabemos cuál, algo que quizá podríamos averiguar adquiriendo más contexto.

Por lo mismo, parece claro que u indica un nombre de usuario, p una contraseña, s es el nombre de un servidor o servicio y e un entorno. El método cx nos da la cadena de conexión.

El problema de los nombres crípticos

Todo este proceso para averiguar qué significa cada símbolo requiere tiempo de investigación, lo que aumenta el coste de introducir cambios y encarece el desarrollo.

Además, una vez que averiguamos lo que significa cada cosa y sus relaciones, tenemos que mantener ese conocimiento en la cabeza, con el riesgo de perderlo. Esto añade más coste.

Y, además, ¿cómo garantizamos que ese conocimiento se difunde en el equipo actual y futuro?

Rename para poner conocimiento en el código

La mejor forma de abordar este problema es trasladar el conocimiento al código. Por ejemplo, así:

public static class C {
    /* u: the username */
    String u;
    /* p: password */
    String p;
    /* s: server name */
    String s;
    /* e: environment prod | staging | local | test */
    String e;

    public C(String u, String p, String s, String e) {
        this.u = u;
        this.p = p;
        this.s = s;
        this.e = e;
    }

    /** 
     * Returns the connection string populated with the configured values
     * */
    public String cx() {
        return String.format("%s:%s@%s/%s", u, p, s, e);
    }
}

Añadir comentarios es una opción legítima, pero los comentarios tienen algunos riesgos. Nos obligan a prestarles atención y mantenerlos. Si no lo hacemos así, corremos el riesgo de que acaben estando desactualizados, contribuyendo a más confusión.

Lo mejor es que el propio código se autodocumente, indicando su intención:

public static class ConnectionConfig {
    String username;
    String password;
    String server;
    String environment;

    public ConnectionConfig(String username,
                            String password,
                            String server,
                            String environment) {
        this.username = username;
        this.password = password;
        this.server = server;
        this.environment = environment;
    }

    public String connectionString() {
        return String.format("%s:%s@%s/%s", username, password, server, environment);
    }
}

Al reflejar nuestro recién adquirido conocimiento en el propio código, evitamos la carga cognitiva de asociar identificadores arbitrarios con sus distintos significados. Podemos olvidarnos de esta clase una vez dejemos de trabajar en ella, porque cuando volvamos su intención sigue estando documentada y no tenemos que buscar más lejos para entenderla.

Y, lo más importante, si es otra persona la que tiene que trabajar con ella más adelante, le habremos ahorrado el esfuerzo de investigarlo. O el tiempo de otra persona que tenga que explicárselo.

Seguridad de rename

Rename es, en los buenos entornos de desarrollo, un refactor automático, con un riesgo bastante bajo y que podemos aplicar con seguridad. Al menos dentro del scope de una clase, método o función y, posiblemente, paquete.

Si el ámbito es más general, dependiendo del lenguaje, podría haber algo de incertidumbre. Si la clase o función renombrada tiene muchos usos, son cambios que afectan a muchos archivos.

Lo mejor, como siempre, es separar la aplicación del refactor de otros cambios, de modo que sea el único.

Para ello, hacemos commit de los cambios que podamos tener pendientes y empezamos con un grupo de cambios vacío. Aplicamos el rename y analizamos el resultado para ver si es lo que esperábamos. En caso de problemas no tenemos más que revertir o arreglarlo manualmente.

Una vez confirmado que el código ha cambiado de la forma deseada, hacemos un nuevo commit.

En resumen

Poner buenos nombres en el código, o aplicar el refactor rename cuando nos encontramos con nombres malos, es una pequeña inversión de tiempo con un gran retorno futuro. El código con buenos nombres tiene un menor coste de mantenimiento.

Y no solo eso, poner buenos nombres es un acto de empatía hacia nuestro equipo hoy y en el futuro.


Esta entrada forma parte de la serie refactoring (1 partes):

  • 1 - Rename, para entender mejor (Este artículo)