[UCR]  
[/\]
Universidad de Costa Rica
Escuela de Ciencias de la
Computación e Informática
[<=] [home] [<>] [\/] [=>]

Tipos Abstractos de Datos y Programación por Objetos

Reporte Técnico PIBDC-03-91

Proyecto 326-89-019

Adolfo Di Mare

Resumen [<>] [\/] [/\]

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]).


1. Programación Estructurada [<>] [\/] [/\]

      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.


2. Tipos Abstractos 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.

Encapsulación Procedimiento Tipo de Dato Mensaje
Encapsulación Procedimiento Tipo de Dato Mensaje
Abstracción Subrutina Objeto Parámetro
Especificación Rutina Clase Argumento
Diseño Método ADT  
Ocultamiento Operación Tipo Abstracto de Datos  
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.


3. Programación por Objetos [<>] [\/] [/\]

      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.

4. Sobrecarga de Identificadores [<>] [\/] [/\]

      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.


5. Sobrecarga de Operadores [<>] [\/] [/\]

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.

6. Constructores y Destructores [<>] [\/] [/\]

      Todos los programadores saben lo importante que es inicializar cualquier variable antes de usarla. En el lenguaje C++ el programador puede definir uno o varios procedimientos especiales que se encargan de inicializar las variables de un tipo, y que además son invocadas automáticamente por el compilador. Estos procedimientos especiales se llaman constructores y también son los encargados de inicializar los punteros a la VMT. Los destructores son también importantes pues se encargan de destruir las variables cuando ya no se utilizarán más, que sucede siempre que se abandona un bloque de instrucciones.

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.

7. Usos de OOP [<>] [\/] [/\]

      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.

8. Conclusión [<>] [\/] [/\]

      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.

9. Reconocimientos [<>] [\/] [/\]

      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.



10. Glosario [<>] [\/] [/\]

Abstracción:
Proceso por medio del que se elimina información para lograr tratar cosas diferentes como si fueran iguales. De esta forma se separan las características de un ente en dos grupos, el de las características que importa y el de las que sobran, para tomar en cuenta únicamente aquellas relevantes a la solución de un problema.
Especificación:
Proceso por medio del que se definen las características de un ente.
Procedimiento o rutina:
Facilidad sintáctica de los lenguajes de programación que le permiten al programador aislar la solución de un problema particular en un módulo. Mediante el uso de procedimientos es posible aplicar la abstracción por parametrización y por especificación.
Abstracción por parametrización:
Uso de argumentos en procedimientos y rutinas.
Abstracción por especificación:
Disciplina de programación que busca que el programador defina qué es lo que hace cada uno de los módulos que componen un programa o sistema, aún antes de realizar la implementación.
Módulos:
Partes que componen un sistema, que generalmente se construyen de forma que sean independientes unas de otra. En general se busca que al hacer cambios en un módulo no sea necesario hacerlo en otros. En estos casos, se dice que los módulos tienen una cohesión baja.
Programación Estructurada:
Conjunto de prácticas y disciplinas de programación que se basan en el uso de Abstracción por parametrización y por especificación para construir sistemas. Además, un lenguaje de programación soporta la Programación Estructurada si cuenta con las construcciones sintácticas 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].
Tipos Abstractos de Datos:
Es un el uso de una técnica de de abstracción que busca juntar en un sólo módulo toda la programación relevante a una estructura de datos en particular. Esto se logra definiendo las operaciones de un tipo de datos. Lo usual es que un ADT provea Ocultamiento de Datos y alguna forma de Encapsulamiento de datos, aunque ésta última no sea completa.
Encapsulamiento:
Facilidad de un lenguaje de programación que permite definir un tipo de datos junto a sus operaciones, con el fin de obtener un tipo abstracto de datos.
Ocultamiento de Datos:
Facilidad de un lenguaje de programación que permite evitar que un programador que usa un tipo abstracto de datos pueda manipular la estructura interna de una instancia. De esta manera se evita que el programador usario del tipo abstracto de datos introduzca inconsistencias en la estructura de datos.
Iteradores:
Abstracción que permite obtener todos los elementos contenidos en un tipo abastracto de datos contenedor. Los contenedores más usuales son los arreglos, las listas, los conjuntos, los árboles y en menor grado los grafos.
Herencia (Extensión de tipos):
Facilidad del lenguaje de programación que permite extender un tipo de datos, agregando 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 y en muchos casos mucho más difícil de programar.
Polimorfismo (Funciones Virtuales):
Un lenguaje de programación que soporta polimorfismo permite diferir la definición de la operación que debe aplicarse un objeto al momento en que esa operación es necesaria. De esta manera, el programador no necesita conocer el objeto sobre el que opera.
      Cuando se usa herencia para extender de muchas formas un tipo de datos, resulta 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.
Sobrecarga de Identificadores:
Facilidad de un lenguaje de programación que permite usar el mismo identificador para para procedimientos diferentes. Los procedimientos se diferencian por sus argumentos y no por su nombre.
Sobrecarga de Operadores:
Facilidad de un lenguaje de programación que le permite al programador definir un nuevo tipo de datos que puede usarse en expresiones. En general, la sobrecarga de operadores implica el uso de los operadores aritméticos [+ - * / ^] en expresiones.
Método:
Operación que es definida junto a un tipo de datos en aquellos lenguajes que soportan encapsulamiento.
Mensaje:
Nombre del método con que se invoca a una operación sobre un objeto. Los términos método y operación son sinónimos, pero uno se usa en el contexto de Programación por Objetos y el otro en el de Abstracción de Datos.
Programación Orientada a los Objetos [OOP]:
Uso de un técnicas de Abstracción de Datos en un lenguaje que también soporta Encapsulamiento, Herencia y Polimorfismo.
Funciones Virtuales:
Polimorfismo.
Programación por Objetos:
Programación Orientada a los Objetos.
Constructores y Destructores:
Facilidad de un lenguaje de programación que le permite al programador definir uno o varios procedimientos especiales que se encargan de inicializar y destruir las variables de un tipo, y que además son invocadas automáticamente por el compilador. En genereal, la misión principal de los destructores es devolver la memoria dinámica asociada a un objeto.


╔══════════╤════════╤════════════════════════════════╗─────┐
║          │        │              ┌─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   ║
╚═══════════════════╧════════════════════════════════╝
El Mapa de los Lenguajes


Bibliografía [<>] [\/] [/\]

[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.


Indice [<>] [\/] [/\]

[-] 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


Acerca del autor [<>] [\/] [/\]

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].

[mailto] Adolfo Di Mare <adolfo@di-mare.com>



Acerca de este documento [<>] [\/] [/\]

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:

Copyright © 1991 Adolfo Di Mare
Derechos de autor reservados © 1991 Adolfo Di Mare <adolfo@di-mare.com>
[home] [<>] [/\]