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)?
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»
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.