next up previous
Siguiente: 7 Introducción a C++ Arriba: Introducción a la Programación Orientada a Objetos Anterior: 5 Más Conceptos de Orientación a Objetos

Subsecciones

6 Aún Más Conceptos de Orientación a Objetos

 
Peter Müller
Globewide Network Academy (GNA)
pmueller@uu-gna.mit.edu

Continuamos con nuestro viaje alrededor del mundo de los conceptos de orientación a objetos, presentando una breve introducción a la asignación estática de memoria versus la asignación dinámica de memoria. Con ésto, podemos introducir el polimorfismo como un mecanismo que permite a los objetos saber que hacer en "runtime". Pero primero, he aquí una breve revisión acerca de los tipos genéricos.

6.1 Tipos Genéricos

Nosotros ya conocemos los tipos genéricos a partir del capítulo  3 cuando hemos hablado acerca de los tipos de datos genéricos abstractos. Al definir una clase, realmente definimos un tipo definido por el usuario. Algunos de estos tipos pueden operar en otros tipos. Por ejemplo, podrían haber listas de manzanas, listas de carros, listas de números complejos o aún listas de listas.

Por el momento, cuando escribimos una definición de clase, debemos poder decir que esta clase debería definir un tipo genérico. Sin embargo, no sabemos con que tipo de datos va a ser usada la clase. Consecuentemente, debemos poder definir la clase con la ayuda de un "contenedor" al cuál nos referimos como si fuera el tipo sobre el que opera la clase. Así, la definición de clase nos provee con un template (plantilla) de una clase real. La definición actual de la clase es creada una vez que declaramos un objeto en particular. Ilustremos esto con el siguiente ejemplo. Suponiendo que tu quieres definir una clase para listas que debería ser de tipo genérico. Así, debería ser posible declarar objetos lista para manzanas, carros o para cualquier otro tipo.

  template class List for T {
    attributes:
      ...       /* Estructura de datos que se necesita para implementar */
                /* la lista */

    methods:
      append(T element)
      T getFirst()
      T getNext()
      bool more()
  }

La clase plantilla List mostrada arriba se ve como cualquier otra definición de clase. Sin embargo, la primera línea declara que List sea una plantilla para tipos variados. El identificador T se usa como un contenedor para un tipo real. Por ejemplo, append() lleva un elemento como argumento. El tipo de este elemento será del tipo datos con el cuál un objeto lista real es creado. Por ejemplo, podemos declarar un objeto lista para manzanas si la definición del tipo manzana (Apple) existe :

  List for Apple appleList
  Apple anApple,
        anotherApple
  appleList.append(anotherApple)
  appleList.append(anApple)

La primera línea declara que appleList sea una lista para mananas. En este momento, el compilador utiliza la definición de plantilla, substituye cada ocurrencia de T con Apple y crea una definición de clase real para ésto. Esto lleva a una definición de clase similar a la que sigue :

  class List {
    attributes:
       ...       /* Estructura de datos que se necesita para implementar */
                 /* la lista */

    methods:
      append(Apple element)
      Apple getFirst()
      Apple getNext()
      bool more()
  }

Esto no es exactamente lo que genera el compilador. El compilador debe asegurse que podamos crear múltiples listas para diferentes tipos en cualquier momento. Por ejemplo, si necesitamos otra lista para digamos, peras, podemos escribir :

  List for Apple appleList
  List for Pear pearList
  ...

En ambos casos el compilador genera una definición de clase real. La razón por la que ambas no presentan conflictos de nomenclatura se debe a que el compilador genera nombres únicos. Sin embargo, desde el momento que ésto no es visible para nosotros, no entraremos en más detalle. De cualquier manera, si tú declaras otra lista de manzanas, el compilador se puede dar cuenta si ya existe una definición de clase real y hace uso de ella o si ti ésta tiene que ser creada. Así,

  List for Apple aList
  List for Apple anotherList

creará la definición de clase real para aList y la reutilizará para anotherList. Por consecuencia, ambas son del mismo tipo. Resumimos ésto en la siguiente definición :

Definición (Template Class) Si una clase A es parametrizada con un tipo de datos B, A es llamada template class. Una vez que un objeto de A es creado, B es reemplazado por un tipo de datos real. Esto permite la definición de una clase real basada en la plantilla especificada para A y en el tipo de datos real.

