Programación Orientada a Objetos Clase # 7 Polimorfismo Jueves, 23 de Mayo de 2002 Agenda Polimorfismo. Operadores new y delete Iván Bernal, Ph.D. Escuela Politécnica Nacional email: imbernal@mailfie.epn.edu.ec Copyright @2002, I. Bernal Iván Bernal, Ph.D. 2 Polimorfismo (1) Con una jerarquía de tipos, suele ser conveniente manipular un objeto utilizando el tipo de la base y no el tipo específico. Se puede escribir código que no depende de un tipo específico. Ejemplo con las formas geométricas: Se puede escribir código que manipule las formas de manera genérica sin considerar si son círculos, cuadrados, triángulos, etc. Toda forma requiere ser dibujada, borrada y movida. El código que las manipula simplemente envía mensajes a un objeto Forma Geométrica, no se preocupa como cada objeto se encarga de manejar el mensaje. Iván Bernal, Ph.D. 3 Polimorfismo (2) Código como el descrito no se verá afectado por la inclusión de nuevos tipos. Añadir nuevos tipos es la manera más común de extender un programa que use POO, para resolver nuevas demandas del programa. Se podría derivar un nuevo tipo: Pentágono sin modificar aquella parte del código que manipula las forma genéricas. Este tipo de facilidad mejora los diseños al mismo tiempo que reduce los costos de mantenimiento del software. Iván Bernal, Ph.D. 4
Polimorfismo (3) Problema: Si una función por ejemplo, comanda que una forma genérica se dibuje, o a un ave genérica que vuele, el compilador no sabe con precisión, el momento de la compilación, la sección de código que se va a ejecutar. Pero eso es lo que quiere el programador, cuando se envíe el mensaje, no quiere saber cuál será el código que se ejecutará. El comando para dibujar una forma geométrica se aplica por igual a un círculo, a un cuadrado, a un triángulo, y el código a ejecutarse dependerá del tipo específico del objeto. Al no conocer la sección exacta de código a ejecutarse, se puede añadir tipos y el código a ejecutarse para este nuevo tipo será diferente, sin necesidad de hacer cambios a la función que comanda las operaciones del tipo genérico. Iván Bernal, Ph.D. 5 Polimorfismo (4) En la figura se tiene un controlador (BirdController) de objetos tipo pájaro (Bird). El controlador no sabe el tipo específico de los objetos tipo pájaro. Para el BirdController esto es conveniente ya que no necesita escribir código para primero descubrir que tipo de objeto se usará en un determinado momento. Cuando se invoque la función move( ), sin conocer el tipo específico de Bird,, el comportamiento adecuado para cada objeto será ejecutado (un ganso Goose- corre, vuela o nada y un pingüino Penguin- corre o nada. Iván Bernal, Ph.D. 6 Polimorfismo (5) Polimorfismo (6) Cómo se consigue lo mencionado en el ejemplo? La respuesta es uno de los trucos fundamentales utilizados en POO. El compilador de un lenguaje tradicional, cuando se llama a una función se produce lo que se conoce como asociación temprana (early binding). El compilador genera una llamada usando el nombre de una función específica, y el enlazador (linker) resuelve la llamada e incluye la dirección del código que va a ser ejecutado. En POO, no se puede determinar la dirección del código a ejecutarse sino hasta que el programa esté en ejecución. Iván Bernal, Ph.D. 7 Iván Bernal, Ph.D. 8
Polimorfismo (7) Lenguajes OO utilizan el concepto de asociación tardía (late binding). Cuando se envía un mensaje a un objeto, el código llamado no se determina hasta que el programa está en ejecución. En C++, el compilador inserta una sección de código en lugar de una llamada absoluta a una función. Este código calculará la dirección del cuerpo de la función, utilizando información almacenada en el objeto que se invoque. Iván Bernal, Ph.D. 9 Polimorfismo (8) Para indicar que se desea que una función de una clase tenga la flexibilidad de late-binding, se utiliza la palabra clave virtual. No es necesario entender en detalle los mecanismos detrás de esta palabra, pero sin ella no se puede hacer POO en C++. Las funciones virtuales permiten expresar diferencias en el comportamiento de objetos de clases de una misma jerarquía. Iván Bernal, Ph.D. 10 Polimorfismo (9) Polimorfismo (10) Para demostrar la idea de polimorfismo, se usa la jerarquía de clases de la figura y se presenta código que utiliza la clase base y no se interesa de los detalles de los otros tipos. Si un nuevo tipo digamos Hexagon es añadido con herencia, el código presentado también trabajará. Iván Bernal, Ph.D. 11 Iván Bernal, Ph.D. 12
Polimorfismo (11) void HacerAlgo(Shape& s) { s.erase(); //... s.draw(); } Esta función manipula cualquier objeto Shape: : es decir, es independiente del tipo específico del objeto que se pase como referencia en la función. Polimorfismo (12) En algún lugar e nuestro programa se utilizará la función HacerAlgo() (): Circle c; Triangle t; Line l; HacerAlgo(c); HacerAlgo(t); HacerAlgo(l); Iván Bernal, Ph.D. 13 Iván Bernal, Ph.D. 14 Polimorfismo (13) Considerando la línea: HacerAlgo() (); Un objeto Circle se pasa como argumento a una función que espera un Shape. Puesto que un Circle es un Shape, el círculo puede tratarse como un Shape dentro de HacerAlgo( ). Cualquier mensaje que HacerAlgo() puede enviar a un Shape,, puede ser aceptado por un Circle. Iván Bernal, Ph.D. 15 Polimorfismo (14) En la función HagaAlgo(), se tiene las líneas: s.erase(); //... s.draw(); Debe notarse que no dice si s es un círculo (Circle) haga tal cosa, y si s es un cuadrado (Square) Square),, haga esta otra, etc. Escribir código que cheque por todos los tipos que pueden pasar como Shape, puede ser complicado y confuso y debe ser alterado cada vez que se añada un nuevo tipo Shape. Iván Bernal, Ph.D. 16
Polimorfismo (15) En el ejemplo se asume que cualquier objeto pasado como argumento es del tipo Shape,, y cada uno sabe como erase( ) y draw( ),, y sabe como manejar los detalles de la operación. Lo impresionante del código en HagaAlgo( ) es que de algún modo, se obtiene el comportamiento deseado. Invocando draw( ) para Circle hace que diferente código se ejecute comparado con cuando se invoca draw( ) para un Square ó un Line. Al enviar el mensaje draw( ) al objeto anónimo Shape,, se invoca el código específico basado en el tipo real de Shape. Polimorfismo (16) Esto es muy interesante ya que, como se mencionó, el compilador no puede saber exactamente el tipo con el cual estará trabajando. De forma intuitiva se podría pensar que se llamen las versiones de erase( ) y draw( ) pero de Shape,, y no las versiones específicas de Circle, Square,, o Line. Sin embargo, el comportamiento adecuado se tiene gracias al Polimorfismo. El compilador y el sistema de soporte del lenguaje al tiempo de ejecución se encargan de los detalles. Iván Bernal, Ph.D. 17 Iván Bernal, Ph.D. 18 Polimorfismo (17) Lo que el programador necesita saber es que polimorfismo está presente y disponible. Sobre todo el programador debe saber como diseñar usando Polimorfismo. Si una función es virtual,, cuando se envíe un mensaje a un objeto, de alguna forma el objeto hará la cosa correcta. Creación y destrucción de objetos (1) Técnicamente, el dominio de la POO es la introducción de tipos de datos abstractos, herencia y polimorfismo, pero existen otros aspectos importantes a revisarse. Construcción y destrucción de objetos. En donde están los datos de un objeto? Cómo se controla el tiempo de vida de un objeto? Iván Bernal, Ph.D. 19 Iván Bernal, Ph.D. 20
Creación y destrucción de objetos (2) C++ ofrece dos alternativas: El almacenamiento y tiempo de vida de un objeto puede determinarse el momento de la compilación, se ubican los objetos en el stack ó almacenamiento estático. Las variables en el stack se denominan automáticas (automatic) ó de alcance determinado (scoped). El área de almacenamiento estático es una sección de memoria que se reserva antes de que el programa inicie su ejecución. Creación y destrucción de objetos (3) Usar las dos opciones ayuda a obtener menores tiempos de ejecución del programa en ciertas tareas, pero se pierde flexibilidad. Se debe saber el número exacto, el tiempo de vida y el tipo de los objetos, al momento de escribir el programa. Iván Bernal, Ph.D. 21 Iván Bernal, Ph.D. 22 Creación y destrucción de objetos (4) La segunda opción es crear objetos dinámicamente en un área de memoria llamada heap. No se conoce sino hasta el tiempo de ejecución del programa cuantos objetos se necesitan, cuál es su tiempo de vida y cual es su tipo exacto. Si se necesita un objeto adicional, se lo construye en el heap,, utilizando la palabra clave new.. Cuando no se requiere más un objeto y se desea liberar el almacenamiento asociado, se desecha el objeto utilizando la palabra clave delete. Creación y destrucción de objetos (5) Debido a que la reserva del almacenamiento se lo realiza dinámicamente durante la ejecución del programa, el tiempo requerido para reservar espacio en el heap es mucho mayor que si se ubicaran los objetos en el stack. Pero se obtiene mayor flexibilidad, esencial para resolver algunos problemas de programación. Cuando se crea el objeto en el heap,, el compilador no sabe el tiempo de vida del objeto. En C++, es tarea del programador determinar cuando debe destruirse un objeto, utilizando delete. Iván Bernal, Ph.D. 23 Iván Bernal, Ph.D. 24