que-es-una-clase-abstracta-en-programacion

Qué son y cómo usar las clases abstractas

Una clase abstracta es una clase que no puede ser instanciada directamente. Es decir, no se pueden crear objetos directamente a partir de ella.

Para utilizar una clase abstracta, es necesario heredar de ella y proporcionar una implementación completa para todos los métodos abstractos definidos en la clase abstracta.

¿Para qué puedes querer una clase que no puede ser instanciada? Una clase abstracta se utiliza para definir una estructura común que puedo reusar mediante herencia, pero que no está lista para ser utilizada directamente en el código.

En último término, las clases abstractas se utilizan para modelar conceptos abstractos o genéricos que no tienen una implementación concreta, pero son la base común de otros objetos.

Como es posible que te hayas quedado un poco así 🤔 con la definición, vamos a verlo mejor con un caso cotidiano.

Propósito de las clases abstractas

Imagina que entras en un concesionario de coches. Te acercas a uno de los vendedores, y tienes la siguiente conversación:

- Hola buenas, querría comprar un coche

+ Por supuesto, que quiere comprar ¿una Berlina, un Cupé, un Sedán?

- No no, yo querría comprar un coche “como concepto”

curso-poo-clase-abstracta

Dependiendo de como pilles el día al vendedor, te va a explicar más o menos amablemente que tu no puedes comprar un coche “como concepto”. Puedes comprar un subtipo particular de coche.

Pero un coche “como concepto” es una abstracción. Es una idea que recoge las características comunes de todos los tipos de coches. Y las ideas no se puede comprar 🤷 (y luego te echaría del concesionario).

En este caso, el Coche es una clase abstracta. Es una idea que representa todo lo que tienen en común los coches. Y no se puede instanciar.

Luego se especializa en subclases, como Berlina, Cupè, Sedán, que son especializaciones de Coche, y sí puedes instanciar.

Métodos y clases abstractas

Métodos abstractos

Los métodos abstractos son métodos declarados, pero que no tienen una implementación en la clase donde se declaran (es decir, básicamente son métodos que no tienen “cuerpo”, están “vacíos”).

Al no tener una implementación, ninguna clase que tenga un método abstracto puede ser instanciada. Por tanto, inmediatamente pasará a ser una clase abstracta.

Si intentamos instanciar una clase abstracta el compilador o el IDE nos dará un error, diciendo que una clase abstracta no puede instanciarse.

Clases abstractas

Las clases abstractas son aquellas que no pueden ser instanciadas directamente, si no que están diseñadas para ser subclaseadas.

Una clase abstracta puede contener tanto métodos abstractos (sin implementación) como métodos concretos (con implementación).

Pero, como hemos comentado, si una clase implementa al menos un método abstracto (sin implementación) obligatoriamente la clase debe ser abstracta.

Ejemplo práctico

Vamos a verlo con nuestro ejemplito de Coche, que es una clase abstracta. Por tanto, no puede ser instanciada directamente.

// Clase abstracta que representa un coche
abstract class Coche
{
	// Método abstracto, sin implementación (sin cuerpo)
	void Conducir();
}

// Intentar crear una instancia de la clase abstracta (esto no es posible)
 Coche cocheAbstracto = new Coche(); // Esto dará un error de compilación

Lo que si podemos hacer es heredar por otras clases que proporcionarán una implementación concreta de sus métodos abstractos.

// Clase que representa una Berlina, derivada de Coche
class Berlina extends Coche
{
	void Conducir()
	{
		Console.Log("Conduciendo una Berlina");
	}
}

// Clase que representa una Berlina, derivada de Coche
class Coupe extends Coche
{
	void Conducir()
	{
		Console.Log("Conduciendo un Coupe");
	}
}

// Crear instancias de las clases derivadas
Coche miBerlina = new Berlina();
Coche miCoupe = new Coupe();

Ejemplos en distintos lenguajes

Veamos ejemplos de declarar clases abstractas en distintos lenguajes de programación

Las clases abstractas en C# se definen utilizando la palabra clave abstract. Estas clases pueden contener métodos abstractos, que deben ser implementados por las clases derivadas.

public abstract class Figura
{
    // Método abstracto
    public abstract double CalcularArea();

    // Método concreto
    public void Mostrar()
    {
        Console.WriteLine("Mostrando figura");
    }
}

public class Circulo : Figura
{
    public double Radio { get; set; }

    public override double CalcularArea()
    {
        return Math.PI * Radio * Radio;
    }
}

// Uso
Circulo circulo = new Circulo { Radio = 5 };
Console.WriteLine(circulo.CalcularArea());
circulo.Mostrar();

