Guía completa de lenguajes de programación: desde ensambladores hasta la orientación a objetos

Enviado por Programa Chuletas y clasificado en Informática y Telecomunicaciones

Escrito el en español con un tamaño de 20,87 KB

Lenguajes de Programación

Generaciones de Lenguajes

En la etapa de codificación se elaboran los programas fuente.

Lenguajes de Primera Generación

Son los ensambladores con un nivel de abstracción muy bajo.

Lenguajes de Segunda Generación

No dependen de la estructura de ningún computador en concreto. Primer lenguaje de alto nivel. Son Fortran, Cobol, Algol y Basic.

Lenguajes de Tercera Generación

Podemos verlos bajo dos enfoques:

  1. Programación Imperativa: Lenguajes fuertemente tipados, con redundancias entre la declaración y el uso de cada tipo de dato, facilitando así la verificación en compilación de las posibles inconsistencias. Son:
  • Pascal: Tipificación de datos rígida, compilación separada deficiente.
  • Modula-2: Se separa la especificación del módulo de su realización concreta, con esto tenemos compilación segura y permite la ocultación de los detalles de la implementación.
  • C: Flexible, sin restricción en los datos.
  • Ada: Permite la definición de elementos genéricos, la programación concurrente de tareas y su sincronización y cooperación.
Programación Orientada a Objetos, Funcional o Lógica: Orientados hacia el campo para el cual han sido diseñados, son:
  • Smalltalk: Precursor de los lenguajes orientados a objetos.
  • C++: Se incorpora a C, ocultación, clases, herencia y polimorfismo.
  • Eiffel: Lenguaje nuevo completamente orientado a objetos, permite la definición de clases genéricas, herencia múltiple y polimorfismo.
  • Lisp: Precursor de los lenguajes funcionales, los datos se organizan en listas que se manejan con funciones, normalmente recursivas.
  • Prolog: Representante de los lenguajes lógicos, se utiliza en la construcción de sistemas expertos.

Lenguajes de Cuarta Generación

Mayor nivel de abstracción, normalmente no son de propósito general, no son aconsejables para desarrollar aplicaciones complejas, por lo ineficiente del código que generan. Según su aplicación concreta son:

  • Bases de Datos: El mismo usuario genera sus informes, listados, resúmenes cuando los necesita.
  • Generadores de Programas: Se pueden construir elementos abstractos fundamentales en un cierto campo de aplicación sin descender a los detalles concretos que se necesitan en los lenguajes de tercera generación. La mayoría generan aplicaciones de gestión en Cobol, y últimamente se han desarrollado herramientas CASE para diseño orientado a objetos, que se pueden utilizar para aplicaciones de sistemas y que generan programas en C, C++, o Ada.
  • Cálculo: Hojas de cálculo, simulación y diseño para control, etc.
  • Otros: Herramientas para la especificación y verificación formal de programas, lenguajes de simulación, lenguajes de prototipos.

Prestaciones de los Lenguajes

Estructuras de Control

Programación Estructurada: Secuencia, selección general, selección por casos, repetición, bucle con contador, bucle indefinido, definición de subprogramas mediante funciones y procedimientos, normalmente se pueden definir en forma recursiva, siendo más claros y sencillos.

Manejo de Excepciones: Son errores o sucesos inusuales:

  • Errores humanos: Se pueden introducir datos que provoquen una división por cero.
  • Fallos de hardware: Mal funcionamiento o capacidad limitada de los recursos.
  • Errores de software: Defecto del software no detectado en las pruebas.
  • Datos de entrada vacíos.
  • Valores fuera de rango.

La medida correctora del sistema operativo es siempre abortar el programa para evitar males mayores, pero esto normalmente no es aceptable y lo que se hace es que se desvía la ejecución del programa, hacia un fragmento de código apropiado.

En Ada hay unos tratamientos predefinidos para unas excepciones predefinidas. El tratamiento analiza la causa de la excepción, la registra, y recupera el estado del sistema para que pueda continuar su ejecución. En Ada, después de la ejecución del tratamiento se abandona por completo la ejecución de la unidad. En PL/1 se reanuda la ejecución inmediatamente después del punto en el que se disparó la excepción. Cuando se produce la excepción en una unidad que no tiene programado su tratamiento, ésta se propaga a la unidad que la llamó. Esta propagación continúa hasta alcanzar la unidad externa y ejecutar el tratamiento predefinido si no existe otro.

