Tendencias en ingeniería del software
Las generaciones de los lenguajes de programación. Cuando se mira hacia atrás en la re- lativamente breve pero ajetreada historia de la ingeniería del software, no puede evitarse el apreciar dos amplias tendencias:
Orient a ción a O bje tO s. t eO ría y pr á c tic
a ❚❚ El desplazamiento del centro de atención de la programación al por menor a la progra-
mación al por mayor. ❚
❚ La evolución de los lenguajes de alto nivel.
La mayoría de los nuevos sistemas de software de dimensión industrial son más grandes y más complejos que sus predecesores de pocos años antes. Este crecimiento de la complejidad ha promovido una cantidad significativa de investigación aplicada útil en ingeniería del software, particularmente en lo referente a descomposición, abstracción y jerarquía. El desarrollo de len- guajes de programación más expresivos ha completado estos avances. La tendencia ha sido un desplazamiento desde los lenguajes que dicen al computador qué hacer, o lenguajes imperativos, hacia lenguajes que describen las abstracciones clave en el dominio del problema (lenguajes declarativos). Wegner ha clasificado algunos de los lenguajes de programación de alto nivel más populares en generaciones, dispuestas de acuerdo con las características que tales lenguajes fue- ron pioneros en presentar:
❚
❚ Lenguajes de primera generación (1954-1958):
FORTRAN I Expresiones matemáticas.
ALGOL 58 Expresiones matemáticas.
Flowmatic Expresiones matemáticas.
IPL V Expresiones matemáticas.
❚
❚ Lenguajes de segunda generación (1959-1961):
FORTRAN II Subrutinas, compilación separada.
ALGOL 60 Estructura en bloques, tipos de datos.
COBOL Descripción de datos, manejo de ficheros.
Lisp Procesamiento de listas, punteros, recolección de basura.
❚
❚ Lenguajes de tercera generación (1962-1970):
PL/1 FORTRAN + ALGOL + COBOL.
ALGOL 68 Sucesor riguroso del ALGOL 60.
Pascal Sucesor sencillo del ALGOL 60.
Simula Clases, abstracción de datos.
❚
❚ El hueco generacional (1970-1980):
Se inventaron muchos lenguajes diferentes, pero pocos perduraron [2].
En generaciones sucesivas, el tipo de mecanismo de abstracción que admitía cada lenguaje fue cambiando. Los lenguajes de la primera generación se utilizaron principalmente para aplicacio- nes científicas y de ingeniería, y el vocabulario de estos dominios de problema fue matemático casi por completo. Así, los lenguajes como el FORTRAN I se desarrollaron para que el pro- gramador pudiera escribir fórmulas matemáticas, liberándole de esta forma de algunas de las complicaciones del lenguaje ensamblador o del lenguaje máquina. Esta primera generación de
lenguajes de alto nivel representó por lo tanto un paso de acercamiento al espacio del problema, y un paso de alejamiento de la máquina que había debajo. Entre los lenguajes de segunda gene- ración, el énfasis se puso en las abstracciones algorítmicas. Por esta época, las máquinas eran cada vez más y más potentes, y el abaratamiento en la industria de los computadores significó que po- día automatizarse una mayor variedad de problemas, especialmente para aplicaciones comercia- les. En este momento, lo principal era decirle a la máquina lo que debía hacer: lee primero estas fichas personales, ordénalas después, y a continuación imprime este informe. Una vez más, esta nueva generación de lenguajes de alto nivel acercaba a los desarrolladores un paso hacia el espa- cio del problema y los alejaba de la máquina subyacente. A finales de los sesenta, especialmente con la llegada de los transistores y la tecnología de circuitos integrados, el costo del hardware de los computadores había caído de forma dramática, pero su capacidad de procesamiento había crecido casi exponencialmente. Ahora podían resolverse problemas mayores, pero eso exigía la manipulación de más tipos de datos. Así, lenguajes como el ALGOL 60 y posteriormente el Pascal evolucionaron soportando abstracción de datos. El programador podía describir el signifi- cado de clases de datos relacionadas entre sí (su tipo) y permitir que el lenguaje de programación apoyase estas decisiones de diseño. Esta generación de lenguajes acercó de nuevo el software un paso hacia el dominio del problema, y lo alejó otro paso de la máquina.
Los setenta ofrecieron un frenesí de actividad investigadora en materia de lenguajes de pro- gramación, con el resultado de la creación de literalmente dos millares de diferentes lenguajes con sus dialectos. En gran medida, la tendencia a escribir programas más y más grandes puso de manifiesto las deficiencias de los lenguajes más antiguos; así, se desarrollaron muchos nue- vos mecanismos lingüísticos para superar estas limitaciones. Solo algunos de estos lenguajes han sobrevivido (¿acaso ha visto el lector libros recientes sobre los lenguajes Fred, Chaos o Tranquil?); sin embargo, muchos de los conceptos que introdujeron encontraron su camino en sucesores de los lenguajes anteriores. Así, existen Smalltalk (un sucesor revolucionario de Simula), Ada (un sucesor del ALGOL 68 y Pascal, con contribuciones de Simula, Alphard y CLU), CLOS (que surgió del Lisp, LOOPS y Flavors), C++ (derivado de un matrimonio entre C y Simula), y Eiffel (derivado de Simula y Ada). Lo que resulta del mayor interés para nosotros es la clase de lenguajes que suelen llamarse basados en objetos y orientados a objetos. Los lenguajes de programación basados en y orientados a objetos son los que mejor soportan la descomposición orientada a objetos del software.
La topología de los lenguajes de primera y principios de la segunda generaciones. Para ilustrar con precisión lo que se quiere decir, pasamos a estudiar la estructura de cada genera- ción de lenguajes de programación. En la figura 2.1, se ve la topología de la mayoría de los lenguajes de programación de la primera generación e inicios de la segunda. Por topología se entiende los bloques físicos básicos de construcción de ese lenguaje y cómo esas partes pueden ser conectadas. En esta figura se aprecia que, para lenguajes como FORTRAN y COBOL, el bloque básico de construcción de todas las aplicaciones es el subprograma (o párrafo, para quie- nes hablen COBOL). Las aplicaciones escritas en estos lenguajes exhiben una estructura física
Orient a ción a O bje tO s. t eO ría y pr á c tic a
relativamente plana, consistente solo en datos globales y subprogramas. Las flechas de la figura indican dependencias de los subprogramas respecto a varios datos. Durante el diseño pueden separarse lógicamente diferentes clases de datos, pero existen pocos elementos en estos lenguajes para reforzar estas decisiones. Un error en una parte de un programa puede tener un devastador efecto de propagación a través del resto del sistema, porque las estructuras de datos globales están expuestas al acceso de todos los subprogramas. Cuando se realizan modificaciones en un sistema grande, es difícil mantener la integridad del diseño original. Frecuentemente aparece la entropía: después de un periodo de mantenimiento aun cuando sea breve, un programa escrito en estos lenguajes suele contener una enorme cantidad de acoplamientos entre subprogramas, con significados implícitos de los datos y flujos de control retorcidos, amenazando la fiabilidad de todo el sistema y, por supuesto, reduciendo la claridad global de la solución.
La topología de los lenguajes de fines de la segunda generación y principios de la terce- ra. A mediados de los sesenta se reconoció finalmente a los programas como puntos inter- medios importantes entre el problema y el computador [3]. Según apunta Shaw, “la primera abstracción del software, ahora llamada abstracción ‘procedimental’, creció directamente de esta visión pragmática del software... Los subprogramas se inventaron antes de 1950, pero en aquel momento no se les apreció como abstracciones en toda la extensión de la palabra... En vez de eso, originalmente se les vio como dispositivos para ahorrar trabajo... Aunque rápidamente los subprogramas fueron considerados como una forma de abstraer funciones del programa” [4]. El hallazgo de que los subprogramas podían servir como un mecanismo de abstracción tuvo tres consecuencias importantes. Primero, se inventaron lenguajes que soportaban una variedad de mecanismos de paso de parámetros. Segundo, se asentaron los fundamentos de la programación estructurada, puestos de manifiesto en la capacidad de los lenguajes para anidar subprogramas y el desarrollo de teorías sobre estructuras de control y ámbito y visibilidad de las declaraciones.
Datos
Subprogramas
Figura 2.1. La topología de los lenguajes de programación de primera generación y principios de la
Tercero, surgieron los métodos de diseño estructurado, que ofrecían una guía a los diseñadores que intentaban construir grandes sistemas utilizando los subprogramas como bloques físicos básicos de construcción. Así no es extraño, tal y como muestra la figura 2.2, que la topología de los lenguajes de finales de la segunda generación y principios de la tercera sea en gran medida una variación sobre el mismo tema de generaciones anteriores. Esta topología se enfrenta a al- gunos de los defectos de lenguajes precedentes, es decir, la necesidad de tener un mayor control sobre las abstracciones algorítmicas, pero sigue fallando a la hora de superar los problemas de la programación a gran escala y del diseño de datos.
La topología de los lenguajes de finales de la tercera generación. Comenzando por el FORTRAN II, y apareciendo en la mayoría de los lenguajes de fines de la tercera generación, surgió otro importante mecanismo estructurador para resolver los problemas crecientes de la programación a gran escala. Proyectos de programación mayores significaban equipos de desa- rrollo mayores y, por tanto, la necesidad de desarrollar independientemente partes diferentes del mismo programa. La respuesta a esta necesidad fue el módulo compilado separadamente, que en su concepción más temprana no era mucho más que un contenedor arbitrario para da- tos y subprogramas, como se ve en la figura 2.3. Los módulos no solían reconocerse como un mecanismo de abstracción importante; en la práctica se utilizaban simplemente para agrupar subprogramas de los que cabía esperar que cambiasen juntos. La mayoría de los lenguajes de esta generación, aunque admitían alguna clase de estructura modular, tenían pocas reglas que exigie- sen una consistencia semántica entre las interfaces de los módulos. Un desarrollador que escribía un subprograma para un módulo podía asumir que sería llamado con tres parámetros diferentes: un número en coma flotante, una matriz de diez elementos y un entero que representase un indicador de tipo booleano. En otro módulo, una llamada a este subprograma podía utilizar parámetros actuales incorrectos que violasen esas suposiciones: un entero, una matriz de cinco
Datos
Subprogramas
Figura 2.2. La topología de los lenguajes de programación de finales de la segunda generación y principios de
Orient a ción a O bje tO s. t eO ría y pr á c tic a
elementos y un número negativo. Del mismo modo, un módulo podía utilizar un bloque de datos comunes que asumiese como propio, y otro módulo violar esas suposiciones manipulando esos datos directamente. Desafortunadamente, al tener la mayoría de estos lenguajes un soporte desastroso para la abstracción de datos y la comprobación estricta de tipos, estos errores solo podían detectarse durante la ejecución del programa.
La topología de los lenguajes de programación basados en objetos y orientados a objetos. La importancia de la abstracción de datos para dominar la complejidad está claramente estable- cida por Shankar: “La naturaleza de las abstracciones que pueden lograrse mediante el uso de procedimientos es adecuada para la descripción de operaciones abstractas, pero no es particular- mente buena para la descripción de objetos abstractos. Este es un serio inconveniente, porque en muchas aplicaciones la complejidad de los objetos de datos que hay que manipular contribuye sustancialmente a la complejidad global del problema” [5]. Este hallazgo tuvo dos consecuencias importantes. Primero, surgieron los métodos de diseño dirigido por los datos, que proporciona- ron una aproximación disciplinada a los problemas de realizar abstracciones de datos en lengua- jes orientados algorítmicamente. Segundo, aparecieron teorías acerca del concepto de tipo, que finalmente encontraron realización en lenguajes como Pascal.
La conclusión natural de estas ideas apareció primero en el lenguaje Simula y fue mejo- rando durante el periodo de vacío generacional de los lenguajes, culminando en el desarrollo de varios lenguajes como Smalltalk, Object Pascal, C++, CLOS, Ada y Eiffel. Por razones que se explicarán brevemente, estos lenguajes reciben el nombre de basados en objetos y orientados
a objetos. La figura 2.4 ilustra la topología de estos lenguajes para aplicaciones de tamaño
pequeño y moderado. El bloque físico de construcción en estos lenguajes es el módulo, que representa una colección lógica de clases y objetos en lugar de subprogramas, como ocurría
Módulos
Subprogramas Datos
en lenguajes anteriores. En otras palabras, “si los procedimientos y funciones son verbos y los elementos de datos son nombres, un programa orientado a procedimientos se organiza alre- dedor de los verbos, mientras que un programa orientado a objetos se organiza alrededor de los nombres” [6]. Por esta razón, la estructura física de una aplicación orientada a objetos de tamaño pequeño a moderado tiene el aspecto de un grafo, no el de un árbol, lo que es típico en lenguajes orientados algorítmicamente. Además, existen pocos o ningún dato global. En vez de eso, los datos y las operaciones están unidos de tal modo que los bloques lógicos de construcción fundamentales de estos sistemas ya no son algoritmos, sino clases y objetos.
Al llegar aquí ya se ha progresado más allá de la programación a gran escala y hay que en- frentarse a la programación a escala industrial. En sistemas muy complejos se encuentra que las clases, objetos y módulos proporcionan un medio esencial, pero, aun así, insuficiente de abstracción. Afortunadamente, el modelo de objetos soporta el aumento de escala. En sistemas grandes existen agrupaciones de abstracciones que se construyen en capas, una sobre otra. A cualquier nivel de abstracción se encuentran colecciones significativas de objetos que colabo- ran para lograr algún comportamiento de nivel superior. Si se examina cualquier agrupación
Figura 2.4. La topología de las aplicaciones de tamaño pequeño a moderado que utilizan lenguajes basados
Orient a ción a O bje tO s. t eO ría y pr á c tic a
determinada para ver su implantación, se desvela un nuevo conjunto de abstracciones coope- rando. Esta es exactamente la organización de la complejidad descrita en el capítulo 1; esta topología se muestra en la figura 2.5.
Fundamentos del modelo de objetos
Los métodos de diseño estructurado surgieron para guiar a los desarrolladores que intentaban construir sistemas complejos utilizando los algoritmos como bloques fundamentales para su construcción. Análogamente, los métodos de diseño orientados a objetos han surgido para ayudar a los desarrolladores a explotar la potencia expresiva de los lenguajes de programación basados en objetos y orientados a objetos, utilizando las clases y los objetos como bloques básicos de construcción.
En realidad, el modelo de objetos ha recibido la influencia de una serie de factores, no solo de la programación orientada a objetos. Por contra, el modelo de objetos ha demostrado ser un concepto unificador en la informática, aplicable no solo a los lenguajes de programación, sino también al diseño de interfaces de usuario, bases de datos e incluso arquitecturas de computa- dores. La razón para este gran atractivo es simplemente que una orientación a objetos ayuda a combatir la complejidad inherente a muchos tipos de sistema diferentes.
Figura 2.5. La topología de las aplicaciones a gran escala que utilizan lenguajes basados en objetos y
El diseño orientado a objetos representa así un desarrollo evolutivo, no revolucionario; no rompe con los avances del pasado, sino que se basa en avances ya probados. Desgraciadamente, hoy en día la mayoría de los programadores han sido educados formal e informalmente solo en los principios del diseño estructurado. Por supuesto, muchos buenos ingenieros han desarrollado y puesto en acción innumerables sistemas de software utilizando estas técnicas. Sin embargo, existen límites para la cantidad de complejidad que se puede manejar utilizando solo descompo- sición algorítmica; por tanto hay que volverse hacia la descomposición orientada a objetos. Es más, si se intenta utilizar lenguajes tales como C++ y Ada como si fuesen lenguajes tradicionales orientados algorítmicamente, no solo se pierde la potencia de la que se disponía, sino que ha- bitualmente se acaba en una situación peor que si se hubiese utilizado un lenguaje más antiguo como C o Pascal. Ofrézcase una taladradora eléctrica a un carpintero que no sabe nada de la electricidad, y la utilizará como un martillo. Acabará por torcer los clavos y romperse algunos dedos, porque una taladradora eléctrica es un martillo desastroso.
POO, DOO y AOO
1Dado que el modelo de objetos se deriva de fuentes tan dispersas, desgraciadamente ha venido acompañado por un embrollo terminológico. Un programador de Smalltalk utiliza métodos, un programador de C++ utiliza funciones miembro virtuales, y un programador de CLOS utiliza fun-
ciones genéricas. Un programador de Object Pascal habla de coerción o conversión forzada de tipos;
un programador de Ada llama a lo mismo una conversión de tipos. Para minimizar la confusión, se definirá qué es orientado a objetos y qué no lo es. El glosario ofrece un resumen de todos los términos descritos aquí, y algunos más.
Bhaskar ha observado que la expresión orientado a objetos “se ha esgrimido a diestro y sinies- tro de forma indiscriminada y con la misma reverencia que se guarda hacia ‘maternidad’, ‘tarta de manzana’ y ‘programación estructurada’” [7]. En lo que se puede estar de acuerdo es en que el concepto de objeto es central en cualquier cosa orientada a objetos. En el capítulo anterior, se definió informalmente un objeto como una entidad tangible que muestra algún comportamien- to bien definido. Stefik y Bobrow definen los objetos como “entidades que combinan las propie- dades de los procedimientos y los datos en el sentido de que realizan computaciones y conservan el estado local” [8]. Definir a los objetos como entidades invita a la pregunta, pero el concepto básico aquí es que los objetos sirven para unificar las ideas de las abstracciones algorítmica y de datos. Jones clarifica más este término reparando en que “en el modelo de objetos, se pone el én- fasis en caracterizar nítidamente los componentes del sistema físico o abstracto que se pretende modelar con un sistema programado... Los objetos tienen una cierta ‘integridad’ que no debería –de hecho, no puede– ser violada. Un objeto solo puede cambiar de estado, actuar, ser manipu- lado o permanecer en relación con otros objetos de maneras apropiadas para ese objeto. Dicho
1 Las siglas en lengua inglesa OOP, OOD y OOA también se utilizan frecuentemente en los círculos informáticos,
Orient a ción a O bje tO s. t eO ría y pr á c tic
a de otro modo, existen propiedades invariantes que caracterizan un objeto y su comportamiento.
Un ascensor, por ejemplo, se caracteriza por propiedades invariantes que incluyen que solo se desplaza arriba y abajo por su hueco... Cualquier simulación de un ascensor debe incorporar estos invariantes, porque son intrínsecos al concepto de ascensor” [9].
Fundamentos del modelo de objetos
Como apuntan Yonezawa y Tokoro, “El término ‘objeto’ surgió casi independientemente en varios campos de la informática, casi simultáneamente a principios de los setenta, para refe- rirse a nociones que eran diferentes en su apariencia, pero relacionadas entre sí. Todas estas nociones se inventaron para manejar la complejidad de sistemas de software de tal forma que los objetos representaban componentes de un sistema descompuesto modularmente o bien unidades modulares de representación del conocimiento” [10]. Levy añade que los siguientes sucesos contribuyeron a la evolución de conceptos orientados a objetos:
❚
❚ Avances en la arquitectura de los computadores, incluyendo los sistemas de capacidades y el apoyo en hardware para conceptos de sistemas operativos.
❚
❚ Avances en lenguajes de programación, como se demostró en Simula, Smalltalk, CLU y Ada.
❚
❚ Avances en metodología de la programación, incluyendo la modularización y la oculta-