Buenas Prácticas de Programación (Parte 1 de 2)

Este artículo presenta una serie de recomendaciones para el desarrollo de software partiendo del reconocimiento de algunos hechos que la práctica del desarrollo de software ha confirmado como verdades indiscutibles:

  • La mayor parte del tiempo y esfuerzo en desarrollo de software está dedicada a la modificación de software existente.
  • Para modificar código existente es necesario primero comprender tanto su estructura como su funcionamiento.
  • Es muy frecuente que el programador que realiza el mantenimiento no sea el autor original del código a mantener.

Entonces, escribir código de manera tal que otros programadores puedan comprender su estructura y funcionamiento reduce el esfuerzo necesario para realizar su (inevitable) mantenimiento. En las siguientes secciones se presentan y justifican algunas recomendaciones que tienen como propósito facilitar la compresión y mantenimiento de código; se incluyen referencias generales a principios de diseño que se desarrollarán con mayor detalle en otros artículos.

Asignación de nombres

El nombre asignado a una variable, constante, enumeración, método, argumento, clase o paquete debe reflejar con la mayor claridad posible el propósito y rol en el código. En consecuencia, no es recomendable emplear siglas, abreviaciones, sufijos o prefijos que fuercen al lector a revisar documentación externa, consultar al autor del código o analizar los detalles de su utilización para entender el propósito del elemento en el código.

Los únicos elementos que debería incluir un verbo en el nombre son los métodos, el resto de los elementos deberían ser nombres (con la única excepción de verbos empleados para nombrar variables o constantes que representan un estado/condición).

Si una variable o método implica una condición, el nombre deberá reflejar claramente la condición; por ejemplo, es preferible emplear “esMayorDeEdad” que simplemente “mayor”. Cuando existan estados opuestos es recomendable emplear los antónimos apropiados; por ejemplo, es preferible emplear “abierto” y “cerrado” que “abierto” y “asegurado”. Es importante emplear siempre la misma palabra para el mismo concepto; por ejemplo, es preferible emplear “obtenerEstudiante()” y “obtenerMateria()” que “obtenerEstudiante()” y luego “recuperarMateria()”; en la última situación el lector se ve forzado a averiguar si “obtener” y “recuperar” se refieren a la misma acción o son simplemente sinónimos.

El uso de valores literales tiende a dificultar la compresión del código, es recomendable reemplazarlos con constantes o enumeraciones; por ejemplo, en lugar de emplear “18” en el código se podría crear una constante “EDAD_MINIMA_PARA_VOTAR”.

Finalmente, una buena manera de verificar si los nombres son adecuados es leer el código en voz alta y preguntar a un colega si comprendió la lectura.

if (estudiante.e > 21)  {


}
if (estudiante.esMayorDeEdad) {


}

Comentarios

En general, los comentarios sólo deberían usarse cuando no es posible expresar en el código algún aspecto del elemento que se comenta; por lo tanto, no es recomendable emplear comentarios para explicar algo que podría quedar claro reorganizando el código y/o seleccionando los nombres apropiados; de la misma manera, no es recomendable incluir en el código comentarios datos registrados en otras herramientas como el autor, propósito y  fecha de modificación de un archivo; estos datos ya están  registrados en el servidor de control de versiones.

Bajo estas consideraciones, es pertinente mencionar algunos de los usos aceptables de los comentarios:

  • Información legal. Por ejemplo, la licencia aplicada al código.
  • Tareas pendientes o deuda técnica. Por ejemplo, los comentarios TODO.
  • Clarificación de sintaxis compleja. Por ejemplo, explicando el propósito de una expresión regular.
  • Soporte para la generación de documentación. Por ejemplo, comentarios Javadoc.

Condiciones

Las estructuras de control condicionales tienen un impacto directo en la complejidad del código y el propósito de su presencia debería estar claramente expresado en el código. A continuación se enumeran algunas prácticas recomendadas para la utilización efectiva y/o simplificación de este tipo de estructuras de control:

Asignar el valor de expresiones booleanas. Por ejemplo:

If (edad > EDAD_MINIMA_PARA_VOTAR) {
puedeVotar = true;
else  {
puedeVotar = false;
}
puedeVotar = edad > EDAD_MINIMA_PARA_VOTAR</td>


Emplear el operador ternario. Por ejemplo:

if (esInvitado) {
costoEntrada = 0
}
else {
costoEntrada = 10;
}
costoEntrada = esInvitado ? 0 : 10;

Encapsular condiciones complejas en métodos o variables intermedias.

Emplear polimorfismo en lugar de sentencias switch.

Lógica dirigida por datos dinámicos.

Extraer comportamiento común de las secciones if/else. En el siguiente ejemplo la extracción permite eliminar la seccion else:

if( conteos.get(palabra) != null ) {
Integer conteo = conteos.get(palabra);
conteos.put(palabra, conteo++);
}
else {
conteos.put(palabra, 1)
}
if( conteos.get(palabra) == null ) {
conteos.put(palabra, 0)
}
Integer conteo = conteos.get(palabra);
conteos.put(palabra, conteo++);

 

Métodos

Si el código fuente es la historia que cuenta el funcionamiento del software, los métodos son los verbos y el resto de elementos los sustantivos. Los métodos, al igual que los párrafos de un libro, son el mecanismo básico para organizar el contenido (código fuente) que describe el funcionamiento de un software.

Los métodos se crean principalmente para 1) reducir la duplicación y 2) crear niveles de abstracción para gestionar la complejidad facilitando de esta manera la lectura y compresión del código. Para facilitar el logro de estos propósitos los métodos deben exhibir las siguientes características fundamentales: 1) deben realizar una sola función y 2) su definición, en términos del nombre y los parámetros, debe transmitir de la manera más clara posible el propósito del método sin necesidad de tener que ver el código que contiene o recurrir a comentarios aclaratorios; ambas características se complementan porque si un método realiza una sola función es más fácil asignarle un nombre y parámetros que transmitan claramente su propósito y si una función tiene un nombre que transmite claramente su propósito es porque quizá este centrada en realizar una sola función.

A continuación se mencionan algunas recomendaciones a tomar en cuenta a la hora de escribir métodos:

Parámetros. Funciones con más de dos parámetros tienden a ser más difíciles de comprender con sólo ver el nombre de la función y la lista de parámetros. Se debe tratar de evitar parámetros booleanos porque usualmente cuando una función requiere este tipo de parámetros está tratando de hacer al menos dos cosas (una para el valor “true” y otra para el valor “false”). De la misma manera, no es recomendable emplear parámetros de salida porque de manera natural se asume que los parámetros son la entrada requerida por un método.

Variables. Para facilitar la compresión del código, las variables locales deben declararse  próximas a la sección del código (ámbito) donde se emplean.

Valores de retorno. No es recomendable devolver valores que representen condiciones de error porque esto obliga al código que usa la función a verificar si el  método se ejecutó exitosamente mezclando, de esta manera, la gestión de errores con tareas relacionadas a lograr el objetivo del método; es mejor emplear excepciones para esos casos.<>/p

Realizar una sola función. De acuerdo con el principio de separación de comando y consulta, un método no debería calcular un valor y realizar cambios en el estado del sistema (causar un efecto colateral) al mismo tiempo; es decir, una función que causa un cambio en el estado del sistema no debería devolver un valor y una función que devuelve un valor no debería cambiar el estado del sistema. Un método no debería tener “secciones”; cada una de estas secciones deberá residir en un método (el método puede estar en la misma clase o en una nueva clase) de manera tal que a cierto nivel de abstracción el método incluya únicamente los pasos necesarios para que la función complete su tarea (los pasos expresados como métodos en el siguiente nivel de detalle). La gestión de errores es una función que en si misma; si un método gestiona excepciones, el bloque try{} debería contener únicamente la llamada a otro método; ese método debería lanzar excepciones (en lo posible no verificadas) para informar sobre posibles errores, de esta manera el código para gestionar errores está claramente separado del código para realizar las funciones de negocio.

Tamaño de un método. Si cada método realiza una única función apoyándose en otros métodos en el siguiente nivel de detalle, ningún método debería llegar tener más de 10 – 20 líneas con excepciones muy específicas como, por ejemplo, en el caso de métodos que contienen estructuras condicionales tipo switch (key){}. La simplificación de estructuras condicionales ya sea encapsulándolas en métodos y/o aplicando estrategias para reorganizarlas de manera que se reduzca el nivel de anidamiento reduce significativamente el número de líneas en un método.

Referencias

[1] Código Limpio: Manual de estilo para el desarrollo ágil de software

[2] Code Complete: A Practical Handbook of Software Construction, Second Edition

[3] Java Code Conventions

[4] Malos olores en el código (bad smells)

[5] The Wise Developers’ Guide to Static Code Analysis featuring FindBugs, Checkstyle, PMD, Coverity and SonarQube

[6] Replacing If-Else With Commands and Handlers

[7] If-Else Is a Poor Man’s Polymorphism

[8] 10 things you’re doing wrong in Java

Comments are closed.