cerosYunos

Consejos y Buenas Prácticas en Programación

computadora
Pablo Sánchez
p.sanchez (arroba) unican (punto) es
Dpto. Ingeniería Informática y Electrónica
Facultad de Ciencias, Universidad de Cantabria



1. Introducción

Cuando se codifica una aplicación software, existe un número potencialmente infinito de programas que satisfacen los mismos requisitos. Sin embargo, no todos estos programas comparten los mismos atributos de calidad. Es decir, no todos son iguales de eficientes tanto en consumo de procesador como de memoria; no todos son igual de legibles; no todos son igual de fáciles de modificar; no todos son igual de fáciles de probar y verificar que funcionan correctamente, etc.

La labor de un Ingeniero Informático o Ingeniero Software no es solamente crear aplicaciones software que funcionen correctamente, sino que además debe crear aplicaciones software robustas, eficientes, fáciles de mantener y un largo etcétera de propiedades extras. Muchas personas están capacitadas para codificar aplicaciones software pero un número bastante más reducido de ellas están capacitadas para hacerlo conforme a los parámetros de calidad que se espera de un titulado superior en Informática. Hoy en día es muy fácil aprender a programar por cuenta propia, dado la amplia cantidad de información disponible a través de internet y la multitud de cursos que se ofertan.

Sin embargo, tanto el autoaprendizaje no debidamente supervisado como la mayoría de los cursos de programación que se imparten llevan a adquirir las habilidades necesarias para la creación de software artesanal, donde el programador codifica un algoritmo y prueba su correcto funcionamiento mediante un proceso de prueba y error tras largos periodos de depuración.

Existe una diferencia entre este tipo de creación de software artesanal y el proceso de desarrolo software que se supone debe seguir un Ingenierio en Informática. En la creación del software artesanal el objetivo principal suele ser que la aplicación funcione sea como sea. En la Ingeniería del Software, un conjunto de soluciones probadas, cuyas ventajas e incovenientes son bien conocidas, se van aplicando con el objetivo de que la aplicación software además de hacer lo que se desea que haga, cumpla una serie de propiedades extras, tales como que el programa sea fácilmente modificable.

No se pone duda que con suficiente ladrillo, cemento, vigas y tejas, un obrero especializado sea capaz de construir una habitación o una caseta de herramientas más que decente, la cual probablemente cumplirá su cometido y puede que permanezca en pie durante muchos años. Pero si se trata de asegurar que esa construcción puede resistir vendavales, tormentas y terremotos, siempre mejor que su construcción sea supervisada por alguien con conocimientos en estructuras de edificios. Y si la obra fuese de mayor envergadura, posiblemente los conocimientos de nuestro obrero especializado, aunque sólidos, no serán suficientes y será necesaria la intervención de un Ingeniero con más sólidos y avanzados conocimientos. Análogamente,no se discute que muchos programadores experimentados puedan crear aplicaciones software, pero sólo aquellos que poseen los conocimientos adecuados podrán hacer que éstas sean además fácilmente adaptables, que sus módulos sean altamente reutilizables, etc.

En resumen, citando al Dr. Ricardo Peña [6] en el prólogo de su libro:

Este libro es, por otra parte, producto de una concepción de la programación como disciplina científica, en contraposición a una concepción artesanal, tan predominante por desgracia en la actividad diaria, en la que el programador construye sus programas por el método de prueba y error. En consecuencia, aquí priman las actividades de diseño y razonamiento frente a la depuración mediante ejecución. En ocasiones, esta concepción puede resultar algo árdua, pero la recompensa de unos programas con una alta garantía de corrección y la satisfacción intelectual de haberlos construidos bien desde el principio, bien mererece la pena.

Esta página contiene una serie de consejos y buenas prácticas acerca de como escribir programas de computador, fruto tanto de la experiencia del autor como de la experiencia y consejos de otros mucho más sabios que él (cf. [1, 2, 3]). Estos consejos están destinados a ayudar a convertir el desarollo software en una disciplina ingenieril, más que en un proceso artesanal. Muchos de los consejos que se dan en esta página constituyen principios de lo que se ha venido a denominar la programación estructurada.

