|
Se discute qué es Abstracción de Datos y Programación por Objetos, usando como base el lenguaje Pascal. |
La ingeniería de sistemas de información ha progresado gracias a la introducción de nuevas técnicas para construir programas. El primer gran avance lo constituye la programación modular, que complementa a la programación estructurada. Otros logros lo constituyen los llamados lenguajes de cuarta generación, que permiten desarrollar sistemas de información en un lenguaje muy especializado.
Pero conforme avanza la tecnología de computadores es necesario crear programas cada vez más sofisticados. En la última década ha tomado gran auge el uso de la programación por objetos, que tiene como requisito la abstracción de datos, y que implica una nueva manera de diseñar programas. El lenguaje Smalltalk [GR-83] ha causado gran revuelo, hasta tal punto que existen compiladores para lenguajes como C++ [Str-86], que han sacado la programación por objetos de los laboratorios de investigación, para ser utilizados en el quehacer diario de las empresas. (Una excelente discusión de los alcances de la programación por objetos puede encontrarse en [Str-88]).
Fundamentalmente, la programación estructurada y la programación modular consisten en el uso de abstracción de procedimientos. La idea es meter un conjunto de instrucciones en un módulo, que tiene una interfaz claramente definida. Mediante la abstracción de procedimientos el programador separa claramente el cómo del qué: una subrutina contiene la lógica necesaria para ejecutar una tarea, y sus parámetros definen los objetos sobre los que trabaja.
Además, la programación estructurada requiere que el
programador use las construcciones IF-THEN-ELSE
,
WHILE-REPEAT
y CASE
. Pero principalmente
un programa estructurado es uno que no utiliza el ya bastante
difamado GOTO
para alterar la secuencia de
ejecución del programa.
Los primeros lenguajes de programación (Fortran, Lisp, Basic y Cobol) no soportan adecuadamente abstracción de procedimientos. Primero, no incorporan las instrucciones de control mencionadas en el párrafo anterior. Segundo, aunque en ellos es posible definir argumentos para subrutinas, no es posible especificar sus tipos de datos, por lo que muchos errores de interfaz, que podrían ser detectados por el compilador, deben ser eliminados manualmente por el programador. Cuando estos lenguajes fueron definidos lo común era no especificar la interfaz entre módulos: hacerlo era visto por los programadores como un trabajo poco creativo y engorroso. Ellos se rehusaban a diseñar adecuadamente sus programas. La "documentación" era labor de funcionarios de poco sueldo, y se creía que lo más importante era escribir el código del programa.
Con el advenimiento de Algol, y luego de Pascal, poco a poco ha cambiado esta mala percepción sobre la especificación y documentación de programas. Con gran éxito se introdujo la verificación de tipos en los argumentos de cada rutina, como una herramienta para reducir el tiempo de desarrollo de un programa. Sin embargo, todavía no se reconocían las grandes ventajas de especificar procedimientos.
PROCEDURE Lea_Arbol_Menu( { EXPORT } { Adolfo Di Mare } {+} VAR f : TEXT; { archivo de lineas de menú } {+} mprin : Linea_P; { linea leída por el llamador } {-} VAR ultLn : Linea_P { puntero a la última línea leída } ); { RESULTADO Lee el archivo que describe los menúes de la aplicación. En ultLn se retorna un puntero al último ítem leído que no corresponde a mprin, sino que es hermano de mprin o tal vez hermano de algún ítem con nivel menor que el de mprin (ie, que corresponde a un menú más cercano al principal que mprin). } { REQUIERE mprin debe apuntar a un registro de menú, o mprim = NIL. El llamado a Lea_Arbol_Menu se debe hacer para leer todos los ítems del menú mprin. } |
Figura N° 1 |
---|
La Figura 1 es un ejemplo de la especificación de un procedimiento, escrito de acuerdo a las convenciones de programación definidas en [DiM-88]. Este es el típico procedimiento que encontramos en un programa. A diferencia de otros ejemplos, la especificación de este procedimiento es bastante completa: la mayoría de los programadores no se molestarían en escribir tanto para una "escuálida" subrutina.
En el procedimiento Lea_Arbol_Menu()
se oculta
cómo se logra el objetivo descrito en la cláusula
RESULTADO
de la especificación. Un
programador puede usar este procedimiento sin necesidad de conocer
el código fuente de la rutina, con sólo disponer de
una copia del código objeto producido al compilarla.
Obviamente, debe asegurarse de que la cláusula
REQUIERE
se cumpla de hecho, o su programa
estaría incorrecto. Las bibliotecas de programas
están formadas por procedimientos como éste.
Es un poco difícil entender lo que
Lea_Arbol_Menu()
hace. Por ejemplo, no se explica en
ningún lado qué es un menú, o una
Linea_P
(o sea, un puntero a una
Linea_T
). Es cierto que los identificadores son
significativos, pero eso no es suficiente para lograr incorporar
este procedimiento en un programa real. Hace falta mucho
más información. Las bibliotecas de programas
comerciales generalmente proveen información adicional al
programador, en la que se describe el contexto de uso de cada
procedimiento.
De este ejemplo se pueden sacar dos conclusiones: primero, que el no usar abstracción de procedimientos es contraproducente. Una rutina como ésta realmente ayuda a partir un problema complejo en partes manejables, lo que garantiza no sólo una conclusión rápida del proceso de programación, sino que además permite reutilizar programas en la forma de procedimientos. C [KR-86], el lenguaje usado en el sistema operativo UNIX [KP-87], ha logrado gran aceptación precisamente por contar con una amplia biblioteca de procedimientos disponible para el programador.
Lo segundo que se puede concluir es que no basta con describir lo que el subprograma hace para que sea posible utilizarlo. También es necesario describir los datos con que trabaja cada procedimiento. Esto se logra mediante la abstracción de datos.
La programación que utiliza abstracción de datos se basa en el hecho de que en un programa se deben integrar y combinar los tipos básicos de datos, como números y caracteres, para formar estructuras de datos más complejas y así representar información dentro del computador. En general existe una fuerte relación entre todos los datos manipulados por un programa, por lo que es conveniente que esa relación esté claramente especificada y controlada, de forma que cada parte del programa "vea" sólo lo que necesita [LG-86].
Esto último es muy importante para separar el programa en partes independientes, o módulos, evitando así que cambios en una parte produzcan errores en otras partes del programa. Por ejemplo, en un programa que usa varios arreglos y matrices para almacenar información, es frecuente que al aumentar el tamaño de una dimensión se olvide aumentar la de los demás arreglos, por lo que el mantenimiento del programa es más difícil. El objetivo perseguido al usar abstracción de datos es lograr aislar todas estas dependencias, de forma que los cambios puedan ser hechos con un mínimo de esfuerzo y en una forma localizada. En nada ayuda tener que buscar por todo el programa en qué lugar debe hacerse cada cambio.
También es importante especificar mediante la abstracción de datos qué es cada estructura de datos. Una lista, por ejemplo, es una estructura de datos que tiene un comportamiento muy bien definido: pueden insertársele nuevos elementos, recorrérsela, encontrar el primer y último elemento, etc. Un programador que use el tipo de datos Lista no debe necesitar descubrir de nuevo ese concepto: simplemente debe poderlo importar de una biblioteca.
Al implementar la Lista como un Tipo Abstracto de Datos (ADT: Abstract Data Type en inglés), el programador decide cuáles procedimientos se necesitan para manipular una lista, y define su interrelación. Un usuario del tipo de datos "lista" no necesitará entonces conocer cómo se interrelacionan (a nivel de implementación) los datos ni los procedimientos que manipulan listas pues le bastará usar las operaciones de la Lista para manejarla.
Al usar abstracción de datos el programador define cómo puede comportarse cada una de las variables de su programa. O sea que además de usar abstracción de procedimientos para construir el programa modularmente, deben especificarse las operaciones válidas sobre los datos. El objetivo es programar módulos que definan nuevos tipos de datos, y además que incluyan varios procedimientos (operaciones) que permitan manipularlos. Este dúo procedimiento-dato es un Tipo Abstracto de Datos. El programador-usuario del ADT podrá manipular las variables del tipo de datos únicamente por medio de sus procedimientos asociados. En un sólo módulo se "encapsula" el nuevo tipo de datos junto con sus procedimientos.
Al hablar de ADTs se dice que se usa Abstracción de Datos porque al definir un ADT el programdor especifica cuáles son las estructuras de datos que su programa utiliza. Se dice que se usa Encapsulamiento de Datos porque junto al tipo de datos también se definen las rutinas que permitirán utilizarlo. Por medio del Ocultamiento de Datos se evita que un programador-usuario del Tipo de Datos Abstracto cambie de forma impropia el valor de una variable. Generalmente al usar ADTs también se usan iteradores, que son módulos que permiten obtener todos los valores almacenados en una variable compuesta, como lo son la lista o el arreglo.
{ Ejemplo de Encapsulamiento } { Turbo Pascal v5.5 } { ADTs, sin ocultamiento } TYPE TYPE Persona_T = OBJECT Persona_T = RECORD direccion: STRING[20]; direccion: STRING[20]; telefono : STRING[6]; telefono : STRING[6]; nombre : STRING[35]; nombre : STRING[35]; cedula : STRING[9]; cedula : STRING[9]; nacido : INTEGER; nacido : INTEGER; { ... más campos ... } { ... más campos ... } FUNCTION Edad : INTEGER; END; { Persona_T } { ... más métodos ... } { más operaciones ... } FUNCTION Edad(VAR p: Person_T) END; { Persona_T } : INTEGER; VAR VAR p : Person_T; p : Person_T; {...} {...} i := p.Edad; i := Edad(p); { "p recibe el mensaje Edad" } { Se "calcula" la Edad de p } |
Figura N° 2 |
---|
Un lenguaje de programación soporta el uso de Abstracción de Datos si tiene construcciones sintácticas que le permiten al programador usar ADTs cómodamente. Por ejemplo, el Turbo Pascal v5.5 [BI-88] soporta encapsulamiento, pues le permite al programador definir un tipo de datos junto a sus operaciones, como se muestra en la Figura 2. Sin embargo, la versión 5.0 de ese mismo lenguaje no soporta encapsulamiento, aunque el programador puede simular encapsulamiento usando módulos Turbo Pascal (unidades).
El ejemplo de la Figura 2 muestra que al implementar un ADT deben definirse dos cosas: primero, los campos que se necesitan para almacenar los valores del dato, y segundo los procedimientos que permiten utilizarlo.
El asociar un tipo de datos con sus procedimientos en un módulo se conoce como encapsulamiento. En Pascal el encapsulamiento se simula mediante unidades, y en Modula-2 [Wir-82] usando módulos. En otros lenguajes, como Turbo Pascal, Smalltalk, Simula y C++, el lenguaje incluye construcciones sintácticas que permiten encapsular un tipo de datos, también llamado una clase, con sus procedimientos. En este caso, los procedimientos se conocen por los nombres "operación", "procedimiento miembro" o "método".
La diferencia entre una operación, en el contexto de los
ADTs, y un método, en el contexto de un lenguaje que
soporta encapsulamiento, es puramente sintáctica. En el
ejemplo Turbo Pascal v5.5 de la
Figura 2 la forma sintácticamente
correcta de invocar al método Edad()
de
Person_T
es "p.Edad;
" mientras que si se
usa un lenguaje que no soporta encapsulamiento (como Modula-2)
debe usarse la conocida forma sintáctica
"Edad(p);
".
La única diferencia entre las dos formas de invocar a la
operación (o método) Edad()
es el lugar
en que la variable de tipo Person_T
debe escribirse.
La primera es la forma "Orientada o los Objetos" porque la
variable "p
" aparece de primero, mientras que la
segunda es orientada a los procedimientos porque lo primero que
aparece es el nombre de la rutina. Es este el simple cambio
sintáctico que, con demasiada pompa, ha dado el nombre a la
Programación Orientada a los Objetos (OOP:
Object Oriented Programming en inglés). En el
ejemplo de la
Figura 2 la forma sintáctica
"p.Edad;
" se interpreta como si el objeto
"p
" es un ente activo en el programa, mientras que la
forma "Edad(p);
" es interpretada como si el trabajo
en un programa lo realizan los procedimientos (que calculan), y no
los objetos.
| ||||||||||||||||||||||||
Figura N° 3: Tabla de sinónimos |
---|
Los computólogos hemos tenido la costumbre de crear muchos nuevos términos para viejos conceptos. Los términos estructura de datos, tipo de datos, tipo abstracto de datos, objeto, clase y variable son muy similares. Todos sirven básicamente para lo mismo: definir variables en un programa, o sea, instancias del tipo de datos (aunque tiene más prestigio hablar de "objetos" o de ADTs, que de simples tipos). La Figura 3 es una lista de los términos usados para referirse a los mismos conceptos (aunque no todos son sinónimos exactos). Sin embargo, el contexto del discurso es muy importante: al hablar de ADTs en Smalltalk necesariamente debe suponerse que el concepto incluye cualidades como herencia y funciones virtuales. Las mismas palabras dichas en el contexto de Pascal o Modula-2 no pueden significar más que abstracción de datos.
Entonces, una operación es una rutina que está asociada a un tipo de datos, y que permite manipularlo, examinarlo y cambiarlo. El término "método" es sinónimo de operación y, aunque es bastante confuso, es el utilizado en ambientes Smalltalk (aunque sí existe una buena razón para usar ese término, no la discutiremos).
Intrínsecamente ligado al concepto de encapsulamiento y abstracción está el concepto de ocultamiento de datos. Al ocultar datos, básicamente lo que se persigue es evitar que la representación usada para un ADT sea accesible a un programador-usuario. Por ejemplo, al implementar el ADT pila puede usarse una lista de nodos con punteros, o un arreglo. Si la representación del ADT es privada, entonces al usar una pila el programador no podría saber si la implementación que usa es una o la otra. El ocultamiento de datos es muy importante para lograr una mejor modularización, pues garantiza que el programador descuidado tenga acceso controlado a las campos que conforman una instancia de un ADT, evitando así que destruya inadvertidamente alguna relación necesaria entre esos campos. Paradójicamente, los muy conocidos lenguajes Smalltalk y Simula soportan tanto abstracción como encapsulamiento de datos, pero que no soportan ocultamiento de datos. El que los diseñadores de esos lenguajes no consideraran necesario el ocultamiento de datos indica que no es hasta hace muy pocos años que se ha llegado a entender bien cuáles son las cualidades que debe tener un programa que está bien modularizado.
Tal vez pueda pensarse que los programadores, por antonomasia, no son "descuidados". Sin embargo, el ser "cuidadoso" requiere de un esfuerzo mental grande, a veces enorme, principalmente cuando se trabaja en grandes proyectos de programación. Si el compilador, la máquina computacional puede verificar las interfaces entre los módulos que comprenden un programa, ¿para qué cargar entonces al programador con esta innecesaria responsabilidad? Como se ha mencionado anteriormente, gran parte del éxito de Pascal como lenguaje radica en su capacidad de verificar tipos, que en realidad es una de esas actividades que podría hacerse manualmente. Al ser ésto responsabilidad de la máquina, se obtiene una significativa reducción del esfuerzo requerido para construir un programa, lo que decrece su costo y tiempo de programación.
El lector debe saber que la programación que usa abstracción de datos requiere de un mayor esfuerzo en el diseño de los programas, pues no basta simplemente con partir en subproblemas el problema a resolver, tal y como se hace al descomponer el diseño del programa de arriba hacia abajo (top-down). Cuando se programa de arriba hacia abajo el programador deja para después decisiones de diseño, pues cada nueva incógnita representa un nuevo procedimiento, en el que se oculta la respuesta correcta. Al final del diseño lo que queda es un grafo dirigido de dependencias de procedimientos. Desgraciadamente, en muchos casos la partición así definida no es la más adecuada para alcanzar los objetivos del programa.
Algunos autores han escrito muchos artículos en los que exponen el diseño de arriba hacia abajo claramente, y han logrado convencer a los teóricos de la programación de que es posible llegar a un excelente programa con sólo partirlo de arriba hacia abajo. La verdad es que el proceso de programación es iterativo, y requiere de muchas revisiones en todos los niveles de un programa. La abstracción de datos permite descomponer más adecuadamente un problema, a partir de los objetos que usa. (Esto último es lo que esta nueva tecnología promete: todavía debe usarse mucho antes de decidir si la anterior afirmación es cierta, o si no lo es, bajo qué condiciones sí puede serlo).
En este artículo no se busca desprestigiar a las metodologías de diseño de arriba hacia abajo. Más bien se busca complementarlas, pues el proceso de programación es una actividad intelectual muy difícil, en la que los pocos avances tecnológicos que se han alcanzado todavía no son suficientes para satisfacer las crecientes demandas de sistemas computacionales. Es simplemente ilusorio olvidar una técnica como el diseño de arriba hacia abajo, que en muchos contextos ha dado muy buenos resultados, para sustituirla por otra que todavía no está bien difundida. Pero como con el uso disciplinado de abstracción de datos se han obtenido muchos éxitos, es el deber de todo profesional o académico conocer esta novel técnica.
Al usar abstracción de datos es necesario diseñar mejor el programa, con lo que se logra una mayor modularidad, que es una de las medidas de calidad en programación. El programa queda mejor modularizado que si hubiera sido hecho simplemente usando abstracción de procedimientos, pero el precio de este beneficio es que el programador puede tardar más en terminar su programa (más aún si no sabe bien lo que es un ADT). Un programa escrito sin usar abstracción de datos es más difícil de mantener, pues su estructura está oscurecida por la maraña de procedimientos que lo componen. Por eso al usar abstracción de datos se logra escribir programas más modulares.
La alta modularización alcanzada al usar ADTs permite que el mantenimiento del programa sea mucho más simple. Sin embargo, en un ambiente de desarrollo de programas es difícil que esta nueva tecnología sea aceptada, pues un administrador miope va a preferir que sus sistemas sean programados muy rápidamente, aunque el posterior mantenimiento sea más difícil. Con el correr del tiempo el mercado aprenderá a apreciar la calidad de los programas, lo que obligará a los programadores a usar tecnologías nuevas que les permitan crear mejores productos a un precio más bajo.
Una de las ventajas más importantes de usar ADTs es que un programador especializado puede dedicarse a depurar totalmente los procedimientos de un ADT específico. Esta persona se asegurará no sólo de que la implementación sea correcta, sino también de que sea muy eficiente. De esta manera un programador-usuario del ADT tiene acceso a rutinas de alta calidad y confiabilidad. Se evita así reprogramar los "trucos" que permiten implementar, eficientemente, una estructura de datos cada vez que se la necesite.
Precisamente el poder escoger entre varias alternativas de implementación es la razón principal para usar abstracción de datos: los detalles de implementación quedan totalmente ocultos dentro de las barreras del ADT, y es posible hacer modificaciones locales, generalmente para mejorar la eficiencia, que no tengan una repercusión global. Por eso el uso de abstracción de datos da dividendos cuando se le da mantenimiento a los programas. La abstracción de datos permite implementar programas que están mejor modularizados, y en la mayoría de los casos no implica desechar la programación estructurada y modular.
La Programación por Objetos es una extensión natural del uso de abstracción de datos. Si los conceptos asociados con ADTs se les agrega la Herencia y le Polimorfismo el resultado es OOP. Sin embargo, en la mente de muchos autores y programdores el término Programación por Objetos sólo significa usar herencia y polimorfismo, aunque no se utilice ocultamiento de datos o programación estructurada. OOP introduce nuevos vocablos para viejos conceptos, lo que en muchos casos confunde al neófito.
La herencia es una facilidad puramente sintáctica del lenguaje de programación que permite extender un tipo de datos, agregándole al final nuevos campos. El efecto de la herencia puede simularse en la mayoría de los lenguajes tradicionales, pero el resultado es un programa menos elegante.
{ SIN Herencia } { CON Herencia } TYPE TYPE Punto_T = RECORD Punto_T = OBJECT x,y : INTEGER x,y : INTEGER END; END; Circulo_T = RECORD Circulo_T = OBJECT(Punto_T) p : Punto_T; r : REAL; r : REAL; END; END; PROCEDURE Mueva(VAR p: Punto_T); PROCEDURE Mueva(VAR p: Punto_T); VAR VAR p : Punto_T; p : Punto_T; c : Circulo_T; c : Circulo_T; BEGIN BEGIN p.x := ... p.x := ... c.p.y := ... { .p se ve feo } c.y := ... { Uso directo de } c.r := ... c.r := ... { los campos. } Mueva(p); Mueva(p); Mueva(c.p); { otra vez .p! } Mueva(c); { SIN .p!!! } END; END; |
Figura N° 4 |
---|
En la Figura 4 se muestra como es
posible extender un tipo de datos por medio de la herencia. Como
cualquier instancia del tipo extendido tiene como prefijo al tipo
base, entonces cualquier función que utiliza una variable
del tipo base puede también aplicarse al tipo extendido.
Es por eso que en la columna derecha de la
Figura 4 el procedimiento
Mueva()
se puede aplicar a las variables
"p
" y "c
" indistintamente: como
"c
" es una variable de tipo Circulo_T
necesariamente contiene a una variable de tipo
Punto_T
, sobre la que el procedimiento
Mueva()
puede operar.
La reutilización de componentes, tan mentada por los
adeptos a la OOP, es precisamente la posibilidad de usar una
función sobre un tipo extendido. Pero esto realmente no es
nada nuevo, como puede verse en la columna izquierda de la
Figura 4, en la que basta incluir una
referencia a un campo en un registro (.p
) para
obtener el mismo efecto que se logra por medio de la herencia.
El otro componente importante de la programación por objetos es el uso del polimorfismo, que se implementa por medio del uso de punteros a funciones. Cuando se usa herencia para extender de muchas formas un tipo de datos, puede resultar luego conveniente que el procedimiento que se use para operar sobre un dato dependa del dato en sí, aunque el programador no haya especificado exactamente cuál es ese procedimiento.
Punto_T PROCEDURE Punto_T.Despliegue(); VIRTUAL; . / \ / \ __ _____ / \ | | PROCEDURE Circulo_T.Despliegue(); VIRTUAL; | | | | \__/ |_____| PROCEDURE Cuadrado_T.Despliegue(); VIRTUAL; Circulo_T Cuadrado_T VAR VEC : ARRAY [1..3] OF ^Punto_T; p : Punto_T; c : Circulo_T; r : Cuadrado_T; BEGIN { ... } VEC[1] := ADDR(p); VEC[2] := ADDR(c); VEC[3] := ADDR(r); { ... } FOR i := 1 TO 3 DO BEGIN VEC[i]^.Despliegue; END; |
Figura N° 5 |
---|
Por ejemplo, en la
Figura 5 se muestra una jeraquía
de objetos en que, por medio de la herencia, se extiende el tipo
de datos Punto_T
a un Circulo_T
o a un
Cuadrado_T
. Es natural que el procedimiento para
desplegar un círculo sea totalmente diferente al que
despliega un cuadrado. Sin embargo, ambos procedimientos tiene en
común que existe un procedimiento llamado
Despliegue()
que permite desplegar variables de tipo
Punto_T
, que es el tipo base para
Circulo_T
y Cuadrado_T
.
El código al final de la
Figura 5 muestra como es posible tener
un arreglo de punteros a variables de tipo Punto_T
,
en el que el procedimiento a usar para hacer el despliegue se
escoge en tiempo de ejecución. Para que la decisión
de cuál es "polimórficamente" el procedimiento que
debe usarse para operar sobre una variable se tome en tiempo de
ejecución, lo que se hace es almacenar en cada variable la
dirección del procedimiento que le corresponde.
Así, la variable "p
" tendrá un puntero
al procedimiento Punto_T.Despliegue()
,
"c
" a Circulo_T.Despliegue()
y
"r
" a Rectangulo_T.Despliegue()
. A
estas operaciones Despliegue()
se les conoce como
métodos virtuales, pues es en tiempo de ejecución
cuando se decide cuál de ellas será invocada para
cada una de las variables.
┌─────>o<*>────>Punto_T::Dibuje() VEC[] │ P ┌───┐ │ │ *─┼────────┘ __ ├───┤ / \ │ *─│───────────>│ C │<*>──>Circulo_T::Dibuje() ├───┤ \__/ │ *─┼────────┐ __ └───┘ │ / \ │ │ ││ └───>││ │<*>───>Cilindro_T::Dibuje() │ ││ \__/ T |
Figura N° 6 |
---|
En la Figura 6 se ve que cada valor de
una clase polimórfica incluye un puntero a su método
polimórfico. Por eso, el valor de la variable
"P
" incluye un puntero a
Punto_T::Dibuje()
, la variable "C
" tiene
un puntero al método polimórfico
Circulo_T::Dibuje()
así como la variable
"T
" incluye su puntero polimórfico hacia
Cilindro_T::Dibuje()
. Este puntero polimórfico
es el que se usa en la ejecución del ciclo
"FOR
" en que se invoca
VEC[i]^.Despliegue
, pues es usando el puntero
polimórfico que siempre es posible determinar cuál
es el método correcto que hay que invocar. Aunque parece
que los valores del vector son uniformes, en realidad cada uno de
ellos apunta a un valor de un tipo diferente que está bien
identificado por su puntero polimórfico. De hecho, el tipo
de "VEC[1]
" es diferente al de "VEC[0]
"
ya al de "VEC[2]
" aunque todos tienen en comúm
que también son de tipo Punto_T
.
VEC[] P ┌───┐ ┌───┐ │ *─┼───────>│ x │ ├───┤ ├───┤ │ *─┼──>C │ y │ VMT Punto_T (Virtual Method Table) ├───┤ ├───┤ ┌───┐ │ *─┼──>T │VMT│────>│ 8 │ == 2 + 2 + 4 sizeof(Punto_T) └───┘ └───┘ ├───┤ │ *─┼───>"class Punto_T" Hilera con el nombre RTTI ├───┤ │ *─┼───>Punto_T::Dibuje() Método virtual No.1 ├───┤ │ *─┼───>Punto_T::Borre() Método virtual No.2 └───┘ |
Figura N° 7 |
---|
En realidad es muy honeroso repetir en cada valor todos los punteros polimórficos. Por eso, para cada tipo polimórfico el compilador crea una tabla de métodos virtuales (VMT: Virtual Method Table), en la que están unificados todos los punteros a métodos polimórficos, como se muesta en la Figura 7. De esta manera, cuando un valor tiene métodos virtuales, este hecho se represanta con un puntero a la VMT.
┌─────┬─────┬─┐ │ x │ y │!│ Punto_T └─────┴─────┴─┘ ││ ││ \/ //───────────────────────────────────\\ ││───────────────────────────────────││ ││ ││ \/ Circulo_T \/ Cuadrado_T ┌───────────┬─┬───────┐ ┌───────────┬─┬───────┐ │ x │ y │!│ radio │ │ x │ y │!│ largo │ └───────────┴─┴───────┘ └───────────┴─┴───────┘ ││ ││ ││ ││ \/ Cilindro_T \/ Rectangulo_T ┌───────────┬─┬───────┬────────┐ ┌───────────┬─┬───────┬───────┐ │ x │ y │!│ radio │ altura │ │ x │ y │!│ largo │ ancho │ └───────────┴─┴───────┴────────┘ └───────────┴─┴───────┴───────┘ |
Figura N° 8 |
---|
En la Figura 8 se muestran los campos de
la jerarquía de tipos
( Punto_T ( Circulo_T ( Cilindro_T ) Cuadrado_T ( Rectangulo_T ) )
. El campo marcado "!
" es el puntero a la VMT.
Cabe mencionar que a las variables de programas que siguen el paradigma OOP se les llama objetos, o sea, que un objeto es una instancia de un tipo.
En el argot popular se acepta que un lenguaje soporta la Programación por Objetos si brinda facilidades para implementar herencia (extensión de tipos), polimorfismo (funciones virtuales) y encapsulamiento. Sin embargo, cada lenguaje orientados a objeto soporta de manera distinta cada uno de estos conceptos. Como ha sucedido con otros lenguajes y paradigmas de programación, el ser orientado a objetos es una cuestión más de fé que académica.
La sintáxis de casi todos los lenguajes de programación exige que los identificadores de variables y procedimientos sean únicos. Sin embargo, es muchas veces conveniente permitir que dos procedimientos diferentes se llamen igual.
{ LO FEO } PROCEDURE PrintReal (VAR o: REAL); PROCEDURE PrintInteger (VAR o: INTEGER); PROCEDURE PrintCardinal (VAR o: CARDINAL); { LO BONITO } VAR r : REAL; i : INTEGER; { ... } Print(r); { Usa la versión Print(REAL) pues "r" es REAL } Print(i); { Usa la versión Print(INTEGER) } |
Figura N° 9 |
---|
Por ejemplo, el procedimiento para imprimir una variable generalmente se llama Print. En el lenguaje Modula existe un print para cada tipo de argumento, como se muestra al principio de la Figura 9.
La sobrecarga de identificadores le permite al programador definir
tres procedimiento que se llaman igual, Print()
, pero
que el compilador distingue por el tipo del argumento que se
utilice. En la
Figura 9 se muestra esta comodidad.
La sobrecarga de identificadores es una facilidad sintáctica que le evita al programador inventar nuevos nombres para los mismos conceptos.
TYPE Matriz_T = RECORD { ... } END; FUNCTION OPERATOR+ (VAR A, B: Matriz_T) : Matriz_T; FUNCTION OPERATOR- (VAR A, B: Matriz_T) : Matriz_T; FUNCTION OPERATOR* (VAR A, B: Matriz_T) : Matriz_T; VAR M,N,O,P : Matriz_T; BEGIN M := (N * O) - P; M := (1 - M) * (P - O); |
Figura N° 10 |
---|
Los matemáticos y los ingenieros se quejan mucho de que los lenguajes de programación no incluyen operadores para operar sobre objetos como números complejos, matrices o vectores. La solución a este problema se da por medio de la sobrecarga de operadores, la que permite definir una función que puede ser invocada en una expresión aritmética. La idea es que el programador pueda escribir un programa en el que las variables que se usan en expresiones sean objetos complejos, como se muestra en la Figura 10.
En ese ejemplo se muestra como se define un nuevo tipo de datos,
Matriz_T
, y se sobrecargan los operadores de suma y
multiplicación. De esta manera, el programador puede
escribir expresiones muy similares a las aritméticas en su
programa, y el compilador se encarga de invocar a los
procedimientos adecuados.
La sobrecarga de operadores facilita mucho la vida al programador, pero desgraciadamente le complica mucho al vida el escritor de compiladores.
CONSTRUCTOR Persona_T.Person_T; { RESULTADO Ejemplo de un constructor para el objeto Person_T } { OJO Esto NO es código Turbo Pascal válido! } BEGIN direccion := "NO TIENE"; telefono := "" nombre := "NADIE" cedula := ""; nacido := -1; END; |
Figura N° 11 |
---|
En la Figura 11 se muestra un ejemplo de un constructor para el tipo de datos Persona, el que se encarga de inicializar sus campos. Aunque la sintásis que se usa para ese ejemplo se parece a la de Turbo Pascal, no es la correcta. Los constructores de Turbo Pascal no son invocados automáticamente por el compilador por lo que el programador debe llamarlos explícitamente. Tienen la importante función de asociar a una variable con sus funciones polimórficas, por lo que si el programador Turbo Pascal olvida invocar a los constructores el resultado siempre desastrozo.
Aunque la razón principal para que existan constructores es
la necesidad de inicializar variable, los destructores no tienen
esa misma función. Estos últimos se usan casi
únicamente para retornar la memoria dinámica
asociada a un variable justo antes de que esa variable ya no vaya
a ser utilizada más en un programa. Por ejemplo, si
"T
" es una instancia de Tree_T
, entonces
el destructor de Tree_T
se encargará de
devolver toda la memoria dinámica asociada a
"T
" antes de abandonar el procedimiento en que
"T
" está definida. Si el lenguaje tiene un
recolector de basura, entonces los usual es que no soporte el uso
de destructores.
Una de las más importantes cualidades del lenguaje C++ es que el programador no tiene que preocuparse por invocar explícitamente a los constructores y destructores, pues el compilador se encarga de hacerlo. De esta manera, cuando una variable ya no podrá ser utilizada porque el flujo de control abandonará el bloque en que la variable ha sido definida, entonces el compilador C++ automáticamente insertará un llamado al destructor de la variable. Esto es muy cómodo para el programador, quien ya no necesita preocuparse de si ha hecho todos los llamados a los destructores y constructores de las variables que usa en su programa.
El campo en que mayor impacto tiene la Programación por Objetos es en el Diseño de Interfaces Hombre-Máquina, pues es muy natural capturar la estructura de los objetos que pueden desplegarse en una pantalla usando una jerarquía de objetos. Pero en casi todos los demás campos el uso de jerarquías no rinde tantos beneficios como en el campo de los gráficos y las pantallas. Sin embargo, como la interfaz de un programa es muy importante, la mayoría de los programadores están aprendiendo a usar la metodologías orientadas a objetos para lograr programas que puedan competir en el mercado. Prácticamente todos los programas modernos deben tener una interfaz programada usando OOP, o de lo contrario el programa será rechazado por los usuarios finales lo que en parte explica la atención que ha recibido esta nueva tecnología.
La programación por objetos es la agregación de varias técnicas de programación que se han desarrollado con el correr del tiempo. Su uso se está difundiendo mucho por la importancia que tiene el desarrollo de interfaces para programas, las que de una manera muy natural pueden implementarse usando técnicas de abstracción y programación por objetos.
Sin embargo, OOP no es una tecnología que vaya a resolver todos nuestros problemas de computación. Las panaceas no existen, y los computólogos nos vemos obligados a conocer cada vez más técnicas para resolver los desafiantes problemas del mundo actual.
Aunque usar ADTs es más difícil que no hacerlo, pues en algunos casos se dura el doble programando, vale la pena el esfuerzo, pues la mejora en la modularidad de los programas que produce aumenta mucho su calidad. Ojalá los programadores acepten este mensaje.
Pero tal vez el efecto colateral más positivo que se obtenga de OOP sea el entrenamiento a que fuerza a los programadores, quienes poco a poco comprenden mejor como hacer programas muy bien modularizados. En estos nuevos programas cada módulo se acerca cada vez más a ser realmente un "componente de software". Esta nueva disciplina de programación dará muy buenos réditos, pues de seguro mejorará mucho la calidad de los sistemas disponibles para los usuarios finales.
Esta investigación se realizó dentro del proyecto de investigación 326-89-019 "Estudios en la tecnología de programación por objetos y C++", inscrito ante la Vicerrectoría de Investigación de la Universidad de Costa Rica. La Escuela de Ciencias de la Computación e Informática también ha aportado fondos para este trabajo.
IF-THEN-ELSE
,
WHILE-REPEAT
, CASE
, procedimientos
y argumentos. Un programa estructurado nunca hace uso del
GO TO
, y generalmente se descompone
modularmente como una jerarquía de procedimientos,
usando la descomposición de Arriba hacia Abajo
[Top-Down].
+ - * / ^
] en expresiones.
╔══════════╤════════╤════════════════════════════════╗─────┐ ║ │ │ ┌─Subrutinas ───┐ ║ │ ║ │ Turbo │ Programación ├─Módulos ──────┤ ║ │ ║ │ Pascal │ Estructurada ├─IF-THEN-ELSE ─┤ ║ │ ║ │ │ ├─WHILE-REPEAT ─┤ ║ │ ║ │ Modula │ └─GO TO ────────┘ ║ │ ║ ├────────┼────────────────────────────────╢ │ ║ │ Turbo │ ┌─Tipos de Datos ──────┐ ║ │ ║ │ Pascal │ ADTs ├─Encapsulamiento ─────┤ ║ A │ ║ │ v5.5 │ ├─Ocultamiento ───────┤ ║ D │ ║ │ │ └─Iteradores ──────────┘ ║ A │ ║ ╔════╗ └────────┴────────────────────────────────╫─┐ │ ║ ║ ╔══╝ ╔══════════════════════╦═══╗ ║ │ │ ║ ║ ║ ║ ║ ║ Herencia ║ O ║ ║ │ │ ║ ║ ║ ══╬══ ══╬══ ╟──────────────────────╢ O ║ ║ │ │ ║ ║ ║ ║ ║ ║ Polimorfismo ║ P ║ ║ │ │ ║ ║ ╚══╗ ╚══════════════════════╩═══╝ ║ │ │ ║ ╚════╝ ┌────────┬────────────────────────────────╫─┘ │ ║ │ │ Sobrecarga de Identificadores ║ │ ║ │ Prolog ├────────────────────────────────╢ │ ║ │ │ Sobrecarga de Operadores ║ │ ║ └────────┴────────────────────────────────╫─────┘ ║ ┌────────────────────────────────╢ ║ │ Constructores y Destructores ║ ╚═══════════════════╧════════════════════════════════╝ |
[BI-88] | Borland International: Turbo Pascal Reference Manual version 5.5, Borland International, California (U.S.A.), 1988. |
[DiM-88] | Di Mare, Adolfo:
Convenciones de Programación para
Pascal,
Reporte Técnico ECCI-01-88, Proyecto
326-86-053, Escuela de Ciencias de la
Computación e Informática, Universidad de
Costa Rica, 1988.
http://www.di-mare.com/adolfo/p/convpas.htm .
|
[DiM-90] | Di Mare, Adolfo: Abstracción de Datos en Pascal, Reporte técnico PIBDC-02-90, ECCI-UCR, 1991. |
[GR-83] | Goldberg, Adele & Robson, David: Smalltalk-80, the Language and its Implementation, Addison-Wesley, 1983. |
[KP-87] | Kernighan, Brian W. & Pike, Rob: El entorno de programación UNIX, Prentice-Hall, 1987. |
[KR-86] | Kernighan, Brian W. & Ritchie, Dennis M.: El lenguaje de programación C, Prentice Hall; 1986. |
[LG-86] | Liskov, Barbara & Guttag, John: Abstraction and Specification in Program Development, McGraw-Hill, 1986. |
[Str-86] | Stroustrup, Bjarne: The C++ Programming Language, Addison-Wesley, 1986. |
[Str-88] |
Stroustrup, Bjarne:
What is Object-Oriented Programming,
IEEE Software,
pp [10-20],
Mayo 1988,
http:// www.research.att.com /~bs/ papers.html .
|
[Wir-82] | Wirth, Niklaus: Programming in Modula-2, Second Edition, R.R. Donnelley & Sons, Harrisonburg, Virginia, 1982. |
[-] | Resumen |
[1] | Programación Estructurada |
[2] | Tipos Abstractos de Datos |
[3] | Programación por Objetos |
[4] | Sobrecarga de Identificadores |
[5] | Sobrecarga de Operadores |
[6] | Constructores y Destructores |
[7] | Usos de OOP |
[8] | Conclusión |
[9] | Reconocimientos |
[10] | Glosario |
|
|
Bibliografía | |
Indice | |
Acerca del autor | |
Acerca de este documento | |
Principio Indice Final |
Adolfo Di Mare: Investigador costarricense en la Escuela de Ciencias de la Computación e Informática [ECCI] de la Universidad de Costa Rica [UCR], en donde ostenta el rango de Profesor Catedrático. Trabaja en las tecnologías de Programación e Internet. Es Maestro Tutor del Stvdivm Generale de la Universidad Autónoma de Centro América [UACA], en donde ostenta el rango de Catedrático y funge como Consiliario Académico. Obtuvo la Licenciatura en la Universidad de Costa Rica y la Maestría en Ciencias en la Universidad de California, Los Angeles [UCLA].
Adolfo Di Mare <adolfo@di-mare.com>
Referencia: | Di Mare, Adolfo:
Tipos Abstractos de Datos y Programación por Objetos,
Reporte Técnico PIBDC-03-91, proyecto 326-89-019,
Escuela de Ciencias de la Computación e Informática,
UCR 1991.
http://www.di-mare.com/adolfo/p/oop-adt.htm .
|
Internet: |
http://www.di-mare.com/adolfo/p/oop-adt.htm
|
Autor: | Adolfo Di Mare
<adolfo@di-mare.com>
|
Contacto: | Apdo 4249-1000, San José Costa Rica Tel: (506) 2511-8000 Fax: (506) 2438-0139 |
Revisión: | ECCI-UCR, Julio 2010 |
Visitantes: |