Concurrencia: Cada lenguaje la aborda de una manera diferente:

  • Corrutinas: Las tareas son corrutinas que son semejantes a subprogramas, y entre ellas se pasan el control de ejecución. Se utilizan en Modula-2.
  • Fork-Join: Una tarea puede arrancar la ejecución concurrente de otras mediante una orden fork, la concurrencia se finaliza con un join invocado por la misma tarea que ejecutó el fork, y con el que ambas tareas se funden en una única. Se utiliza en UNIX y en el lenguaje PL/1.
  • Cobegin-Coend: Todas las tareas que se deben ejecutar concurrentemente se declaran dentro de cobegin-coend. Se finaliza la concurrencia cuando todas las tareas han acabado. Se utiliza en Algol68.
  • Procesos: Cada tarea se declara como un proceso. Todos los procesos declarados se ejecutan concurrentemente desde el comienzo del programa, y no es posible iniciar uno nuevo. Se utiliza en Pascal concurrente. En Ada sí se puede lanzar un proceso en cualquier momento.

Para lograr la sincronización y cooperación entre las tareas disponemos de:

  • Variables compartidas: Ejecutándose en el mismo computador (multiprogramación), o en distintos computadores pero utilizando una memoria compartida (multiproceso), son: Semáforos, regiones críticas condicionales, monitores.
  • Paso de mensajes: Ejecutándose en computadores que no tienen ninguna memoria común (procesos distribuidos), que están en una red lo que posibilita el paso de mensajes. Communicating Sequential Processes (CSP), llamadas a procedimientos remotos, Rendezvous de Ada.

Estructuras de Datos

Datos simples: Enteros, reales, enumerado, los datos de tipo subrango permiten acotar el rango de un tipo de dato ordinal para crear un nuevo tipo de dato.

Datos compuestos: Formaciones, registros, matrices, vectores, datos dinámicos (punteros).

Constantes: Ya se pueden declarar constantes simbólicas con nombre. En Modula-2 no se pueden declarar constantes de tipo formación o registro, aunque sí en Ada.

Comprobación de tipos: Las operaciones que se pueden realizar con los datos de un programa dependen del nivel de comprobación de tipos, hay 5 niveles:

  • Nivel 0 (Sin tipos): Aquí están los lenguajes en los que no se pueden declarar nuevos tipos de datos, son Basic, Cobol, Lisp, Apl.
  • Nivel 1 (Tipado automático): El compilador decide qué tipo es el más adecuado para cada dato, cambiándolo si es necesario.
  • Nivel 2 (Tipado débil): También la conversión es automática pero solo entre tipos similares.
  • Nivel 3 (Tipado semirrígido): Los datos se deben declarar previamente, pero la vía de escape es la compilación separada, así puede ser distinto lo que se declara y compila en un módulo, de lo que se compila y declara en otro módulo. Así es Pascal.
  • Nivel 4 (Tipado fuerte): No hay escape, el programador debe hacer cualquier conversión de tipos, la comprobación de tipos se realiza en compilación, carga y ejecución. Así son Ada y Modula-2.

Abstracciones y Objetos

Abstracciones funcionales: En todos los lenguajes se tiene que definir el nombre y la interfaz de la abstracción, se tiene que codificar la operación que realiza y disponer de un mecanismo para la utilización de la abstracción. Normalmente en todos los lenguajes está oculta la codificación de la operación, para quien hace uso de la misma. Pascal, Modula-2, Ada tienen visibilidad por bloques, esto es, pueden consultar o modificar datos de bloques externos. En Fortran hay total opacidad de dentro hacia fuera e viceversa.

Tipos abstractos de datos: Se deben agrupar tanto el contenido (atributos de los datos de la abstracción) y las operaciones para el manejo del contenido, y debe haber un mecanismo de ocultación que impida acceder al contenido por otra vía diferente.

Objetos: Modula-2 no dispone de mecanismos de herencia ni polimorfismo, así es complicado usarlo en diseño orientado a objetos. Ada solo dispone de polimorfismo de sobrecarga, pero no de anulación ni diferido, con lo que también es complicado. Los que sí son aptos son: Smalltalk, Object Pascal, Eiffel, Objective-C, C++.

Modularidad

La primera cualidad que se exige es la compilación separada, que permite preparar y compilar separadamente el código de cada módulo. Además, diremos que la compilación es segura si en tiempo de compilación es posible comprobar que el uso de un elemento es consistente con su definición. Fortran y Pascal tienen compilación no segura. Ada y Modula-2 tienen compilación segura, en Modula-2 definimos la interfaz con DEFINITION MODULE y la codificamos con IMPLEMENTATION MODULE. En Ada con package y package body. En C ANSI el compilador, la compilación no será del todo segura pues hay posibilidad de error si no coincide la interfaz del módulo con la copia usada en el programa.

Criterios de Selección del Lenguaje

  • Imposición del cliente.
  • Tipo de aplicación: Cada día aparecen nuevos lenguajes asociados a un campo de aplicación concreta. Solo se podría usar ensambladores para partes de código para hardware muy especial.
  • Disponibilidad y entorno: Ver qué compiladores existen para el computador elegido. El desarrollo será más sencillo cuanto más potentes sean las herramientas elegidas.
  • Experiencia previa.
  • Reusabilidad: Es interesante conocer las librerías disponibles y tener herramientas que organicen las mismas.
  • Transportabilidad: Está ligada a que exista un estándar del lenguaje que se pueda adoptar en todos los compiladores.
  • Uso de varios lenguajes: No es aconsejable, pero hay veces en que es más sencillo de codificar en diferentes lenguajes.

