que-es-y-cuando-usar-polimorfismo-en-programacion

Qué es y cómo usar el polimorfismo

El polimorfismo es el último pilar que nos falta por ver de la programación orientada a objetos. El polimorfismo es la capacidad de que un objeto pueda comportarse de diferentes maneras según el contexto en el que se utilice.

Lo cual es una definición muy teórica y muy bonita, pero poco práctica. Dicho de otra forma,

Polimorfismo es una forma muy compleja de decir que un alcalde es una persona

Hay que reconocer que el polimorfismo, como palabra, suena muy bien. Es de esas palabras que te sientes listo sólo de decirla. Qué bien suena 🤯. Pero, en realidad no es tan difícil como parece al principio.

Básicamente el polimorfismo consiste en que,

Un objeto de una clase hija de otra, puede ocupar una variable de la clase padre, pero preservando su comportamiento.

Vale, eso sigue sonando complicado. Mejor vamos a verlo con un ejemplo de código. Allá vamos a por otro clásico de la POO ¡ejemplos con frutas!

Supongamos que tenemos una clase padre Fruta, y dos clases hijas derivadas Manzana y Naranja

class Fruta
class Manzana  extends  Fruta
class Naranja  extends  Fruta
/// ... otras frutas

Lo que dice el polimorfismo es que una Naranja puede ocupar el lugar (una variable) de tipo Fruta. Así que,

// 👍 caso normal, objeto fruta en variable fruta
Fruta miFruta = new Fruta();

// 🎭 ¡POLIMORFISMO! Puedo meter una naranja en una variable tipo fruta
Fruta miFruta = new Naranja();

// ❌ esto NO puedes hacerlo, porque no todas las frutas son naranjas
Naranja miNaranja = new Fruta();

O, volviendo al ejemplo del alcalde, si tienes un autobús donde pueden montarse Persona, pueden subirse Alcaldes. Y alumnos. Y fontaneros. Porque (momento hippie 🦄🌈) todos son personas.

curso-poo-autobus

Un autobús lleva personas (con sentimientos)

Cómo funciona el polimorfismo

Meter objetos derivados en variables de tipo uno de sus clases padres es solo la mitad de la “gracia”. Para terminar de ver el polimorfismo, necesitamos hablar de cómo se comporta un objeto cuando ocupa una variable de otro tipo que no es el suyo propio.

Para esto tenemos que recordar que las clases hijas, además de heredar propiedades y métodos que tienen sus padres, pueden sobreescribir algunos o todos de ellos.

Por ejemplo, supongamos que Fruta y Manzana tienen un método muy sencillo, que escribe en consola su nombre. “Fruta” y “Manzana” respectivamente.

class Fruta 
{
    DiTuNombre() { console.write("Fruta") }
}

class Manzana extends  Fruta
{
	// sobreescribo el método
    DiTuNombre() { console.write("Manzana") }
}

Lo que queremos saber es qué pasa cuando jugamos a meter objetos de un tipo en otro tipo. Tenemos los tres supuestos válidos.

// fruta "normal"
Fruta miFruta = new Fruta();
miFruta.DiTuNombre();                   // 👍 Caso fácil, imprime "Fruta"

// naranja "naranja"
Naranja miNaranja = new Naranja();     
miNaranja.DiTuNombre();                 // 👍 Caso fácil, imprime "Naranja"

// aquí el lio, naranja como fruta
Fruta miNaranjaFruta = new Naranja();
miNaranjaFruta.DiTuNombre();            // 🎭 POLIMORFISMO, imprime "Naranja"

Veamos lo que ha pasado:

  • 👍Fruta como fruta, y naranja como naranja, no tienen más misterio. Cada uno llama al método que le toca, y santas pascuas.
  • 🎭El caso “complicado” es una Naranja guardada en Fruta. Aquí entra el polimorfismo, y el resultado es que imprime “naranja”.

Que en realidad no es tan complicado. Al llamar a un método, se llama al método del objeto que tengas. El tipo de variable, da igual, lo que importa es lo que realmente tienes guardado en esa variable.

O, dicho de otra forma, si tu le preguntas su profesión a un Alcalde, a un profesor, o a un fontanero, te va a decir su profesión. Da igual que esté sentado en su silla de su oficina, en el asiento de un autobús, o en un banco del parque.

¿Se ha entendido? Síganme para más consejos de personas sentadas en sillas 🪑.

Cuando usar el polimorfismo

Hay diferentes situaciones donde podéis usar el polimorfismo. Cubrir todos los casos sería imposible. Pero os voy a comentar dos importantes, que usaréis frecuentemente. Además son buenos ejemplos de uso de polimorfismo.

Para el ejemplo, que imagina que tienes una forma geométrica Figura, especializada en tres clases hijas Rectangulo, Triangulo y Circulo.

class Figura { 	
	Dibujar() { /* ... */ }
	
	/* aqui más cosas... */
}

class Rectangulo extends Figura { 	
	Dibujar() { /* ... */ }
}

class Triangulo extends Figura { 	
	Dibujar() { /* .. */ }
}

class Circulo extends Figura { 	
	Dibujar() { /*... */ }
}

Todas tienen su método Dibujar() sobreescrito para dibujarse correctamente en la pantalla, cada una con sus peculiaridades.

Funciones con parámetro de clases padre

El primer ejemplo, hacer una función que recibe una Figura, y por tanto puede recibir Rectangulo, Triangulo y Circulo.

function ProcesarFigura(Figura figura)
{
	figura.Dibujar();
	
	// hacer otras cosas con figura
}

El tener el método sobre cargado, la función simplemente llama a Dibujar(), y cada objeto se dibujará correctamente.

De esta forma la función ProcesarFigura no tiene que saber detalles de cómo dibujar los objetos, eso lo sabe cada objeto. Ella solo sabe que quiere dibujar objetos, no cómo hacerlo.

Además, nos permite reutilizar el código. La función ProcesarFigura puede funcionar en el futuro, aunque añadas más tipos de Figuras.

Colecciones de clases padre

Otro ejemplo muy habitual, es tener una colección de elementos donde queremos guardar varios tipo más específicos. Por ejemplo, tenéis una colección de elementos Figura.

Figuras[] ListadoFiguras;

Gracias al polimorfismo, dentro podemos almacenar objetos de tipo Rectangulo, Triangulo, Circulo.

Finalmente, cuando queremos dibujarlos recorremos toda la colección, e invocamos al método Dibujar().

ListadoFiguras.foreach(Dibujar);

Cada figura se dibujará correctamente, llamando al método oportuno.

Ejemplo de un polimorfismo en diferentes lenguajes

Vamos a ver cómo sería la sintaxis para hacer una polimorfismo entre clases en diferentes lenguajes de programación.

En C++, definimos clase base Fruta con un método virtual DiTuNombre() que devuelve “Soy una fruta”. Luego creamos una subclase llamada Naranja que hereda de Fruta y sobrescribe el método DiTuNombre() devolviendo “Soy una naranja”

// Definición de la clase base Fruta
class Fruta {
public:
    // Método virtual que muestra información genérica de la fruta
    virtual std::string DiTuNombre() {
        return "Soy una fruta";
    }
};

// Definición de la subclase Naranja que hereda de Fruta
class Naranja : public Fruta {
public:
    // Método sobreescrito que muestra información específica de la naranja
    std::string DiTuNombre() override {
        return "Soy una naranja";
    }
};

int main() {
	// Crear instancias de Fruta y Naranja
    Fruta* fruta = new Fruta();
    Fruta* naranja = new Naranja();

    std::cout << fruta->DiTuNombre() << std::endl; // Output: Soy una fruta
    std::cout << naranja->DiTuNombre() << std::endl; // Output: Soy una naranja
}

El caso de C# es muy similar al anterior. La clase base Fruta tiene un método virtual DiTuNombre(). La clase Naranja hereda de Fruta y sobrescribe el método con su propio DiTuNombre().

// Definición de la clase base Fruta
public class Fruta
{
    public virtual string DiTuNombre()
    {
        return $"Soy una fruta";
    }
}

// Definición de la subclase Naranja que hereda de Fruta
public class Naranja : Fruta
{
    // Método sobreescrito que muestra información específica de la naranja
    public override string DiTuNombre()
    {
        return $"Soy una naranja";
    }
}

// Crear instancias de Fruta y Naranja
Fruta fruta = new Fruta();
Fruta naranja = new Naranja();

Console.WriteLine(fruta.DiTuNombre()); // Output: Esta es una fruta llamada Manzana.
Console.WriteLine(naranja.DiTuNombre()); // Output: Esta es una fruta llamada Naranja. Es de color naranja.

Ahora veamos el polimorfismo en JavaScript. Nuevamente creamos la clase base Fruta con su método DiTuNombre(). La clase Naranja extiende la clase Fruta y sobrescribe el método.

// Definición de la clase base Fruta
class Fruta {
    // Método virtual que muestra información genérica de la fruta
    DiTuNombre() {
        return "Soy una fruta";
    }
}

// Definición de la subclase Naranja que hereda de Fruta
class Naranja extends Fruta {
    // Método sobreescrito que muestra información específica de la naranja
    DiTuNombre() {
        return "Soy una naranja";
    }
}

// Crear instancias de Fruta y Naranja
const fruta = new Fruta();
const naranja = new Naranja();

// Llamar al método DiTuNombre() en ambas instancias
console.log(fruta.DiTuNombre()); // Output: Soy una fruta
console.log(naranja.DiTuNombre()); // Output: Soy una naranja

Finalmente en Python también podemos definir nuestra clase Fruta con un método di_tu_nombre(). Luego creamos una subclase Naranja que hereda de Fruta y sobrescribe el método di_tu_nombre()

# Definición de la clase base Fruta
class Fruta:
    # Método virtual que muestra información genérica de la fruta
    def di_tu_nombre(self):
        return "Soy una fruta"

# Definición de la subclase Naranja que hereda de Fruta
class Naranja(Fruta):
    # Método sobreescrito que muestra información específica de la naranja
    def di_tu_nombre(self):
        return "Soy una naranja"

# Crear instancias de Fruta y Naranja
fruta = Fruta()
naranja = Naranja()

# Llamar al método di_tu_nombre() en ambas instancias
print(fruta.di_tu_nombre())  # Output: Soy una fruta
print(naranja.di_tu_nombre())  # Output: Soy una naranja

Como vemos, los conceptos de polimorfismo son más o menos parecidos en todos los lenguajes que incluyen programación a objetos, más allá de las diferencias de sintaxis.

¿De donde viene el polimorfismo?

Igual que hemos hecho con los otros pilares, vamos a ver los motivos que llevaron a inventar el POLIMORFISMO. Recordemos que la gente ya hacía agrupaciones de datos. Pero si cualquier para del programa podía modificarlos, se montaban unos líos muy interesantes e inmantenibles.

pilares-oop

Los cuatro pilares del OOP

Así que inventaron la ENCAPSULACION. Al estar encapsulados, cada clase tenía que ser independiente, lo cuál obligaba a tener mucho código duplicado. Para permitir reaprovechar código, se introduce la HERENCIA.

Pero con la herencia por si sola, no evitamos del todo tener que repetir código. Por ejemplo, en el caso de Rectangulo, Triangulo, Circulo, tendríamos que hacer todas las funciones que trabajan con ellas, repetidas para cada tipo. Para evitar esto se introduce el POLIMORFISMO.

En resumen:

  • La encapsulación impide que me toquen los objetos de forma incontrolada
  • La herencia permites que no tenga que repetir el código de las clases
  • El polimorfismo completa la herencia, y me permite no tener que repetir el código de lo que usa mis clases (colecciones, funciones)

Por último, además desde el punto de vista de la abstracción y el modelado, el POLIMORFISMO tiene sentido. Por la HERENCIA una Naranja tiene las mismas propiedades que Fruta, porque es una especialización de Fruta (herencia).