1.6. Objetos intercambiables gracias al polimorfismo

Cuando se manejan jerarquías de tipos, se suele tratar un objeto no como el tipo específico si no como su tipo base. Esto le permite escribir código que no depende de los tipos específicos. En el ejemplo de la figura, las funciones manipulan figuras genéricas sin preocuparse de si son círculos, cuadrados, triángulos, etc. Todas las figuras se pueden dibujar, borrar y mover, pero estas funciones simplemente envían un mensaje a un objeto figura, sin preocuparse de cómo se las arregla el objeto con cada mensaje.

Semejante código no está afectado por la adición de nuevos tipos, y añadir nuevos tipos es la forma más común de extender un programa orientado a objetos para tratar nuevas situaciones. Por ejemplo, puede derivar un nuevo subtipo de figura llamado pentágono sin modificar las funciones que tratan sólo con figuras genéricas. Esta habilidad para extender un programa fácilmente derivando nuevos subtipos es importante porque mejora enormemente los diseños al mismo tiempo que reduce el coste del mantenimiento del software.

Hay un problema, no obstante, con intentar tratar un tipo derivado como sus tipos base genéricos (círculos como figuras, bicicletas como vehículos, cormoranes como pájaros, etc). Si una función va a indicar a una figura genérica que se dibuje a sí misma, o a un vehículo genérico que se conduzca, o a un pájaro genérico que se mueva, el compilador en el momento de la compilación no sabe con precisión qué pieza del código será ejecutada. Este es el punto clave - cuando el mensaje se envía, el programador no quiere saber qué pieza de código será ejecutada; la función dibujar() se puede aplicar a un círculo, un cuadrado, o un triángulo, y el objeto ejecutará el código correcto dependiendo de tipo específico. Si no sabe qué pieza del código se ejecuta, ¿qué hace? Por ejemplo, en el siguiente diagrama el objeto ControladorDePájaro trabaja con los objetos genéricos Pájaro, y no sabe de qué tipo son exactamente. Esto es conveniente desde la perspectiva del ControladorDePájaro, porque no hay que escribir código especial para determinar el tipo exacto de Pájaro con el que está trabajando, o el comportamiento del Pájaro. Entonces, ¿qué hace que cuando se invoca mover() ignorando el tipo específico de Pájaro, puede ocurrir el comportamiento correcto (un Ganso corre, vuela, o nada, y un Pingüino corre o nada)?

Polimorfismo

Figura 1.8. Polimorfismo


La respuesta es el primer giro en programación orientada a objetos: el compilador no hace una llamada a la función en el sentido tradicional. La llamada a función generada por un compilador no-OO provoca lo que se llama una ligadura temprana (early binding), un término que quizá no haya oído antes porque nunca ha pensado en que hubiera ninguna otra forma. Significa que el compilador genera una llamada al nombre de la función específica, y el enlazador resuelve esta llamada con la dirección absoluta del código que se ejecutará. En POO, el programa no puede determinar la dirección del código hasta el momento de la ejecución, de modo que se necesita algún otro esquema cuando se envía un mensaje a un objeto genérico.

Para resolver el problema, los lenguajes orientados a objetos usan el concepto de ligadura tardía (late binding). Cuando envía un mensaje a un objeto, el código invocado no está determinado hasta el momento de la ejecución. El compilador se asegura de que la función existe y realiza una comprobación de tipo de los argumentos y el valor de retorno (el lenguaje que no realiza esta comprobación se dice que es débilmente tipado), pero no sabe el código exacto a ejecutar.

Para llevar a cabo la ligadura tardía, el compilador de C++ inserta un trozo especial de código en lugar de la llamada absoluta. Este código calcula la dirección del cuerpo de la función, usando información almacenada en el objeto (este proceso se trata con detalle en el Capítulo 15). De este modo, cualquier objeto se puede comportar de forma diferente de acuerdo con el contenido de este trozo especial de código. Cuando envía un mensaje a un objeto, el objeto comprende realmente qué hacer con el mensaje.

Es posible disponer de una función que tenga la flexibilidad de las propiedades de la ligadura tardía usando la palabra reservada virtual. No necesita entender el mecanismo de virtual para usarla, pero sin ella no puede hacer programación orientada a objetos en C++. En C++, debe recordar añadir la palabra reservada virtual porque, por defecto, los métodos no se enlazan dinámicamente. Los métodos virtuales le permiten expresar las diferencias de comportamiento en clases de la misma familia. Estas diferencias son las que causan comportamientos polimórficos.