NOTA: Merece la pena destacar que la adherencia a estos principios y consejos tiene una clara influencia en la calificación de prácticas y pruebas correspondientes a las asignaturas que el autor de esta página imparta en la Universidad de Cantabria o centros similares.

2. Lista de Buenas Prácticas

Consejo 1: Evitar el uso de variables globales.

Justificación: Una variable global es una variable colocada fuera del ámbito de varias funciones y que es globalmente accesible por todas ellas. No se debe confundir con un atributo de una clase, que es un campo dentro de un registro que varias funciones manipulan. Un consejo útil para distinguir atributos de variables globales es pensar si la variable corresponde a una característica identificable del conjunto de individuos representado por una clase.

El uso de variables globales pueda resultar tentador por lo cómodo de evitar tener que estar pasando la variable global como parámetro. Sin embargo, el uso de variables globales puede tener efectos laterales bastante peligrosos (cf [4]). El problema es que al ser la variable globalmente accesible, ésta puede, por malicia o simple infortunio, ser modificada fácilmente por terceras funciones, asignando a la variable un valor no deseado. Este valor no deseado puede desecandenar una serie de fallos en cadena. Al ser la variable accesible por un número potencialmente largo de funciones, una vez producido el fallo, depurar el programa hasta averiguar cual es la causa del fallo puede ser un proceso largo y tedioso. En muchas Escuelas de Ingeniería Informática el uso no justificado de variables globales es causa directa de suspenso en un examen o práctica de programación.

NOTA: Linus Torvalds realiza la misma recomendación para la creación de código para el núcleo de Linux: sólo usar variables globales cuando es realmente necesario. Ver aquí.

NOTA: El uso de variables globales no será causa directa de suspenso en las prácticas y pruebas correspondientes a las asignaturas que el autor de esta página imparta en la Universidad de Cantabria o centros similares, pero si rebajará sensiblemente la calificación de las mismas. Un abuso en el uso de variables globales si será causa automática de suspenso.

Consejo 2: Evitar el uso de sentencias goto, break y continue .

Justificación: Evitar el uso de sentencias que rompan el flujo secuencial de ejecución de un programa es el principio básico de la programación estructurada. Las sentencias tipo goto (cf. [1]) dificultan la legibilidad, depuración y verficiación de programas, al permitir a la ejecución de un programa realizar saltos arbitrarios. Aquellas funciones que hacen uso de sentencias goto son mucho más difíciles de reutilizar y depurar.

El uso de sentencias break y continue suele estar asociado a técnicas de construcción de bucles artesanales, donde primero se codifica un bucle y luego se comprueba su correción mediante prueba y error usando depuración. El no usar estas sentencias obliga normalmente a pensar primero que condición o invariante debe cumplirse durante la ejecución del bucle, y acontinuación codificar el bucle. Esta segunda técnica resulta mucho más ingenieril, es la base para la posterior verificación formal del bucle y suele contribuir a mejorar la legibilidad de los programas.

Por ejemplo. en un bucle tipo while libre de sentencias break, sabemos que el bucle terminará cuando la condición deje de cumplirse. Por tanto, para saber si un bucle terminará, bastará comprobar que en el cuerpo del bucle se realiza algún cómputo que converge hacia tal condición. Además, en ausencia de sentencias break, a la salida del bucle sabemos que la negación de la condición del bucle siempre será un predicado verdadero, lo que constituye la base para seguir diseñando el programa.

Comentar finalmente, que las sentencias break y continue son versiones degeneradas de la sentencia goto. La misión principal de estas instrucciones es forzar la salida de un bucle o su continuación. Si el programa fuese modificado y las sentencias break o continue quedasen anidadas dentro de nuevos bucles, éstas podrían dejar de cumplir su función. Al modificar el programa las sentencias break y continue podrían salir de, o continuar con, un bucle que no es el deseado, respectivamente. En ambos casos, la aplicación presentaría un comportamiento no deseado.

Consejo 3: Usar un único return por función, que se colocará como última sentencia de la función.