Aspectos Metodológicos

Normas y Estilo de Codificación

Se deben fijar normas concretando un estilo de codificación, para que todo el equipo lo adopte y respete, en el que se deberán concretar lo siguiente:

  • Formato y contenido de las cabeceras de cada módulo.
  • Formato y contenido para cada tipo de comentario.
  • Utilización del encolumnado.
  • Elección de nombres.

Además hay que fijar las siguientes restricciones:

  • Tamaño máximo de subrutinas.
  • Tamaño máximo de argumentos en subrutinas.
  • Evitar anidamiento excesivo.

Manejo de Errores

Defecto: Errata de software.

Fallo: Un elemento del programa no funciona correctamente, produciendo un resultado (parcial) erróneo.

Error: Estado inadmisible de un programa al que se llega como consecuencia de un fallo.

Actitudes respecto al tratamiento de errores son:

  • No considerar los errores: Se exige que los datos introducidos sean correctos y que el programa no tenga ningún defecto.
  • Prevención de errores: Es la programación a la defensiva en que el programa desconfía sistemáticamente de los datos o argumentos introducidos. Se evitan resultados incorrectos a base de no producir resultados en caso de fallo. Una ventaja es que se evita la propagación de errores, facilitando el diagnóstico de los defectos.
  • Recuperación de errores: Se trata de restaurar el programa en un estado correcto y evitar que el error se propague, para esto se hacen dos cosas:
    • Detección del error: Hay que concretar qué situaciones se consideran erróneas y realizar las comprobaciones adecuadas en ciertos puntos del programa.
    • Recuperación del error: Tenemos:
      • Recuperación hacia adelante: Se identifica la naturaleza y el tipo de error y se toman las acciones que corrijan el estado del programa y pueda continuar su ejecución. Este esquema se puede programar mediante el mecanismo de excepciones.
      • Recuperación hacia atrás: Corregir el estado del programa a un estado anterior a la aparición del error. Esta es la forma de operar de los sistemas basados en transacciones, los esquemas de transacciones mantienen la consistencia en las bases de datos.

Los sistemas que tienen una previsión o recuperación de errores se llaman tolerantes a fallos.

Aspectos de Eficiencia

Con la potencia actual no se debe sacrificar la claridad en la codificación por una mayor eficiencia.

Eficiencia en memoria: Resulta suficiente recurrir a un compilador con posibilidades de compresión de memoria, y adopción de algoritmos eficientes.

Eficiencia en tiempo: Sobre todo en sistemas de tiempo real, adopción de algoritmos eficientes y haciendo a veces un perjuicio a la eficiencia en memoria y utilizando técnicas de codificación como: tabular cálculos complejos, empleo de macros, desplegado de bucles, sacar fuera de los bucles lo que no haya que repetir, evitar operaciones en coma flotante, etc.

Transportabilidad del Software

Tiene como factores la utilización de estándares y aislar las peculiaridades destinando un módulo específico para cada una de ellas.

Técnicas de Pruebas de Unidades

Para un software crítico, el costo de las pruebas puede ser la partida más importante. Se deben hacer pruebas por módulo, pruebas de integración y pruebas del sistema total.

Objetivos de las Pruebas de Software

Conseguir que el programa funcione incorrectamente y se descubran los defectos, se pretende que con el mínimo esfuerzo posible se detecten el mayor número posible de defectos. La prueba ha de ser automática exigiendo así crear un entorno de prueba que asegure unas condiciones predefinidas que se restablezcan a cada pasada. Tenemos pruebas de caja negra y pruebas de caja transparente.

Pruebas de Caja Negra

Es la comprobación de la especificación entrada-salida del software. Es la única estrategia que puede adoptar el cliente. Hay que elegir un interrogatorio amplio y coherente, los métodos que ayudan en la elaboración de casos de prueba son:

  • Partición en clases de equivalencia: Se determinan las clases y se establecen las pruebas para cada clase.
  • Análisis de valores límite: Hace un especial hincapié en las zonas del espacio de ejecución que están próximas al borde, los errores en los límites pueden ser graves al solicitar recursos que no se habían preparado.
  • Comparación de versiones: Se hacen diferentes versiones del mismo programa hechas por diferentes programadores y se someten al mismo juego de pruebas, se comparan y evalúan los resultados, cuando produzcan los mismos resultados podemos elegir cualquier versión. Esto no es infalible pues un error en la especificación lo arrastrarían todas las versiones.
  • Empleo de la intuición: Las personas ajenas al desarrollo del módulo suelen aportar un punto de vista más distante y fresco.

