martes, 22 de mayo de 2012

Implementación de polimorfismo en funciones y funciones virtuales de objetos


POLIMORFISMO



En programación orientada a objetos (POO), una función virtual o método virtual es una función cuyo comportamiento, al ser declarado "virtual", es determinado por la definición de una función con la misma cabecera en alguna de sus subclases. Este concepto es una parte muy importante del polimorfismo en la POO.

El concepto de función virtual soluciona los siguientes problemas:


En POO, cuando una clase derivada hereda de una clase base, un objeto de la clase derivada puede ser referido (o coercionado) tanto como del tipo de la clase base como del tipo de la clase derivada. Si hay funciones de la clase base redefinidas por la clase derivada, aparece un problema cuando un objeto derivado ha sido cohercionado como del tipo de la clase base. Cuando un objeto derivado es referido como del tipo de la base, el comportamiento de la llamada a la función deseado es ambiguo.
Distinguir entre virtual y no virtual sirve para resolver este problema. Si la función en cuestión es designada "virtual", se llamará a la función de la clase derivada (si existe). Si no es virtual, se llamará a la función de la clase base.


Ejemplo

Por ejemplo, una clase base Animal podría tener una función virtual come. La subclase Pez implementaría come() de forma diferente que la subclase Lobo, pero se podría invocar a come() en cualquier instancia de una clase referida como Animal, y obtener el comportamiento de come() de la subclase específica.

Esto permitiría a un programador procesar una lista de objetos de la clase Animal, diciendo a cada uno que coma (llamando a come()), sin saber qué tipo de animales hay en la lista. Tampoco tendría que saber cómo come cada animal, o cuántos tipos de animales puede llegar a existir.

El siguiente, es un ejemplo en C++:

# include <iostream>
using namespace std;
class Animal
{
public:
virtual void come() { cout << "Yo como como un animal genérico.\n"; }
};

class Lobo : public Animal
{
public:
void come() { cout << "¡Yo como como un lobo!\n"; }
};

class Pez : public Animal
{
public:
void come() { cout << "¡Yo como como un pez!\n"; }
};

class OtroAnimal : public Animal
{
};

int main()
{
Animal *unAnimal[4];
unAnimal[0] = new Animal();
unAnimal[1] = new Lobo();
unAnimal[2] = new Pez();
unAnimal[3] = new OtroAnimal();

for(int i = 0; i < 4; i++) {
unAnimal[i]->come();
}

for (int i = 0; i < 4; i++) {
delete unAnimal[i];
}
return 0;
}

Salida con el método virtual come:
Yo como como un animal genérico.
¡Yo como como un lobo!
¡Yo como como un pez!
Yo como como un animal genérico.
Salida sin el método virtual come:
Yo como como un animal genérico.
Yo como como un animal genérico.
Yo como como un animal genérico.
Yo como como un animal genérico.


Las Clases abstractas y funciones virtuales puras


Una función virtual pura o método virtual puro es una función virtual que necesita ser implementada por una clase derivada que no sea abstracta. Las clases que contienen métodos virtuales puros son denominadas "abstractas". Éstas no pueden ser instanciadas directamente, y una subclase de una clase abstracta sólo puede ser instanciada directamente si todos los métodos virtuales puros han sido implementados por esa clase o una clase padre.

Los métodos virtuales puros normalmente tienen una declaración (cabecera) pero no tienen definición (implementación). Como ejemplo, una clase base abstracta como "SimboloMatematico" puede ofrecer una función virtual pura como hazOperacion, y las clases derivadas "Suma" y "Resta" pueden implementar hazOperacion para ofercer implementaciones concretas. La implementación de hazOperacion no tendría sentido en la clase "SimboloMatematico" porque "SimboloMatematico" es un concepto abstracto cuyo comportamiento es definido solamente por cada tipo (subclase) de "SimboloMatematico" dado.
De forma similar, una subclase dada de "SimboloMatematico" no sería completa sin una implementación de hazOperacion. Aunque los métodos virtuales puros normalmente no tienen implementación en la clase que los declara, en C++ permite hacer esto, ofreciendo un comportamiento por omisión en el que la clase derivada puede delegar si es apropiado.

Las funciones virtuales puras también son utilizadas donde las declaraciones de métodos se utilizan para definir una interfaz para la que las clases derivadas proveerán todas las implementaciones. Una clase abstracta sirviendo como interfaz contiene sólo funciones virtuales puras, y ningún miembro de datos (variables, constantes, etc.) ni métodos ordinarios. El uso de clases puramente abstractas como interfaces funciona en C++ ya que éste soporta herencia múltiple. Debido a que muchos lenguajes orientados a objetos no soportan herencia múltiple, normalmente ofrecen un mecanismo por separado para hacer interfaces. Esto es así por ejemplo en Java.

C++

En C++, las funciones virtuales puras son declaradas utilizando una sintaxis especial = 0 como se muestra a continuación:
class B {
virtual void una_funcion_virtual_pura() = 0;
};
La declaración de la función virtual pura ofrece sólo la cabecera del método. Normalmente no se ofrece una implementación de la función virtual pura en una clase abstracta, pero puede ofrecerse. Toda clase hija no-abstracta continúa necesitando redefinir el método, pero la implementación ofrecida por la clase abstracta puede ser llamada de esta forma:
void Abstracta::virtual_pura() {
// haz algo
}

class Hija : Abstracta {
virtual void virtual_pura(); // ya no es abstracta; puede ser instanciada
};

void Hija::virtual_pura() {
Abstracta::virtual_pura(); // se ejecuta la implementación de la clase abstracta
}
El compilador sabe a qué implementación del método llamar en tiempo de ejecución creando una tabla de punteros a todas las funciones virtuales de una clase, llamada vtable o tabla virtual.


Destructores virtuales

Los lenguajes orientados a objetos normalmente gestionan la reserva y la liberación de memoria automáticamente cuando los objetos son creados y destruidos. Sin embargo, algunos lenguajes orientados a objetos permiten implementar un método destructor personalizado si se desea. Uno de estos lenguajes es C++, y como se ilustra en el siguiente ejemplo, es importante para una clase base de C++ el tener un destructor virtual para asegurar que se llamará siempre al destructor de la mayoría de clases derivadas. En el ejemplo siguiente, sin destructor virtual, mientras que borrar una instancia de la clase B llamará correctamente a los destructores para la clase B y para la clase A si se borra como instancia de B, una instancia de B borrada mediante un puntero a su clase base A fallará al no llamar al destructor para B.

# include <iostream>

class A
{
public:

A() { }
~A() { std::cout << "Destruye A" << std::endl; }
};

class B : public A
{
public:

B() { }
~B() { std::cout << "Destruye B" << std::endl; }
};

int main()
{
A* b1 = new B;
B* b2 = new B;

delete b1; // Sólo se llama a ~A() aunque b1 sea una instancia de la clase B
// porque ~A() no se ha declarado como virtual
delete b2; // Llama a los destructores ~B() y ~A()

return 0;
}
Salida:
Destruye A
Destruye B
Destruye A

La declaración correcta del destructor para la clase A como virtual ~A() asegurará que el destructor para la clase B es llamado en ambos casos del ejemplo anterior.


Polimorfismo y Funciones virtuales

El Polimorfismo (implementado en C++ con funciones virtuales) es la tercera característica esencial de un lenguaje orientado a objetos, después de la abstracción de datos y la herencia.
De hecho, nos provee de otra dimensión para la separación entre interfaz y la implementación, desacoplando el qué del cómo. El Polimorfismo permite mejorar la organización del código y su legibilidad así como la creación de programas extensibles que pueden "crecer" no sólo durante el desarrollo del proyecto, si no también cuando se deseen nuevas características.
La encapsulación crea nuevos tipos de datos combinando características y comportamientos. El control de acceso separa la interfaz de la implementación haciendo privados (private) los detalles. Estos tipos de organización son fácilmente entendibles por cualquiera que venga de la programación procedimental. Pero las funciones virtuales tratan de desunir en términos de tipos. En el Capítulo 14, usted vió como la herencia permitía tratar a un objeto como su propio tipo o como a su tipo base. Esta habilidad es básica debido a que permite a diferentes tipos (derivados del mismo tipo base) ser tratados como si fueran un único tipo, y un único trozo de código es capaz de trabajar indistintamente con todos. Las funciones virtuales permiten a un tipo expresar sus diferencias con respecto a otro similar si ambos han sido derivados del mismo tipo base. Esta distinción se consigue modificando las conductas de las funciones a las que se puede llamar a través de la clase base.

Evolución de los programadores de C++

Los programadores de C parecen conseguir pasarse a C++ en tres pasos. Al principio, como un "C mejorado", debido a que C++ le fuerza a declarar todas las funciones antes de usarlas y a que es mucho más sensible a la forma de usar las variables. A menudo se pueden encontrar errores en un programa C simplemente recompilándolo con un compilador de C++.

El segundo paso es la "programación basada en objetos", que significa que se pueden ver fácilmente los beneficios de la organización del código al agrupar estructuras de datos junto con las funciones que las manejan, la potencia de los constructores y los destructores, y quizás algo de herencia simple. La mayoría de los programadores que han trabajado durante un tiempo con C ven la utilidad de esto porque es lo que intentan hacer cuando crean una librería. Con C++ usted recibe la ayuda del compilador.
Usted se puede encontrar atascado en el nivel de "programación basada en objetos" debido a que es de fácil acceso y no requiere mucho esfuerzo mental. Es también sencillo sentir cómo está creando tipos de datos - usted hace clases y objetos, envía mensajes a esos objetos, y todo es bonito y pulcro.

Pero no sea tonto. Si se para aquí, se está perdiendo una de las más importantes partes del lenguaje, que significa el salto a la verdadera programación orientada a objetos. Y esto se consigue únicamente con las funciones virtuales.

Las funciones virtuales realzan el concepto de tipo en lugar de simplemente encapsular código dentro de estructuras y dejarlo detrás de un muro, por lo que son, sin lugar a dudas, el concepto más difícil a desentrañar por los nuevos programadores en C++. Sin embargo, son también el punto decisivo para comprender la programación orientada a objetos. Si no usa funciones virtuales, todavía no entiende la POO.

Debido a que las funciones virtuales están intimamente unidas al concepto de tipo, y los tipos son el núcleo de la programación orientada a objetos, no existe analogía a las funciones virtuales dentro de los lenguajes procedurales. Como programador procedural, usted no tiene referente con el que comparar las funciones virtuales, al contrario de las otras características del lenguaje. Las características de un lenguaje procedural pueden ser entendidas en un nivel algorítmico, pero las funciones virtuales deben ser entendidas desde el punto de vista del diseño.

No hay comentarios:

Publicar un comentario