Justificación: Este consejo una consecuencia de uno de los principios de la programación estructurada. Dicho principio establece que los programas deberían tener un único punto de entrada y un sólo punto de salida. Además, enlanzado con el consejo anterior, un return puede considerarse como un GOTO hacia el final de la función. El uso de un único return por función, colocado consecuentemente al final de la misma, facilita tanto la depuración como la adaptabilidad de los programas.

Por ejemplo, si estamos depurando un programa y queremos saber que valor devuelve una función, en caso de existir un único return, bastará con o imprimir el valor devuelto antes de invocar dicho return; o poner un punto de ruptura en tal sentencia. Si por el contrario tuviésemos, pongamos por ejemplo, 5 sentencias return en la función, tendríamos que colocar y gestionar 5 puntos de ruptura durante la depuración del programa.

Como ejemplo de mejora de la adaptabilidad, supongamos ahora que tenemos una función que devuelve una longitud en centímetros. Supongamos también que tenemos que adaptar nuestro software para que trabaje en el sistema anglosajón, devuelviendo ahora en lugar de una cantidad en centímetros, la misma cantidad pero expresada pulgadas. La adaptación requerida sería tan fácil como invocar una función cmApulgadas(..) sobre el valor devuelto por la función. Si hay un sólo return, sólo tendremos que modificar una línea de código. Si tenemos, pongamos por ejemplo, 5 sentencias return, tendremos que modificar 5 líneas de código, lo cual es más propenso a errores.

Consejo 4: Evitar escribir funciones y procedimientos demasiado largas.

Justificación: Según estudios piscológicos, la mente humana no es capaz de procesar más de 6 ó 7 detalles diferentes a la vez (y a mí, particularmente, me parece éste un número bastante generoso). Las funciones demasiado largas suelen contener un número de detalles superior a este límite. Ello dificulta su legibilidad y comprensión y por tanto, su mantenimiento.

En general, en funciones demasiado largas hay trozos claramente diferenciados de código, débilmente acoplados. Cada uno de estos bloques suele realizar una tarea distinta. Por ejemplo, es habitual en muchas funciones que al principio se preparen los datos para realizar un cómputo, o se inicialice alguna estrucura. A continuación se realizan una serie de cálculos y por último se presenten por la salida los resultados.

En ese caso, por ejemplo, se recomienda dividir la función en tres subfunciones inicializa(..), calcular(..) e imprimir(..). No importa que las expectativas iniciales de reutilización de estas funciones sean prácticamente nulas. En general, si dos trozos de código pueden aparecer juntos en una sola función o separados en dos subfunciones, la opción recomendada siempre es separarlos, salvo justificación irrefutable en contra.

Según Linus Torvalds, creador del núcleo de Linux, "las funciones deberían ser cortas y dulces, y servir para un único propósito. Deberían ocupar una o dos pantallas estándares de texto, es decir, una pantalla de 24 líneas por 80 caracteres de ancho." (Ver aquí)

Consejo 5: Evitar copiar y pegar trozos cuasiidénticos de código a lo largo de una aplicación software.

Justificación: Si un trozo de código, por ejemplo, una serie de sentencias destinadas a impimir una matriz por pantalla, se copia y pega cada vez que se necesite dicha funcionalidad, el resultado será una aplicación con bloques idénticos de código dispersos a través de múltiples lugares de una aplicación software. El principal problema viene cuando hay que modificar este bloque de código. Por ejemplo, si simplemente queremos añadir un recuadro a la matriz, deberemos modificar de la misma forma múltiples puntos de nuestra aplicación, lo que es propenso a errores y puede provocar problemas de consistencia. En lugar de copiar y pegar trozos de código a lo largo de una aplicación software, la práctica recomendada es encapsular dicho trozo de código en una función e invocarla cuando se necesite.

NOTA: Muchos entornos de desarrollo traen funcionalidades para encapsular trozos de códigos en funciones tan automáticamente como sea posible. Suele aparecer en utilidades de refactorización bajo el nombre de "ExtractMethod".