En C++, las clases abstractas se definen utilizando al menos una función virtual pura. Una función virtual pura se define con = 0 al final de la declaración de la función.

#include <iostream>
#include <cmath>

class Figura {
public:
    // Método abstracto
    virtual double CalcularArea() const = 0;

    // Método concreto
    void Mostrar() const {
        std::cout << "Mostrando figura" << std::endl;
    }
};

class Circulo : public Figura {
public:
    double Radio;

    double CalcularArea() const override {
        return M_PI * Radio * Radio;
    }
};

// Uso
int main() {
    Circulo circulo;
    circulo.Radio = 5;
    std::cout << circulo.CalcularArea() << std::endl;
    circulo.Mostrar();
    return 0;
}

JavaScript no tiene soporte nativo para clases abstractas, podemos simularlas “más o menos” con distintas técnicas.

class Figura {
    // Constructor opcional para inicializar propiedades comunes
    constructor() {
        if (this.constructor === Figura) {
            throw new Error("No se puede instanciar una clase abstracta.");
        }
    }

    // Método abstracto
    CalcularArea() {
        throw new Error("Debe implementar el método abstracto CalcularArea");
    }

    // Método concreto
    Mostrar() {
        console.log("Mostrando figura");
    }
}

class Circulo extends Figura {
    constructor(radio) {
        super();  // Llama al constructor de la clase base
        this.radio = radio;
    }

    CalcularArea() {
        return Math.PI * this.radio * this.radio;
    }
}

// Uso
try {
    let figura = new Figura();  // Esto lanzará un error
} catch (error) {
    console.error(error.message);
}

let circulo = new Circulo(5);
console.log(circulo.CalcularArea());
circulo.Mostrar();

En TypeScript, las clases abstractas se definen utilizando la palabra clave abstract.

abstract class Figura {
    // Método abstracto
    abstract calcularArea(): number;

    // Método concreto
    mostrar(): void {
        console.log("Mostrando figura");
    }
}

class Circulo extends Figura {
    radio: number;

    constructor(radio: number) {
        super();
        this.radio = radio;
    }

    calcularArea(): number {
        return Math.PI * this.radio * this.radio;
    }
}

// Uso
const circulo = new Circulo(5);
console.log(circulo.calcularArea());
circulo.mostrar();

En Python, las clases abstractas se definen utilizando el módulo abc y la clase ABC. Los métodos abstractos se definen utilizando el decorador @abstractmethod.

from abc import ABC, abstractmethod
import math

class Figura(ABC):
    @abstractmethod
    def calcular_area(self):
        pass

    def mostrar(self):
        print("Mostrando figura")

class Circulo(Figura):
    def __init__(self, radio):
        self.radio = radio

    def calcular_area(self):
        return math.pi * self.radio ** 2

# Uso
circulo = Circulo(5)
print(circulo.calcular_area())
circulo.mostrar()

Buenas prácticas Consejos

Como casi todo en POO, la duda que vais a tener es cuándo y hasta donde usar algo. En el caso de las clases abstractas, es proporcionar implementación de métodos comunes que pueden ser reutilizados por las subclases.

Ejemplo, tienes un objeto DbConnection que gestiona las conexiones de base de datos. Ese objeto tiene una cierta lógica interna, ya “hace cosas”. Pero por si mismo, no es capaz de definirse totalmente.

Para definirse necesita saber más detalles, en este ejemplo de la base de datos que va a usar. Así tendremos DbConnectionMySQL, DbConnectionPostgresSQL, DbConnectionMongo o cosas así.

Aquí tenemos un posible ejemplo de detección de clase abstracta

  • Tenemos una clase que tiene ya lógica
  • Por si misma no funcionar
  • Necesita especializarse en distintos subtipos

Así que posiblemente DbConnection deba ser una clase abstracta.

Hasta aquí la parte “bien”. Pero las clases abstractas también tienen sus problemas, y sus detractores. El mayor problema es que pueden complejizar tu modelo de objetos (y tu vida con ellos), y acabar con un pollo del 15 🐔.

Cuando programas POO “clásica” corres el riesgo de volverte loco usando clases abstractas. Así que acabas teniendo DbConnectionBase, que usa DbRepositoryBase, que usa DbObjectableBase que usa… todo con “base”.

En resumen, si tienes que usar una clase abstracta, úsala. Para eso están, son útiles. Pero úsalas con cabeza, y ten como objetivo el acoplamiento entre clases y los niveles de herencia que vas a usar.

Y si tienes dudas, prefiere otras soluciones como composición o interfaces, que no quedan “tan elegantes” desde el punto de vista de la POO clásica, pero te darán menos problemas en el futuro.