Podemos definir clases plantilla con más de un parámetro. Por ejemplo, los directories son colecciones de objetos donde cada objeto puede ser referenciado por medio de una clave. Por supuesto, un directorio debería poder almacenar todo tipo de objetos. Pero existen también varias posibilidades para claves. Por ejemplo, pudieran ser strings o números. Consecuentemente, definiríamos una clase plantilla Directory que esté basado en dos tipos de parámetros, uno para la clave y otro para los objetos almacenados.

6.2 Asignación Estática y Dinámica de Memoria (Static and Dynamic Binding)

En lenguajes de programación fuertemente tipificados tu normalmente tienes que declarar variables antes de usarlas. Esto también implica la definición de la variable, en la que el compilador reserva espacio para ésta. Por ejemplo, en Pascal una expresión como

  var i : integer;

declara que la variable i sea de tipo integer. Adicionalmente, define suficiente espacio en la memoria para retener un valor integer (entero).

Con la declaración amarramos (asignamos) el nombre i al tipo integer. Esta asignación es válida dentro del ámbito en el cuál es declarada i. Esto permite al compilador checar en el momento de la compilación la consistencia de los tipos. Por ejemplo, la siguiente asignación dará por resultado un error de "type mismatch" (desigualdad en los tipos) cuando tratas de compilarla :

  var i : integer;
  ...
  i := 'string';

Llamamos a este tipo particular de asignación "estática" debido a que queda fija al momento de la compilación.

Definición (Asignación Estática de Memoria) Si el tipo T de una variable se asocia explícitamente con su nombre N por medio de la declaración, decimos que N está staticamente asignada a T. El proceso de asignación es conocido como asignación estática de memoria (static binding).

Existen lenguajes de programación que no usan variables explícitamente tipificadas. Por ejemplo, algunos lenguajes permiten introducir variables una vez que se necesitan :

  ...       /* La variable i no aparece */
  i := 123  /* Creación de i como un integer */

El tipo de i se conoce en el momento que su valor es establecido. En este caso, i es de tipo integer ya que le hemos asignado un número entero. Así, debido a que el contenido de i es un número entero, el tipo de i es entero.

Definición (Asignación Dinámica de Memoria) Si el tipo T de una variable N está implícitamente asociado con su contenido, decimos que N está dinámicamente asignada a T. El proceso de asociación se llama asignación dinámica de memoria (dynamic binding).

Ambas asignaciónes difieren en el momento en el que el tipo se asigna a la variable. Considera el siguiente ejemplo que es posible solamente por medio de la asignación dinámica :

  if somecondition() == TRUE then
    n := 123
  else
    n := 'abc'
  endif

El tipo de n después de la instrucción if depende de la evaluación de somecondition(). Si es TRUE, n es de tipo integer mientras que en el otro caso, es de tipo string.

6.3 Polimorfismo

  El polimorfismo le permite a una entidad (por ejemplo, variable, función u objeto) adoptar una variedad de representaciones. Por lo tanto, debemos distinguir diferentes tipos de polimorfismo los cuáles serán planteados aquí.

El primer tipo es similar al concepto de asignación dinámica de memoria. Aquí, el tipo de una variable depende de su contenido. Así, su tipo depende del contenido en un momento específico :

  v := 123        /* v es un integer */
  ...             /* v se usa como integer */
  v := 'abc'      /* v "cambia" a string */
  ...             /* v se usa como string */

Definición (Polimorfismo (1)) El concepto de asignación dinámica de memoria permite a una variable el adoptar diferentes tipos dependiendo de su contenido en un momento en particular. Esta habilidad de una variable es conocida como polimorfismo. Otro tipo de polimorfismo puede ser definido por funciones. Por ejemplo, suponiendo que se quiera definir una función isNull() que regresa TRUE si su argumento es 0 (cero) y FALSE de cualquier otro modo. Para números enteros ésto es fácil:

  boolean isNull(int i) {
    if (i == 0) then
      return TRUE
    else
      return FALSE
    endif
  }

Sin embargo, si queremos checar números reales, deberíamos usar otra comparación debido al problema de la precisión :

  boolean isNull(real r) {
    if (r < 0.01 and r > -0.99) then
      return TRUE
    else
      return FALSE
    endif
  }

En ambos casos queremos que la función se llame isNull. En lenguajes de programación sin polimorfismo para funciones, no podemos declarar estas dos funciones debido a que el nombre isNull sería doblemente definido. Sin polimorfismo para funciones, los nombres doblemente definidos serían ambiguos. Sin embargo, si el lenguaje tomara en cuenta los parámetros de la función, esto funcionaría. Así, funciones (o métodos) son identificados en forma única por:

