gravatar

Cómo mejorar la calidad de sus programas - Parte 2


Conceptos básicos


En primer lugar revisaremos algunos conceptos básicos que resulta necesario tener presentes para comprender los problemas que típicamente se presentan durante el diseño y codificación de programas y cómo solucionarlos.

Programación estructurada


La programación estructurada propone el uso de tres tipos de estructuras de control: secuencial, selectivo (condicional) e iterativo (bucles); del diseño descendente y de recursos abstractos, como medios que permite escribir algoritmos “propios”.

El diseño descendente propone, básicamente, la división del problema que se quiere resolver en sub-problemas más sencillos. Una serie de refinamientos sucesivos del problema original conduce a la especificación de una jerarquía de sub-problemas que descienden en su nivel de abstracción, hasta que en su nivel inferior su solución mediante un lenguaje de programación debe ser trivial.


La figura anterior muestra parte de la jerarquía en la que se dividió un problema. En ella se puede ver cómo la programación modular y estructurada propone, mediante el diseño descendente, un uso elemental de la abstracción (que frecuentemente se menciona el la literatura como "recursos abstractos"). Cada nivel de la jerarquía de sub-problemas representa una solución abstracta al problema original. A medida que se desciende en el los niveles de la jerarquía, desciende el nivel de abstracción con el que se describe la solución y por ello aparecen más sub-problemas.

La programación estructurada conlleva una estrecha relación con la programación modular.

Programación modular


La programación modular propone la división de un programa en unidades de programa independientes que en la implementación se convierten en rutinas: procedimientos, funciones y módulos (entendidos éstos últimos como archivos que agrupan funciones y procedimientos).

Así, en lugar de identificar distintos segmentos de código mediante etiquetas y acceder a ellos por medio de saltos (sentencia GOTO), éstos se encapsulan en rutinas (lo que permite evitar muchos errores y facilita su reutilización) que tienen nombres (lo que permite mejorar la expresividad del código).

El diseño descendente es la técnica que propone reducir la complejidad de los problemas dividiendo cada uno en un pequeño conjunto de problemas más sencillos. Esta técnica (también conocida como método de refinamientos sucesivos) postula que la solución de problemas no triviales se debe abordar mediante su descomposición en problemas más simples, que a la vez se podrán descomponer en otros aún más simples, y así sucesivamente hasta llegar a un nivel de complejidad manejable. Los refinamientos terminan cuando el problema puede ser expresado directamente en un lenguaje de programación estructurada (su solución es trivial), es decir, que la solución se alcanza por medio de elementos conocidos: operaciones matemáticas, algoritmos y estructuras de datos tradicionales y elementos del lenguaje de programación elegido.

Esta técnica guarda una estrecha relación con el método de descomposición (reducción o simplificación) que es común aplicar en las matemáticas para resolver ecuaciones, cuyo resultado final es un número o bien una expresión irreductible (forma canónica o forma normal).

Como se verá más adelante, los estilos de programación estructurada y modular guardan una relación indisociable, ya que los fines que la primera se plantea sólo son alcanzables de forma adecuada aplicando también las técnicas propuestas por la segunda.

Programación modular y estructurada


La programación modular y estructurada tiene como uno de sus fundamentos la utilización de tres tipos de estructuras de control que permiten derivar el flujo de ejecución.

La introducción de las estructuras de control permitió dotar al código de una mayor expresividad y legibilidad, haciendo posible que los programadores pudieran realizar derivaciones del flujo de ejecución por medio de estas tres estructuras básicas, en vez de hacerlo cada uno a su propio modo manipulando saltos. Así, el uso de estructuras de control del flujo de ejecución hace totalmente innecesario el uso de sentencias que explícitamente producen saltos condicionales o incondicionales: particularmente de la sentencia GOTO.

Pero la aplicación de estructuras de control y de otros conceptos como el diseño descendente y los recursos abstractos, tiene como objetivo dotar a los programas de ciertas características distintivas de calidad que los convierten en algoritmos “propios”.