Considere el ejemplo de la figura. El diagrama de la familia de clases (todas basadas en la misma interfaz uniforme) apareció antes en este capítulo. Para demostrar el polimorfismo, queremos escribir una única pieza de código que ignore los detalles específicos de tipo y hable sólo con la clase base. Este código está desacoplado de la información del tipo específico, y de esa manera es más simple de escribir y más fácil de entender. Y, si tiene un nuevo tipo - un Hexágono, por ejemplo - se añade a través de la herencia, el código que escriba funcionará igual de bien para el nuevo tipo de Figura como para los tipos anteriores. De esta manera, el programa es extensible.

Si escribe una función C++ (podrá aprender dentro de poco cómo hacerlo):

void hacerTarea(Figura& f) {
    f.borrar();
    // ...
    f.dibujar();
}

Esta función se puede aplicar a cualquier Figura, de modo que es independiente del tipo específico del objeto que se dibuja y borra (el «&» significa «toma la dirección del objeto que se pasa a hacerTarea()», pero no es importante que entienda los detalles ahora). Si en alguna otra parte del programa usamos la función hacerTarea():

Circulo c;
Triangulo t;
Linea l;
hacerTarea(c);
hacerTarea(t);
hacerTarea(l);

Las llamadas a hacerTarea() funcionan bien automáticamente, a pesar del tipo concreto del objeto.

En efecto es un truco bonito y asombroso. Considere la línea:

hacerTarea(c);

Lo que está ocurriendo aquí es que está pasando un Círculo a una función que espera una Figura. Como un Círculo es una Figura se puede tratar como tal por parte de hacerTarea(). Es decir, cualquier mensaje que pueda enviar hacerTarea() a una Figura, un Círculo puede aceptarlo. Por eso, es algo completamente lógico y seguro.

A este proceso de tratar un tipo derivado como si fuera su tipo base se le llama upcasting (moldeado hacia arriba[16]). El nombre cast (molde) se usa en el sentido de adaptar a un molde y es hacia arriba por la forma en que se dibujan los diagramas de clases para indicar la herencia, con el tipo base en la parte superior y las clases derivadas colgando debajo. De esta manera, moldear un tipo base es moverse hacia arriba por el diagrama de herencias: «upcasting»

Upcasting

Figura 1.9. Upcasting


Todo programa orientado a objetos tiene algún upcasting en alguna parte, porque así es como se despreocupa de tener que conocer el tipo exacto con el que está trabajando. Mire el código de hacerTarea():

f.borrar();
// ...
f.dibujar();

Observe que no dice «Si es un Círculo, haz esto, si es un Cuadrado, haz esto otro, etc.». Si escribe un tipo de código que comprueba todos los posibles tipos que una Figura puede tener realmente, resultará sucio y tendrá que cambiarlo cada vez que añada un nuevo tipo de Figura. Aquí, sólo dice «Eres una figura, sé que te puedes borrar() y dibujar() a ti misma, hazlo, y preocúpate de los detalles».

Lo impresionante del código en hacerTarea() es que, de alguna manera, funciona bien. Llamar a dibujar() para un Círculo ejecuta diferente código que cuando llama a dibujar() para un Cuadrado o una Línea, pero cuando se envía el mensaje dibujar() a un Figura anónima, la conducta correcta sucede en base en el tipo real de Figura. Esto es asombroso porque, como se mencionó anteriormente, cuando el compilador C++ está compilando el código para hacerTarea(), no sabe exactamente qué tipos está manipulando. Por eso normalmente, es de esperar que acabe invocando la versión de borrar() y dibujar() para Figura, y no para el Círculo, Cuadrado, o Línea específico. Y aún así ocurre del modo correcto a causa del polimorfismo. El compilador y el sistema se encargan de los detalles; todo lo que necesita saber es que esto ocurre y lo que es más importante, cómo utilizarlo en sus diseños. Si un método es virtual, entonces cuando envíe el mensaje a un objeto, el objeto hará lo correcto, incluso cuando esté involucrado el upcasting.



[16] N. de T: En el libro se utilizará el término original en inglés debido a su uso común, incluso en la literatura en castellano.