NOTA: El abuso de la técnica de "corta y pegar" será causa directa de un detrimento notorio en la calificación de las prácticas y pruebas correspondientes a las asignaturas que el autor de esta página imparta en la Universidad de Cantabria o centros similares.

Consejo 6: Colocar cada clase o módulo en un fichero separado.

Justificación: Usar un fichero diferente para cada módulo o clase permite su reutilización a nivel individual entre aplicaciones. La única excepción a esta normal es que una clase o módulo esté muy fuertemente acoplada a otra, siendo por tanto imposible que funcione de forma separada a esta última. Sólo en este caso particular estaría justificado colocar ambos módulos o clases en un mismo fichero.

NOTA: Las colocación de todos los módulos o clases de una aplicación software en un único fichero supondrá automáticamente un cero, con total independencia de la calidad de su contenido, en la calificación de las prácticas y pruebas correspondientes a las asignaturas que el autor de esta página imparta en la Universidad de Cantabria o centros similares.

Consejo 7: Colocar la función main en una clase o módulo separado e independiente.

Justificación: La función main suele representar múltiples lenguajes de programación (e.g., C, C++, C# o Java) el punto de entrada a la aplicación. Su único objetivo es iniciar la aplicación. Por tanto su funcionalidad no pertenece a ninguno de los módulos o clases en los que se descompone una aplicación. Por tanto, la práctica recomendada es crear un módulo o clase Runner o Launcher que contenga únicamente la función main y las funciones auxiliares que ésta pudiese necesitar. Eliminando la función main de los módulos o clases en los que se descompone una aplicación incrementamos la reutilización de estos elementos a nivel individual.

Consejo 8: Evitar el uso de elementos no habituales de un lenguaje.

Justificación: Muchos lenguajes, como C, contienen elementos que constituyen excepciones a la regla general. Normalmente, estos elementos son solamente conocidos por aquellos programadores que han leido el manual de referencia del lenguaje en profundidad, y que en contra de la opinión del autor de esta página, se conocen como programadores avanzados.

Por ejemplo, en C los enteros pueden usarse como valores booleanos en las condiciones de las estructuras de control. En C, el cero se considera como falso y cualquier valor distinto de cero como verdadero. Por tanto, si queremos escribir una función que divida dos números en caso de que el denominador no sea cero, podríamos escribir el código que aparece a la izquierda de la Tabla 1. No obstante, esta forma de escribir código es críptica y por tanto desaconsejable. Las unicas justificaciones para escribir código de esta forma son por vanidad o vagancia. En el último caso, el programador busca ahorrar escribir unos cuantos caracteres. En el primer caso, el programador "avanzado" busca incrementar su ego d, mediante su reconocimiento como un conocedor de los secretos más ocultos del lenguaje de programación. En ambos casos, se trata de aptitudes en absoluto recomendables.

El código debe ser escrito para que sea entendido por cuantos más programadores mejor. Por tanto, debe escribirse de una forma sencilla y que pueda ser bien entendida. Se recomienda por tanto usar la alternativa de la Tabla 1 derecha en lugar del código de la Tabla 1 izquierda.

Figure 1. (izq) Codificación ilegible apoyándose en rarezas del lenguaje. (dch) Codificación legible
  float divide(int a, int b) {
        float result = 0;
        if (!b) {
             result = a/b;
        } // if
        return result;
  } // divide
  float divide(int a, int b) {
        float result = 0;
        if (b != 0) {
             result = a/b;
        } // if
        return result;
  } // divide

Consejo 9: Usar siempre llaves ({}) en las estructuras de control.

Justificación: Muchos lenguajes, inspirados en C, permiten la posibilidad de suprimir las llaves que delimitan un bloque de código ({}) cuando dicho bloque contiene una sola sentencia. Esto suponíauna ventaja en los 70, los 80 e incluso los 90. Las ventajas eran: (1) reducir el tamaño del código fuente, cuando cada kilobyte era importante; (2) reducir el espacio que ocupaba un función en pantalla (Ver aquí).

Hoy en día una humilde memoria USB es capaz de almacenar varios gigabytes, siendo le coste de un kilobyte despreciable. De igual modo, los monitores ya han superado con creces el límite de las 25 líneas, por lo que intentar comprimir el espacio que un bloque de código ocupa en pantalla ha dejado de ser relevante. Lenguajes modernos como Java mantienen esta característica, básicamente por compatibilidad con C y C++. Se concluye por tanto que el evitar el uso de llaves como no tiene ningún beneficio práctico hoy en día y debe ser atribuido a la simple pereza del programador. La pereza, aparte de ser un pecado capital, es una cualidad en absoluto admirable.

Por otro lado, el omitir llaves puede generar varios problemas (Ver aquí). En primer lugar, podemos encontrarnos con el caso en el que tengamos que extender dicho bloque de código con más sentencias. Por ejemplo, podríamos necesitar añadir una setencia para imprimir algún valor con objeto de depurar un programa. Si hemos evitado el uso de llaves, puede que no caigamos en la cuenta de que el bloque de código no está delimitado por llaves (error muy común cuando se llevan varias horas programando). La nueva sentencia quedará, en contra de nuestras intenciones, fuera de la estructura de control.

En segundo lugar, algunos lenguajes admiten sentencias vacías. Puede darse el caso de que, por error, si no usamos llaves escribamos un ; tras la estructura de control, que ejecutará por tanto una sentencia vacía (e.g. for(int i=1;i<MAX_VECTOR;i++);). Encontrar y solucionar estos errores suele ser un proceso bastante largo y tedioso. Por tanto, se aconseja usar siempre llaves para delimitar los bloques de código pertenecientes a una estructura de control, incluso cuando no sean necesarias.

Consejo 10: Colocar al lado de una llave que cierre un bloque de código un indicativo de que tipo de estructura cierra.

Justificación: Es común cuando se anidan diversos bloques de código encontrarse con líneas de código que simplemente sean una secuencia de cierre de llaves tipo }}}}. En estos casos, es muy fácil perder la pista de que tipo de bloque de código o estructura cierra cada llave. Por tanto, se aconseja poner al lado de cada llave algún comentario que indique que tipo de estructura o bloque de código se cierra.

