Cómo y porqué programar con buen estilo

Porqué

Sobre los lenguajes de programación

Un lenguaje de programación es, como todo lenguaje, una forma de comunicación. Es decir, permite transmitir información entre dos entidades de forma que la idea original del transmisor pueda ser recuperada por el receptor.

Los lenguajes de programación tienen la particularidad de cumplir un doble rol. Por un lado sirven para comunicar humanos con computadoras. Son la forma de transformar una serie de abstracciones como algoritmos, módulos, tipos de datos y sistemas en algo que una computadora pueda ejectuar. El segundo rol, y que no se ve tanto a primera vista, es que un lenguaje de programación sirve para comunicar humanos con humanos. Por ejemplo, para que alguien le cuente un algoritmo a otro. O en muchos casos para que un programador pueda mañana recuperar las ideas que volcó en código hoy.

La capacidad de expresar un significado usando un lenguaje depende tanto del lenguaje, como del emisor. Pero el emisor usualmente influye mucho más: es claro que la forma de construir un mensaje puede impactar muy fuertemente en su claridad.

En el caso de los lenguajes de computadoras, la claridad también depende de a quien se dirija el lenguaje. Por ejemplo, para lo computadora son equivalentes las funciones:

float f(float a, float b) {return a*b ;}

typedef float longitud ; typedef float area ; area area_rectangulo (longitud base, longitud altura) { /* Devuelve el area de un rectangulo de base `b' y altura `h' */ return base*altura ; }

Pero a una persona le transmite mucha más información y contenido la segunda función. En el sentido inverso, para la mayoría de la gente son iguales:

    a = 1
    b = 2

a = 1 ; b = 2 ;

Aunque una computadora probablemente se confunda por los errores de sintaxis (los ';' faltantes).

Los objetivos

Dado lo anterior, debería ser clara la motivación para poner atención en el estilo de programación. Los aspectos que normalmente se denominan "estilo" son aspectos relacionados a los lenguajes como medio de comunicación entre personas, y que usualmente no influyen en la comunicación humano-máquina.

La meta final del programador es construir programas. Y el ideal es construir "buenos" programas. Hay diversas cualidades generalmente aceptadas de lo que es un programa "bueno", y cualquier herramienta, técnica o método que nos ayude a mejorar esas cualidades es bienvenida.

Las cualidades que se ven beneficiadas de forma más directa por un buen estilo son:

Cómo

Principios

  1. El estilo del código no es un resultado final, sino algo para preservar a lo largo de toda su escritura.

    Motivo: Es cierto que las primeras veces escribir con estilo requiere un esfuerzo consciente, una vez que uno se acostumbra al estilo, seguirlo deja de ser un esfuerzo adicional. Usualmente es más trabajo "arreglar un código después, que escribirlo bien desde el principio".

  2. El estilo debe ser uniforme en un mismo proyecto.

    Motivo: Al leer un código, uno se acostumbra al estilo usado en una forma que permite entenderlo con un vistazo general. Si hay código con estilos mezclados, leer algo con un estilo después de haberse acostumbrado a leer algo con otro puede ser confuso. Por ejemplo, si dos tramos de código usan distintas abreviaturas para lo mismo, o distintas formas de indentar (que pueden hacer que dos estructuras de control iguales se vean diferentes).

  3. El estilo de código debe promover programas que pueden ser comprendidos de forma inmediata (suponiendo el conocimiento del problema que éste resuelve).

    Motivo: Los problemas de computación ya son complejos y no hay motivo para aumentar su complejidad con código rebuscado. Los programas deberían ser soluciones, no problemas.

  4. El estilo no debe promover fragmentos de código que le dan demasiada importancia a detalles irrelevantes.

    Motivo: Escribir demasiado, le da relevancia a aspectos no fundamentales, y ocupa la atención en aspectos secundarios del programa. Un buen programa debería enfocar la atención en lo importante, y permitir abstraerse de los detalles.

  5. El estilo de indentación debe permitir ver la estructura del código sin mirar el código en sí (es decir, con solo ver la distribución de espacio en blanco y espacio escrito).

    Motivo: Cuando se busca un tramo de programa o se lee rápido, uno puede visualizar la distribución de espacio blanco/no blanco pero no tiene tiempo para ver estructuras, o concordancia de paréntesis/llaves/corchetes.

  6. El estilo de indentación debe poder usarse de la misma forma en distintos lenguajes y verse similar (para construcciones lingüísticas similares).

    Motivo: Muchas veces es necesario cambiar de lenguaje (entre un proyecto o entre varios), y preservar un estilo uniforme permite no tener que estar fabricando reglas nuevas cada vez. Además, un estilo que no puede ser preservado, probablemente implica dependencia de lingüísmos específicos y no de abstracciones generales.

  7. El estilo de código debe permitir fácilmente realizar cambios básicos en el código: agregar una línea a un bloque, borrar una línea de un bloque, mover líneas en un bloque.

    Motivo: Este tipo de cambios es muy usual, y si el estilo dificulta realizarlos interrumpe en la forma de trabajo.

  8. El nombre de un objeto cualquiera del programa (función, variable, tipo), debe permitir identificar el objeto de forma no ambigua rápidamente dentro del área de visibilidad del objeto.

    Motivo: Un estilo de esta forma permite leer el código sin tener que detenerse en cada identificador a recordar (o buscar) donde estaba definido y que era.