Un algoritmo “propio” es aquel que:

  • Tiene un único punto de entrada y un único punto de salida.
  • Todas las sentencias que contiene son alcanzables (no contiene partes que nunca se ejecutan, también denominadas “código muerto”).
  • No contiene bucles infinitos.

La aplicación del concepto de modularidad facilita el cumplimiento de la primer característica.

Así, un algoritmo propio debe conformar una rutina cuyas características son:

  • El código cumple con la definición de algoritmo.
  • Tiene una interfaz (o firma), es decir su nombre y sus parámetros formales, que conforman su punto de entrada.
  • Tiene un punto de salida, que es su última sentencia, que tiene como efecto la derivación del flujo de ejecución a la línea de código que sigue a la llamada.
Entonces, es posible acceder a toda rutina de código haciendo uso de su interfaz (una llamada en la que se proporcionan los parámetros reales requeridos), en lugar de hacerlo mediante saltos explícitos.

Esto también implica que una rutina no debería incluir saltos incondicionales hacia otras rutinas o hacia el programa principal (la rutina debe terminar luego de su última sentencia). Por ejemplo, no debería incluirse más de una sentencia RETURN en una función o procedimiento.

Gambas admite el uso de múltiples sentencias RETURN en una misma rutina.

Conceptualmente una función debe devolver un valor obligatoriamente y no debería producir efectos colaterales. En cambio un procedimiento no debe devolver nunca un valor y puede producir efectos colaterales.

Un efecto colateral es un cambio al estado actual del programa, es decir, una asignación a una variable cuya modificación persiste luego de haber concluido la ejecución de la rutina.

En Gambas no hay diferencia entre usar las palabras claves FUNCTION, PROCEDURE o SUB. Cualquier rutina que las use puede ser tanto un procedimiento como una función. Si lo piensa un momento, verá que lo que define que una rutina sea una función es que devuelve un valor, no la palabra clave utilizada en su declaración.

Gambas tampoco exige que en una función (según su definición conceptual) se utilice:

RETURN {expresión}

ya que el tipo de datos que se declara para la devolución siempre tiene un valor predefinido.

Escribir programas siguiendo esta técnica permite que sean fácilmente comprensibles (una cualidad indispensable para lograr programas fáciles de mantener). Sin embargo, se debe considerar que el contexto en el que se idearon estas premisas en las que se fundamenta el paradigma modular y estructurado, era de un uso caótico y generalizado de saltos condicionales e incondicionales.

No hay nada intrínsecamente malo en ellas, nunca lo hubo en los saltos incondicionales ni en la sentencia GOTO en particular. Sin embargo, los problemas surgieron de su uso inadecuado y de su abuso, algo que puede ocurrir con cualquier otro elemento de un lenguaje de programación si se usa de modos inapropiados.

Observe que no son muchas las diferencias entre un salto a una rutina etiquetada y una llamada a una rutina parametrizada (función o procedimiento):

  • Una rutina etiquetada requiere el uso de variables globales, mientras que una rutina parametrizada utiliza sus argumentos y variables locales.
  • Las variables globales que se utilizan en una rutina etiquetada pueden estar declaradas en cualquier parte del programa, mientras que los parámetros formales de una rutina parametrizada se declaran junto con ella.
  • Mientras que en cada llamada a una rutina parametrizada los valores de sus argumentos forman parte de la misma llamada, en cada salto a hacia una rutina etiquetada los valores de las variables globales que ésta utiliza pueden estar definidos en cualquier parte del programa.
  • Mientras que al terminar la ejecución de una rutina parametrizada el control de ejecución retorna a la línea que sigue a su llamada, en el caso de una rutina etiquetada el control de ejecución puede saltar hacia cualquier parte del programa.

Estas pequeñas diferencias tuvieron, sin embargo, un impacto muy importante en la calidad de los programas. La forma correcta de usar los saltos incondicionales requerían que los programadores se auto-impusieran algunas restricciones de modo que su uso fuera equivalente a utilizar rutinas parametrizables.

Los saltos también se utilizaban para iterar una rutina y para derivar el flujo de ejecución de manera condicionada. No obstante, se trata de situaciones muy similares a la descrita antes.

De modo que las estructuras de control y las rutinas parametrizables introdujeron la posibilidad de que los programadores derivaran el flujo de ejecución de sus programas de formas más controladas (algo que era perfectamente posible utilizando saltos), pero ahora de una forma más fácil.

Hoy en día un lenguaje como Gambas mantiene la sentencia GOTO, pero aunque es más restrictiva que la sentencia GOTO del BASIC original no tiene caso utilizarla para realizar bucles o en reemplazo de funciones y procedimientos.

Desde que en 1966 los científicos computacionales Corrado Böhm y Giuseppe Jacopini demostraron el teorema de la estructura muy lentamente se comenzaron a dejar atrás las dudas sobre el uso de las estructuras de control.

La ventaja de escribir programas propios sería que se obtiene un estilo de programación que haría posible utilizar métodos de verificación formal (aunque Gambas no brinda facilidades para hacerlo) y que se acomodaría mejor al aprendizaje de otras técnicas de programación como el “diseño de software por contrato”.

No obstante, existen circunstancias específicas en las que usarlas puede significar una leve mejora en la legibilidad de la rutina en la que se usan o en su rendimiento, y otras circunstancias en las que resulta poco menos que imprescindible el uso de sentencias que producen saltos incondicionales. Por ejemplo:

  • Al escribir una función generalmente resultará necesario utilizar al menos una sentencia RETURN para indicar el punto de salida y devolver un valor.
  • En el tratamiento de errores frecuentemente se produce un salto incondicional desde la línea que genera el error a la línea que contiene la sentencia CATCH (ubicada en la misma rutina o en otra de las rutinas de la pila de llamadas), pero resulta ser un modo muy claro de manejar las excepciones.
  • Las sentencias BREAK y CONTINUE en ciertas circunstancias permiten evitar la evaluación repetida de expresiones condicionales y por ello una pequeña mejora en el rendimiento del programa: el programador se ahorra escribir alguna línea de código y el programa tiene un costo computacional levemente menor (aunque generalmente inapreciable a simple vista).
  • El uso de la sentencia RETURN en combinación con la sentencia IF, en ciertos casos puede permitirle al programador expresar un algoritmo de forma más breve y cuya lógica es claramente evidente, aunque al mismo tiempo la rutina pierde la cualidad de ser un algoritmo propio.

La escritura estricta de algoritmos propios es útil si se pretende usar algún método de verificación formal para demostrar que el programa cumple con la especificación (es decir, que el algoritmo realmente soluciona el problema que se supone debe resolver, bajo las condiciones previstas) y que lo hace sin producir errores.

El método de verificación formal original basado en la lógica de Hoare conlleva un arduo trabajo y en consecuencia un alto costo, por lo que generalmente no se utiliza. Sin embargo, el método de derivación de programas de Dijkstra (basado en el de Hoare) y otros posteriores como el diseño por contratos son más asequibles.

No obstante, los métodos más extendidos para la prevención, descubrimiento y corrección de errores se basan en pruebas unitarias, pruebas de regresión, de integración y de aceptación, y en técnicas como “escribir las pruebas primero” (testing first) de la metodología Extremme Programming.

En conclusión, la escritura de programas propios no hace daño (no si se utilizan adecuadamente los conceptos de POO, especialmente el polimorfismo), lo que sumado a un poco de experimentación permite descubrir aquellos casos en los que el uso de sentencias que producen saltos incondicionales resulten ventajosas.

Entonces, se puede tomar como pauta que las instrucciones que producen saltos incondicionales de forma limitada, se deben utilizar únicamente cuando resulte muy claro que facilitan la comprensión del código. Exploraremos varias de estas situaciones en el apartado Re-factorización.




gravatar

El binomio TRY / CATCH

Creo que en este párrafo estás hablando de Java en vez de Gambas :-)

Saludos.

Julián

Los comentarios están habilitados para que los lectores puedan participar en la corrección del libro, realizar preguntas puntuales o sugerencias. Todo comentario fuera de estos objetivos será eliminado. Por favor, tenga en cuenta lo siguiente:

- Cumpla las normas de etiqueta.

- Realice críticas constructivas.

- No sea redundante.