La práctica habitual es usar: (1) el nombre del tipo de estructura de control que cierra; (2) el nombre de la función, en el caso del cierre defunciones; (3) el nombre de la clase, registro o módulo, en sus respectivos casos. Esto ayuda a asociar cada llave con la estructura que cierra.

Consejo 11: Inicializar siempre las variables cuando se declaran.

Justificación: Existen lenguajes como Java que inicializan las variables a un valor constante por defecto cuando éstas son declaradas. Por ejemplo, las variables de tipo entero son inicializadas a cero. Otros lenguajes, como C ó C++, no inicializan las variables por defecto aningún valor. Por tanto, en estos lenguajes si no se asigna ningún valor inicial a las variables, éstas contendran un valor indeterminado, presentando la aplicación software probablemente un comportamiento no determinista.

Saber qué lenguajes inicializan automáticamente las variables y cuales no, y en caso de que las inicialicen, a qué valor lo hacen, puede ser una tarea bastante compleja, dado el gran número de lenguajes de programación existentes. Por tanto, la práctica recomendada es, por seguridad, iniciliazar siempre explícitamente las variables con independencia de que el lenguaje lo haga o no. Algunos programadores podrían esgrimir el argumento de que esta solución es ineficiente porque las inicilizaciones son redundantes. Sim embargo si las inicializaciones son redundantes y el compilador está bien diseñado, éste debería simplemente obviarlas. Además, un lenguaje que inicializa siempre las variables a un valor por defecto no es un lenguaje pensado para ser eficiente. Consecuentemente, no deberíamos preocuparnos por la eficiencia, más allá de lo razonable, cuando usamos este tipo de lenguajes.

Consejo 12: No declarar atributos de clases y registros como públicos, ni acceder a ellos directamente.