Reglas

No hay un "estilo correcto", sino que hay muchos. Definitivamente hay distintos criterios sobre cuál de ellos es el mejor, y discusiones bizantinas al respecto. De todos modos, si hay un acuerdo bastante generalizado sobre varias cosas que se consideran "mal estilo".

Las siguientes reglas no son rígidas, sino que buscan cumplir los principios dados antes, y cada una da algunas variantes.

Cuando hay variantes para elegir, la idea es elegir una y sólo una, y mantenerla, al menos dentro de un mismo proyecto. La mezcla de estilos hace que un lector del código no pueda acostumbrarse a las reglas y tenga que estar mirando el texto del programa cuando quiere ver la estructura.

¿Cuándo escribir con estilo?

Hay que convencerse de que no hay que decir "escribo el código así nomás y después lo arreglo, total al final voy a tirar un montón de líneas...".

Cambios de estilo

Las reglas de estilo son flexibles. Esto no significa que uno va escribiendo y cambiando de estilo. Es muy importante dentro de un mismo proyecto mantener siempre las mismas reglas rígidas, aunque estas sean distintas a las que uno usa en otros proyectos.

Incluso, cuando se trabaja sobre un proyecto escrito por otro, es mejor adaptarse al estilo en que está escrito en vez de mezclarlos.

Cuánto escribir

Usualmente, se puede hacer variar la forma en un rango que va desde lo abreviado/implícito hasta lo palabrero/explícito ('terse' y 'verbose' son los términos más encontrados en los textos en inglés).

Ejemplos:

   /* abrev. */
   for (i=-1; s[++i];) ;

/* Esta es en cambio la versión palabrera (o 'verbose') del mismo * programa para calcular la longitud de una cadena. */ posicion = 0 ; /* Posición inicial de la cadena */ caracter = cadena[posicion] ; while (caracter != TERMINADOR_CADENAS) { /* Avanzar a la posición siguiente */ posicion = posicion + 1 ; caracter = cadena[posicion] ; } longitud = posicion ;

Se aconseja buscar una forma intermedia, que no distraiga la atención innecesariamente con detalles obvios, pero sin perder claridad:

   
   /* Calcular longitud de s en len*/ 
   len = 0 ;
   while (s[len] != '\0')
       len++ ;

Nombres de objetos (variables/funciones)

No ayuda nada ahorrarse un par de letras llamando a una variable global pri_ele en vez de primer_elemento (y evitamos confundirla con primo_elegido), o una funcion global strsz (impronunciable) en vez de string_size. Es por ello que se recomienda no usar abreviaturas, a menos que sea una abreviatura clara y ampliamente usada en el área de aplicación del software (como AFIP en un sistema contable, o CPU en un programa de diagnóstico de hardware).

De todas maneras, un nombre excesivamente largo corre peligro de no ser recordado, requiriendo ir y volver a lo largo del código. De ésto se deduce que se debe evitar la redundancia (por ejemplo, un campo llamado 'longitud_secuencia' en una estructura 'secuencia' debería en realidad llamarse 'longitud'). Incluso una variable local de contador probablemente pueda llamarse i en vez de contador sin perder claridad (y permitiendo concentrar la atención en otras cosas).

El tamaño del código usualmente depende mucho más del buen diseño que de la longitud de los identificadores.

Otra convención que beneficia la lectura y el entendimiento de un código es el uso adecuado de mayúsculas/minúsculas. En general los nombres de los diveros objetos combinan de manera consistente mayúsculas/minúsculas, en cambio la declaración de constantes es toda con mayúsculas.

Algunos ejemplos de posibles variantes para nombres de objetos y definiciones:

    twoWords
    twowords
    two_words

#undef SERIAL_PARANOIA_CHECK #define CONFIG_SERIAL_NOPAUSE_IO #define SERIAL_DO_RESTART #undef CONFIG_SERIAL_CONSOLE #define INT_MIN (-INT_MAX - 1) #define INT_MAX 2147483647

Indentación

Dados los principios anteriores algo esencial en cualquier estilo de indentación es que las componentes de una instrucción compuesta (es decir, las instrucciones condicionales de un if dentro del if, o las instrucciones que se repiten dentro de un while) deben ir a la derecha de esta instrucción. Hay muchas formas aceptadas de hacer esto, por ejemplo:

    if (<cond>) {
            <body>
    } /* Conocido como el estilo K&R */

    if (<cond>)
    {
            <body>
    } /* Conocido como el estilo BSD */

    if (<cond>)
            {
            <body>
            }  /* Conocido como el estilo Whitesmiths */

    if (<cond>)
      {
        <body>
      } /* Conocido como el estilo GNU */