Debido a que la lista de parámetros de ambas funciones isNull son diferentes, el compilador puede deducir el funcionamiento correcto usando los tipos reales de los argumentos:

  var i : integer
  var r : real

  i = 0
  r = 0.0

  ...

  if (isNull(i)) then ...   /* Se usa isNull(int) */
  ...
  if (isNull(r)) then ...   /* Se usa isNull(real) */

Definición (Polimorfismo (2)) Si una función (o método) es definida por la combinación de

hablamos de polimorfismo. Este tipo de polimorfismo nos permite reutilizar el mismo nombre para funciones (o métodos) tanto como la lista de parámetros difiera. Algunas veces, este tipo de polimorfismo es llamado overloading.

El último tipo de polimorfismo permite que un objeto escoja los métodos correctos. Considera nuevamente la función move(), que toma como argumento un objeto de la clase Point. Hemos usado esta función con cualquier objeto de las clases derivadas, debido a que se mantiene la relación es-un(a).

Considera ahora una función display() que debería usarse para dibujar objetos desplegables. La declaración de esta función podría ser como sigue:

  display(DrawableObject o) {
    ...
    o.print()
    ...
  }

Nos gustaría usar esta función con objetos derivados de DrawableObject:

  Circle acircle
  Point apoint
  Rectangle arectangle

  display(apoint)      /* Debería invocar apoint.print() */
  display(acircle)     /* Debería invocar acircle.print() */
  display(arectangle)  /* Debería invocar arectangle.print() */

El método actual debería ser definido por el contenido del objeto o de la función display(). Debido a que ésto es algo complicado, he aquí un ejemplo más abstracto :

  class Base {
  attributes:

  methods:
    virtual mira()
    mar()
  }

  class Derivado hereda de Base {
  attributes:

  methods:
    virtual mira()
    mar()
  }

  demo(Base o) {
    o.mira()
    o.mar()
  }

  Base unabase
  Derivado underivado

  demo(unabase)
  demo(underivado)

En este ejemplo definimos dos clases Base y Derivado. Cada clase define dos métodos mira() y mar(). El primer método es definido como virtual. Esto significa que si este método es invodado, su definición debería ser evaluada por el contenido del objeto.

Enseguida definimos una función demo() que lleva un objeto Base como argumento. Consecuentemente, podemos usar esta función con objetos de la clase Derivado debido a que se mantiene la relación es-un(a). Llamamos a esta función con un objeto Base y un objeto Derivado, respectivamente.

Supongamos que mira() y mar() están definidos para imprimir solamente su nombre y la clase en la que fueron definidos. La salida sería como sigue:

  mira() of Base ha sido llamada.
  mar() de Base ha sido llamada.
  mira() de Derivado ha sido llamada.
  mar() de Base ha sido llamada.

¿Por qué sucede ésto ? Veamos qué es lo que pasa. La primera llamada a demo() utiliza un objeto Base. Así, el argumento de la función es "llenado" con un objeto de clase Base. Cundo llega el momento de invocar el método mira() su funcionalidad actual es escogida basándose en el contenido actual del correspondiente objeto o. En esta ocasión se trata de un objeto Base. Por consecuencia, es llamada mira() tal y como se define en la clase Base.

La llamad a mar(), no está sujeta a esta resolución de contenido. No está marcada como virtual. Consecuentemente, mar() es llamada en el ámbito de la calse Base.

La segundo llamada a demo() lleva un objeto Derivado como argumento. Así, el argumento o es llenado con un objeto Derivado. Sin embargo, o en sí mismo solamente representa la parte Base del objeto provisto underivado.

Ahora, la llamada a mira() se evalúa examinando el contenido de o, de ahí que sea llamado dentro del ámbito de Derivado. Por otro lado, mar() todavía es evaluado dentro del ámbito de Base.

Definición (Polimorfismo (3)) Los objetos de superclases pueden ser llenados con objetos de sus subclases. Los operadores y métodos de las subclases pueden ser definidos para ser evaluados en dos contextos :

1.
Basándose en el tipo de objeto, conduciendo a una evaluación dentro del ámbito de la superclase.
2.
Basándose en el contenido del objeto, conduciendo a una evaluación dentro del ámbito de la subclase contenida.
El segundo tipo es llamado polimorfismo.
next up previous
Siguiente: 7 Introducción a C++ Arriba: Introducción a la Programación Orientada a Objetos Anterior: 5 Más Conceptos de Orientación a Objetos
P. Mueller
8/31/1997