Justificación: Este consejo se basa en el principio de ocultación de la información. Según este principio (cf. [5]), una aplicación software debe descomponerse en un conjunto de módulos software que se comunican entre sí a través de interfaces bien definidas. Estas interfaces deben exponer la funcionalidad ofrecida por cada módulo a la vez que esconden los detalles de su implementación. Siguiendo estos principios, el acceso a atributos de una clase o registro debe hacerse a través de una serie de métodos o funciones, denominadas getters y setters. Las responsabilidades de estos métodos son garantizar que estos atributos contengan valores correctos, de acuerdo con la semántica de cada campo y del registro o clase como unidad. El propósito principal es impidir que se puedan escribir en los atributos valores carentes de sentido (e.g., edades con valores negativos, o fechas tales como 30/02/2010).

Además, obligando a cambiar el valor de un atributo de una clase a través de un método setter podemos fácilmente hacer, por ejemplo, que una serie de objetos sean notificados cada vez que el valor de este atributo cambie (Ver aquí).

Consejo 13: No ser excesivamente estrictos con la ocultación de información.

Justificación: A pesar de ser la ocultación de información algo positivo, su exceso puede tener efectos negativos. Uno de estos efectos negativos es que los módulos o clases no puedan ser adaptados por medio de alguna relación de especialización, tipo herencia. Por tanto, se aconseja no ser excesivamente protectivos con la visibilidad de funciones y atributos de una clase, permitiendo que éstos puedan ser modificados por las subclases que hereden de dicha clase. Esto debería incrementar la adaptabilidad y facilidad de mantenimiento.

Consejo 14: No usar caracteres propios del castellano en la codificacion de programas.

Justificación: Las vocales acentuadas y la ñ, al no ser caracteres básicos ASCII, pueden sufrir alteraciones cuando un fichero de código se abre en dos computadoras diferentes, incluso si ambas contienen el mismo sistema operativo. Para evitar estos problemas se recomienda codificar directamente en inglés, lo que además facilita la legibilidad del código por parte de la comunidad no hispano hablante. En caso de no dominar el inglés, o simplemente preferir no usarlo, se recomienda renunciar a las vocales acentuadas y sustuir la ñ por su forma catalana (ny) o por su forma gallega (nh).

Consejo 15: Controlar desbordamientos en vectores cuando se accede a ellos.

Justificación: Para evitar acceder a posiciones situadas fuera de los límites de un vector (array) cuando se accede a una posición del mismo, se recomienda dedicar al menos unos segundos a analizar si la expresión numérica usada para acceder a una posición determinada del vector podría, en algún caso, tomar algún valor que se situáse fuera de sus límites. Si así fuese, según el lenguaje de programación que estemos usando, podríamos obtener: (1) una excepción en tiempo de ejecución (e.g Java); o (2) obtener un compartamiento del programa no determinista (e.g. C), puesto que en este segundo caso no podemos predecir que hay más allá de los límites de un vector. Por tanto no podríamos saber que valor estamos escribiendo o leyendo.

Consejo 16: Controlar que no se dereferencian punteros nulos.

Justificación: Se recomienda antes de dereferenciar un puntero, dedicar al menos unos segundos a analizar si éste pudiese ser nulo, con objet de evitar acceder a posiciones de memoria indeseadas. Si se derefencia un puntero nulo en tiempo de ejecución se obtiene, dependiendo del lenguaje de programación utilizado: (1) una excepción en tiempo de ejecución (e.g Java); o (2) un comportamiento no determinista de la aplicación software (e.g. C). Este segundo caso suele dar como resultado un acceso a una zona de memoria no permitida, con la consiguiente interrupción inmediata de aplicación software por parte del sistema operativo (en el mejor de los casos).

Consejo 17: Comprobar consistencia semántica de los argumentos de una función.

Justificación: Una función podría verse obligada a usar para sus argumentos tipos de datos cuyo rango de valores abarque más valores de los deseados para dichos argumentos. Por ejemplo, sea la función setDia(int dia) que asigna a un atributo de una clase Fecha un valor para el día del mes. Aunque el tipo del argumento está declarado como int, dicho argumento sólo debería aceptar valores comprendidos entre el 1 y el 31. Además, se debería comprobar que el día es consistente con el mes de la fecha, ya que no todos los meses tienen 31 días.