La cantidad de espacios con que se indenta, también es algo para elegir. Normalmente se usa al menos 2, y hasta 8. Lo más usual es 4 u 8. La ventaja de usar indentación chica es que se puede ver más código en la pantalla (o al imprimir), y la de usar indentaciones más grandes es que la estructura es más visible.

Dado que la mayoría de las pantallas/impresoras tienen 80 columnas de texto, conviene no pasarse de ese límite, partiendo las líneas muy largas.

Para indentar se pueden usar TABs o espacios. Para que el código se vea bien en cualquier entorno o editor, hay que ser muy cuidadoso con no mezclar inadecuadamente TABs con espacios. La forma más fácil es usar solamente espacios. La forma más elegante es usar TABs para marcar indentación (al principio del renglón), y espacios para encolumnar cosas cuando se parten líneas o se ponen comentarios a la derecha. Ejemplo:

    /* Nota: los --- denotan espacios, y los /######/ TABs */
    if (condición) {
    /######/instrucción ;
    /######/while (condición) {
    /######//######/instrucción ;----------/* comentario */
    /######//######/otra instrucción ;-----/* otro comentario */
    /######/}
    /######/  /* Este tab puede ir o dejarse la línea en blanco */
    /######/if (condición 1 &&
    /######/----condición 2 && /* Notar el TAB seguido de espacios */
    /######/----condición 3) {
    /######//######/instrucciones ;
    /######/}
    }

Cuando se parten expresiones largas en varias líneas conviene partir en los operadores de menos precedencia. Por ejemplo, a[2]==5 || i!=7 conviene partirlo después del ||, no después del [ o del ==.

Si una línea larga es un llamado a función, conviene separar en las comas que distinguen parámetros. También puede separarse una cadena en dos.

Algunos ejemplos (tomados del fuente del kernel de Linux, busmouse.c).

	if (put_user((char)buttons | 128, buffer) ||
	    put_user((char)dxpos, buffer + 1) ||
	    put_user((char)dypos, buffer + 2))
		return -EFAULT;
...
		printk(KERN_ERR "busmouse: trying to free mouse on"
		       " mousedev %d\n", mousedev);

Pueden revisarlos en el archivo anterior, donde además puede verse la combinación apropiada de TABs y espacios.

Algo que se hace bastante seguido es poner al mismo nivel cadenas de if/else if/else if/.../else, de esta forma:

    if (cond1) {
    	bloque 1
    } else if (cond2) {
    	bloque 2
    } else if (cond3) {
    	bloque 3
    } else {
    	bloque 4
    }

A pesar de que lingüisticamnte cada if y cada bloque debería ir a la derecha del anterior, se usa esta estructura vertical ya que usualmente esa es la estructura del significado del código. Es decir, con lo anterior uno normalmente trata de decir:

if
	# cond1 --> bloque1
	# cond2 --> bloque2
	# cond3 --> bloque3
	# not (cond1 or cond2 or cond3) --> bloque4
fi

Comentarios

Es de extremada importancia la inclusión de comentarios dentro de los códigos. Los mismos facilitan la comprensión del significado y/u objetivo de las funciones, secuencias de control, sentencias en general. O sea, no es necesario comentar cada línea, sino bloques estratégicos, resultados de retornos de funciones, guardas de if's o de bucles, o declaraciones de variables y/o constantes que jugarán un papel protagonista en el programa.

En general se distinguen las siguientes categorías de comentarios:

  1. Comentarios en las funciones/variables: se hace una breve descripción de la funcionalidad de la rutina o de la variable, destacando el aspecto de relevancia en cada caso.
  2. Comentarios en los archivos: la explicación del contenido del archivo, resumiendo lo que se ¨encierra¨ dentro del mismo, o hablando del TAD que representa, etc.
  3. Comentarios en el código: de tipo aclaratorios, sólo cuando algo necesita o es digno de una explicación o justificación.
  4. Comentarios de cierre: aclaraciones relevantes que inducen al entendimiento completo del programa en cuestión.

Constantes

Para lograr los objetivos planteados inicialmente, y poder introducir modificaciones en el código sin necesidad de rehacer el mismo, debemos tener siempre en cuenta cuántas líneas deberíamos retocar ante determinados cambios.

Supongamos que estamos trabajando con información provista por ciertos archivos. Supongamos además, que la ruta actual a esos archivos es /home/pepe/info/, y digamos que abrimos archivos en ese directorio unas 100 veces. Cómo impactaría (en la actualización del código) si de repente pepe deja de existir y toda la información se muda a /var/juan/data/?

Sí, abría que modificar unas 100 líneas de código por lo menos, una por cada fopen y unas cuántas más por cada fclose. Pero podemos ahorrarnos ese trabajo teniendo declarada una constante que representa la ruta al directorio de interés, sea cual sea esa ruta.

En nuestro ejemplo, teniendo definido

#define DATA_SOURCE ¨/home/pepe/info¨
basta con hacer un único cambio a

#define DATA_SOURCE ¨/var/juan/data¨
y a lo largo del código utilizar DATA_SOURCE como ruta constante al directorio de interés. Vale aclarar que este ejemplo se extiende a constantes de todo tipo (enteras, flotantes, de caracteres, etc.)