Nos puede tocar corregir alguna función que tiene 500 líneas, libre de comentarios y además con una sangría mal alineada. No creas que esta función ha sido obra de un genio que domina el caos. Ha sido el producto de muchos retoques de mucha gente y, además, no funciona. Por eso ahora te toca a ti.

Muchas veces me he sentido tonto por no entender una función larga, pero también es cierto que iba con desventaja: él que la hizo lógicamente sabía mejor que hubo ahí y luego, sólo te piden un pequeño retoque. Bueno, es posible que te toca este trabajo porque dentro lo que cabe eres él que menos tonto se presenta ante el problema. Así tranquilo. Si tú no entiendes funciones más largas que 30 líneas, otros las entienden aún mucho menos. Y como ya te toca, no preguntes si puedes modificar algo más de lo que te han pedido. Modifica la función para que por una vez sea comprensible.

¿Cómo hacemos esto? Pues, básicamente usamos dos técnicas a la vez.

  1. Siempre cuando hayamos entendido algo lo apuntamos como una nota en forma de un comentario en el código. Porque así guardamos lo que ya hemos entendido. Podemos ser detallado. Somos nosotros que debemos arreglar la función y aunque muchos no se molestan en poner comentarios, nadie se queja si están. (¿Quién aparte de ti se los leería?)
  2. No te cortes a renombrar variables con mal nombre si son locales en esta función. Compilándolo todo verás si esto funciona. Ten cuidado con esto en lenguajes interpretados ya que muchos permiten usar variables sin declarar. Olvidarse de cambiar el nombre de una variable en un sitio donde se usa puede causar que al final tienes dos variables distintos (y otro error más):
  3. En cuanto hayamos entendido una estructura, la exportamos como una subfunción y sustituimos este trozo por la llamada a esta nueva subfunción.

El tercer punto requiere más explicación: la idea fundamental es separar la función larga en un conjunto de subfunciones de manera que el código de ninguna de ellas ocupe más que una pantalla. (Es lo que yo soy capaz de entender.) Estas subfunciones requerirán parámetros. La meta es minimizar el número de estos parámetros.

Bloques internos – en muchos lenguajes marcados con llaves {} – suelen ser un buen candidato para convertirse en una subfunción. Por ejemplo puedes optar por

if (alguna expresión)
{
    mi_subfunción();
}
else
{
    mi_subfunción_else();
}

También puedes separar una estructura entera: un switch, un bucle o un if que a su vez llaman a más funciones dependientes. Es favorable que metes la estructura entera en una función para quede simétrica. No hagas cosas así

if (not mi_subfuncion_hace_algo_si_el_if_está_true())
{
    ejecuta_algo_con_else();
}

Antes de convertir un bloque en una subfunción debes averiguar cuales variables serán necesario. Idealmente declaras variables donde se necesitan, es decir mueve la declaración dentro del bloque si es posible. Estas serán variables locales de la nueva subfunción y no aparecerán en el interfaz.

Empieza por lo fácil. Reemplaza

if (a > b)
{
    c = a;
}
else
{
    c = b;
}

por

c = max(a, b);

A medida que cambias las cosas fáciles, las estructuras no tan fáciles se hacen más evidente. Crear una subfunción no es sólo escribir un código que hace lo mismo en otra forma. Puede aportar la información semántica que, como en nuestro ejemplo, se quiere usar el máximo de dos valores. Haciéndolo de esta forma es más probable que ves el bosque tras tantos árboles.

Cuando separas código piensa que una función puede seguir distintos pasos, por ejemplo inicialización, proceso, salida. Estos pueden ser funciones distintas que usan una estructura de datos como entrada y salida.

Idealmente tus funciones se parecen al índice de un libro. La función de máxima jierarquía contiene llamadas a los capítulos; estas a su vez son funciones que llaman a subcapítulos hasta el último nivel donde finalmente está el código realmente ejecutable. Como ejemplo ponemos tres niveles de jierarquía:

mi_funcion()
{
    // Llama a entrada, proceso, salida
    DatosEntrada entrada;
    lee_datos(datos);
    DatosSalida salida;
    procesa_datos(entrada, salida);
    muestra_salida(salida);
}

función lee_datos(DatosEntrada datos)
{
    // Llama a las distintas fuentes de entrada
    lee_datos_disco(datos);
    lee_datos_internet(datos);
    lee_datos_no_sé_donde(datos);
}

función lee_datos_disco(DatosEntrada datos)
{
    // Lee de una fuente en concreto
    FILE* f = fopen("fichero", "r");
    fread(&datos.numero, size(int), 1, f);
}

Cuando exportas funciones como miembro de una clase, decláralas privadas menos excepciones justificadas. Código duplicado debería estar en una subfunción siempre.

En la initialización puedes poner un retorno anticipado si no puedes seguir con los datos presentes. Mejor que muchos if añadidos sobre si está el fichero, si puedo leer datos etc., con un else perdido cien líneas más abajo, es algo como

if (no tengo fichero)
{
    return;
}
// Aquí se cumple la condición que tengo un fichero y
// puedo seguir sin más chequeo.

De esta forma lo que realmente quieres hacer en esta función no está añidado en el nivel cinco sino en la columna principal.

En fin, entender una función larga consiste en separarla en bloques comprensibles. Podemos intentarlo nada más que en la mente, pero si tenemos la posibilidad, podemos representar nuestra comprensión en el código – adaptándolo con una reestructuración y comentarios y lo que ya hemos hallado. De esta forma entendemos la función y la mejoramos a la vez.

Anuncios