Existen dos soluciones diferentes para este problema: (1) comprobar la coherencia de los argumentos antes de ejecutar el cuerpo de la función y lanzar una excepción si estos argumentos fuesen inconsistentes; o (2) declarar precondiciones para la invocación de esta función. La precondición advierte que el correcto funcionamiento de la función no está garantizado si no se respetan las precondiciones. El ejemplo de abajo muestra una función Java con precondiciones. Una precondición, usando lenguaje coloquial, es una forma de decir "el que avisa no es traidor".

Figura 2. Una función Java con precondición.
  /**
  * Sets the current day of the month
  * @param day Day of the month to be assigned
  * Pre: day must be between [1..31]
  */
  void setMonthDay(int day) {}

Consejo 18: Acostumbrase a depurar aplicaciones software sin usar depuradores.

Justificación: Los depuradores de código son unas excelentes herramientas. Pero, por desgracia, a medida que se programan sistemas más y más complejos, dejan de ser útiles, dado que en muchas ocasiones, simplemente no pueden usarse. Por ejemplo, suelen fallar cuando se trata de integrar una aplicación con un sistema mantenido por terceros, cuyo código fuente no está disponible. No funcionan normalmente cuando cuando se trabaja con simuladores de tecnologías novedosas, como por ejemplo, de dispositivos móviles. Sin embargo, casi siempre dispondremos algún lugar donde poder imprimir mensajes y los valores de aquellas variables que sean de nuestro interés. Por eso se aconseja acostumbrarse a depurar aplicaciones software a base de imprimir mensajes por consola en lugar de usar depuradores.

Consejo 19: Expresar valores literales como constantes.

Justificación: Cuando se codifica una aplicación software, en muchas ocasiones hay que usar valores literales (e.g. el valor del número e, el tamaño máximo de un vector, la dimensión de una matriz o el CIF de una empresa). Se aconseja expresar estos valores como constantes. Esto debería incrementar la adaptabilidad de la aplicación, dado que si estos valores mutasen, simplemente deberíamos modificar el valor asociado a una constante.

Consejo 20: La herencia nunca debe usarse para heredar atributos de una clase.

Justificación: La herencia se ha considerado en muchas ocasiones un mecanismo de reutilización o de extensión. De hecho en Java la herencia se declara con la palabra clave extends. Sin embargo, la utilización de la herencia como simple mecanismo de reutilización suele resultar en una violación del principio de sustitución de Liskov [7]. Dicho principio establece que para que nuestro sistema de tipos sea seguro y los objetos que pertenecen a él tengan el comportamiento esperado, cada objeto de una clase hija debe poder ser utilizado de forma segura allá donde se requiera un objeto de la clase padre, en sustitución de éste. Esto lo que establece es que la herencia es una forma de reutilización del tipo, pero no necesariamente de su implementación. Por tanto, si lo que queremos es reutilizar implementación, es mucho más recomendable y seguro reutilizar por composición má delegación.

3. Referencias

[1] Edsger W. Dijkstra. "Letters to the editor: go to statement considered harmful". Communications of the ACM 11(3):147–148 Marzo 1968.

[2] Edsger W. Dijkstra. "A Discipline of Programming". Prentice Hall, 1976.

[3] Michael A. Jackson. "Principles of Program Design". Academic Press, 1975.

[4] William Wulf and Mary Shaw. "Global Variable Considered Harmful." ACM SIGPLAN Notices 8(2):28-34, Febrero 1973.

[5] David Parnas. "On the Criteria to Be Used in Decomposing Systems Into Modules". Communications of the ACM, 15(12), December 1972.

[6] Ricardo Peña. "Diseño de Programas: Formalismo y Abstracción.". Prentice Hall, 1998.
[7] Robert C. Martin. "Agile Software Development, Principles, Patterns, and Practices" Pearson. 2ª Edición. Junio 2011.


Last update: 22/09/2010

Valid XHTML 1.0 Transitional ¡CSS Válido!