Pruebas de Caja Transparente

Ahora se conoce y se tiene en cuenta la estructura interna del módulo. Se trata de conseguir que el programa transite por todos los posibles caminos de ejecución y ponga en juego todos los elementos del código. Si solo efectuamos pruebas de caja negra quedarán inexplorado multitud de caminos. Las pruebas de caja negra y caja transparente deben ser complementarias y nunca excluyentes. Los métodos son:

  • Cubrimiento lógico: Se determina un conjunto de caminos básicos que recorran las líneas del flujo del diagrama alguna vez, que vendrá determinado por el número de predicados simples con arreglo a la siguiente fórmula: Nº máximo de caminos = Nº de predicados + 1. Tenemos diferentes niveles de cubrimiento:
    • Nivel 1: Se elaboran casos de prueba para que se ejecuten al menos una vez todos los caminos básicos, cada uno de ellos por separado.
    • Nivel 2: Se elaboran casos de prueba para que se ejecuten todas las combinaciones de caminos básicos por parejas.
    • Nivel 3: Se elaboran casos de prueba un número significativo de las combinaciones posibles de caminos.

Como mínimo las pruebas de cada módulo deben garantizar el nivel 1. Nunca se podrá detectar la falta de un fragmento de código.

  • Prueba de bucles: Se deben elaborar pruebas para:
    • Bucle con número no acotado de repeticiones: Ejecutar el bucle 0, 1, 2, algunas, muchas veces.
    • Bucle con número máximo (M) de repeticiones: Ejecutar el bucle 0, 1, 2, algunas, M-1, M, M+1 veces.
    • Bucles anidados: Ejecutar todos los bucles externos en su número mínimo de veces para probar el bucle más interno. Para el siguiente nivel de anidamiento, ejecutar los bucles externos en su número mínimo de veces, y los bucles internos un número típico de veces. Repetir así hasta completar todos los niveles.
    • Bucles concatenados: Si son independientes se probarán por separado con los criterios anteriores.
  • Empleo de la intuición.

Estimación de Errores No Detectados

Resulta imposible demostrar que un módulo no tiene defectos. Para estimar los defectos que quedan sin detectar procedemos así:

  1. Anotar el número de errores que se producen inicialmente con el juego de casos de prueba.
  2. Corregir el módulo hasta que no tenga ningún error con el mismo juego de casos de prueba.
  3. Introducir un número determinado de errores en el módulo.
  4. Someter al módulo con errores al juego de casos de prueba y contar los errores que se detectan.
  5. Suponiendo la misma proporción, el porcentaje de errores sin detectar será el mismo para los errores iniciales que para los deliberados.

Estrategias de Integración

Se deben depurar los errores que se producen al integrar los diferentes módulos. Tenemos varias maneras:

  • Integración Big Bang: Se realiza la integración en un único paso.
  • Integración descendente: Tenemos un módulo principal que se prueba con módulos de andamiaje, que más tarde se van sustituyendo uno por uno por los verdaderos, realizando las pruebas de integración. Los sustitutos deben ser los más simples posible. La ventaja es que se ven desde el principio las posibilidades de la aplicación, sus inconvenientes son que limita tanto el trabajo en paralelo como el ensayo de situaciones especiales.
  • Integración ascendente: Se codifica por separado y en paralelo todos los módulos de nivel más bajo, para probarlos se escriben módulos gestores que los hacen funcionar independientemente o en combinaciones sencillas. Tiene como ventajas que se facilita el trabajo en paralelo y facilita el ensayo de situaciones especiales. Su inconveniente es que es difícil ensayar el funcionamiento global hasta el final de su integración.

La mejor solución es usar integración ascendente con los módulos de nivel más bajo y descendente con los de nivel más alto, lo que se llama integración sandwich.

Pruebas de Sistema

Son pruebas de caja negra al sistema completo, tenemos las siguientes clases:

Objetivo de las Pruebas

  • De recuperación: Comprobar la capacidad del sistema para recuperarse ante fallos.
  • De seguridad: Comprobar el mecanismo de protección contra un acceso no autorizado.
  • De resistencia: Comprobar el comportamiento ante situaciones excepcionales.
  • De sensibilidad: Comprobar el tratamiento que da el sistema a ciertas singularidades relacionadas con los algoritmos utilizados.
  • De rendimiento: Comprobar las prestaciones del sistema que son críticas en tiempo.

Pruebas Alfa

Son las primeras que se realizan en un entorno controlado, donde el usuario tiene el apoyo de alguna persona del equipo de desarrollo.

Pruebas Beta

Uno o varios usuarios trabajan con el sistema en su entorno normal, sin apoyo de nadie, anotando cualquier problema que se presente.

Entradas relacionadas: