Ir al contenido principal

Clases abstractas e interfaces

En el mundo de la programación orientada a objetos, las clases abstractas y las interfaces son herramientas fundamentales para crear diseños flexibles y reutilizables. Estos conceptos nos permiten definir contratos que otras clases deben cumplir, estableciendo una base común para la implementación de funcionalidades específicas. Mientras que las clases abstractas proporcionan una implementación parcial que puede ser heredada, las interfaces definen únicamente la estructura que deben seguir las clases que las implementan.

Dominar las clases abstractas e interfaces es esencial para aplicar principios sólidos de diseño de software, crear código más mantenible y facilitar el trabajo en equipo. A través de este artículo, exploraremos cómo utilizar estas herramientas para crear jerarquías de clases más eficientes y cómo aprovechar al máximo las capacidades que C# ofrece en este ámbito.

Clases abstractas

Una clase abstracta es una clase que no puede ser instanciada directamente, sino que está diseñada para servir como clase base para otras clases. Las clases abstractas pueden contener tanto métodos implementados como métodos abstractos (sin implementación), proporcionando una base común para sus clases derivadas.

Características de las clases abstractas

Característica Descripción
Palabra clave Se declaran con la palabra clave abstract
Instanciación No pueden ser instanciadas directamente
Métodos abstractos Pueden contener métodos sin implementación
Métodos concretos Pueden contener métodos con implementación completa
Herencia Solo se puede heredar de una clase abstracta
Constructores Pueden tener constructores para inicializar campos

Declaración de una clase abstracta

// Clase abstracta que representa un vehículo genérico
public abstract class Vehiculo
{
    // Propiedades comunes a todos los vehículos
    public string Marca { get; set; }
    public string Modelo { get; set; }
    public int Año { get; set; }
    
    // Constructor de la clase abstracta
    public Vehiculo(string marca, string modelo, int año)
    {
        Marca = marca;
        Modelo = modelo;
        Año = año;
    }
    
    // Método concreto - implementación común
    public void MostrarInformacion()
    {
        Console.WriteLine($"Vehículo: {Marca} {Modelo} ({Año})");
    }
    
    // Método abstracto - debe ser implementado por las clases derivadas
    public abstract void Arrancar();
    
    // Método abstracto con parámetros
    public abstract void Acelerar(int velocidad);
    
    // Método virtual que puede ser sobrescrito (opcional)
    public virtual void Detener()
    {
        Console.WriteLine("El vehículo se está deteniendo...");
    }
}

Implementación de clases derivadas

// Clase que hereda de Vehiculo
public class Coche : Vehiculo
{
    public int NumeroPuertas { get; set; }
    
    // Constructor que llama al constructor de la clase base
    public Coche(string marca, string modelo, int año, int numeroPuertas) 
        : base(marca, modelo, año)
    {
        NumeroPuertas = numeroPuertas;
    }
    
    // Implementación obligatoria del método abstracto
    public override void Arrancar()
    {
        Console.WriteLine($"El coche {Marca} {Modelo} está arrancando con la llave.");
    }
    
    // Implementación obligatoria del método abstracto
    public override void Acelerar(int velocidad)
    {
        Console.WriteLine($"El coche acelera hasta {velocidad} km/h");
    }
    
    // Sobrescribir método virtual (opcional)
    public override void Detener()
    {
        Console.WriteLine("El coche frena suavemente hasta detenerse.");
    }
}

public class Motocicleta : Vehiculo
{
    public bool TieneSidecar { get; set; }
    
    public Motocicleta(string marca, string modelo, int año, bool tieneSidecar) 
        : base(marca, modelo, año)
    {
        TieneSidecar = tieneSidecar;
    }
    
    public override void Arrancar()
    {
        Console.WriteLine($"La motocicleta {Marca} {Modelo} arranca con el botón de encendido.");
    }
    
    public override void Acelerar(int velocidad)
    {
        Console.WriteLine($"La motocicleta acelera hasta {velocidad} km/h con gran agilidad");
    }
}

Ejemplo de uso de clases abstractas

class Program
{
    static void Main()
    {
        // No podemos crear una instancia de Vehiculo directamente
        // Vehiculo vehiculo = new Vehiculo(); // ¡Error de compilación!
        
        // Creamos instancias de las clases derivadas
        Coche miCoche = new Coche("Toyota", "Corolla", 2023, 4);
        Motocicleta miMoto = new Motocicleta("Honda", "CBR600", 2023, false);
        
        // Podemos usar referencias de la clase base
        Vehiculo[] vehiculos = { miCoche, miMoto };
        
        foreach (Vehiculo vehiculo in vehiculos)
        {
            vehiculo.MostrarInformacion();
            vehiculo.Arrancar();        // Llamada polimórfica
            vehiculo.Acelerar(80);      // Llamada polimórfica
            vehiculo.Detener();         // Llamada polimórfica
            Console.WriteLine();
        }
    }
}

Interfaces

Una interfaz define un contrato que especifica qué métodos, propiedades y eventos debe implementar una clase, pero no proporciona implementación alguna. Las interfaces permiten que diferentes clases compartan funcionalidades comunes sin estar relacionadas por herencia.

Características de las interfaces

Característica Descripción
Palabra clave Se declaran con la palabra clave interface
Nomenclatura Por convención, empiezan con la letra "I"
Implementación No contienen implementación (hasta C# 8.0)
Herencia múltiple Una clase puede implementar múltiples interfaces
Acceso Todos los miembros son públicos por defecto
Instanciación No pueden ser instanciadas directamente

Declaración de interfaces

// Interface para objetos que pueden volar
public interface IVolador
{
    double AltitudMaxima { get; }
    void Despegar();
    void Volar(double altitud);
    void Aterrizar();
}

// Interface para objetos que pueden nadar
public interface INadador
{
    double ProfundidadMaxima { get; }
    void Sumergirse(double profundidad);
    void Nadar(double velocidad);
    void Emerger();
}

// Interface para objetos que pueden ser reparados
public interface IReparable
{
    bool NecesitaReparacion { get; }
    void Reparar();
    void Inspeccionar();
}

Implementación de interfaces

// Clase que implementa múltiples interfaces
public class Avion : Vehiculo, IVolador, IReparable
{
    public double AltitudMaxima { get; private set; }
    public bool NecesitaReparacion { get; private set; }
    
    public Avion(string marca, string modelo, int año, double altitudMaxima) 
        : base(marca, modelo, año)
    {
        AltitudMaxima = altitudMaxima;
        NecesitaReparacion = false;
    }
    
    // Implementación de métodos abstractos de Vehiculo
    public override void Arrancar()
    {
        Console.WriteLine($"El avión {Marca} {Modelo} está encendiendo motores.");
    }
    
    public override void Acelerar(int velocidad)
    {
        Console.WriteLine($"El avión aumenta velocidad a {velocidad} km/h en la pista.");
    }
    
    // Implementación de la interfaz IVolador
    public void Despegar()
    {
        Console.WriteLine("El avión está despegando...");
    }
    
    public void Volar(double altitud)
    {
        if (altitud <= AltitudMaxima)
        {
            Console.WriteLine($"Volando a {altitud} metros de altitud.");
        }
        else
        {
            Console.WriteLine($"¡Altitud máxima excedida! Máximo: {AltitudMaxima}m");
        }
    }
    
    public void Aterrizar()
    {
        Console.WriteLine("El avión está aterrizando...");
    }
    
    // Implementación de la interfaz IReparable
    public void Inspeccionar()
    {
        Console.WriteLine("Inspeccionando el avión...");
        // Simulamos que a veces necesita reparación
        NecesitaReparacion = new Random().Next(1, 10) <= 3;
        Console.WriteLine($"Estado: {(NecesitaReparacion ? "Necesita reparación" : "En buen estado")}");
    }
    
    public void Reparar()
    {
        if (NecesitaReparacion)
        {
            Console.WriteLine("Reparando el avión...");
            NecesitaReparacion = false;
            Console.WriteLine("Reparación completada.");
        }
        else
        {
            Console.WriteLine("El avión no necesita reparación.");
        }
    }
}

// Clase que implementa múltiples interfaces
public class Submarino : IVolador, INadador
{
    public double AltitudMaxima => 100; // Puede emerger hasta 100m sobre el agua
    public double ProfundidadMaxima { get; private set; }
    
    public Submarino(double profundidadMaxima)
    {
        ProfundidadMaxima = profundidadMaxima;
    }
    
    // Implementación de IVolador (capacidad limitada)
    public void Despegar()
    {
        Console.WriteLine("El submarino emerge a la superficie.");
    }
    
    public void Volar(double altitud)
    {
        if (altitud <= 0)
        {
            Console.WriteLine("El submarino está en la superficie.");
        }
        else
        {
            Console.WriteLine("¡Los submarinos no pueden volar en el aire!");
        }
    }
    
    public void Aterrizar()
    {
        Console.WriteLine("El submarino se sumerge desde la superficie.");
    }
    
    // Implementación de INadador
    public void Sumergirse(double profundidad)
    {
        if (profundidad <= ProfundidadMaxima)
        {
            Console.WriteLine($"Sumergiéndose a {profundidad} metros de profundidad.");
        }
        else
        {
            Console.WriteLine($"¡Profundidad máxima excedida! Máximo: {ProfundidadMaxima}m");
        }
    }
    
    public void Nadar(double velocidad)
    {
        Console.WriteLine($"El submarino navega a {velocidad} nudos bajo el agua.");
    }
    
    public void Emerger()
    {
        Console.WriteLine("El submarino está emergiendo a la superficie.");
    }
}

Uso de interfaces con polimorfismo

class Program
{
    static void Main()
    {
        // Crear objetos que implementan interfaces
        Avion boeing = new Avion("Boeing", "747", 2020, 13100);
        Submarino submarino = new Submarino(500);
        
        // Usar polimorfismo con interfaces
        IVolador[] objetosVoladores = { boeing, submarino };
        INadador[] objetosNadadores = { submarino };
        IReparable[] objetosReparables = { boeing };
        
        Console.WriteLine("=== Objetos que pueden volar ===");
        foreach (IVolador volador in objetosVoladores)
        {
            volador.Despegar();
            volador.Volar(1000);
            volador.Aterrizar();
            Console.WriteLine();
        }
        
        Console.WriteLine("=== Objetos que pueden nadar ===");
        foreach (INadador nadador in objetosNadadores)
        {
            nadador.Sumergirse(200);
            nadador.Nadar(15);
            nadador.Emerger();
            Console.WriteLine();
        }
        
        Console.WriteLine("=== Objetos reparables ===");
        foreach (IReparable reparable in objetosReparables)
        {
            reparable.Inspeccionar();
            reparable.Reparar();
            Console.WriteLine();
        }
    }
}

Diferencias entre clases abstractas e interfaces

Aspecto Clases Abstractas Interfaces
Implementación Pueden tener métodos implementados y abstractos Solo definen contratos (hasta C# 8.0)
Herencia Solo se puede heredar de una Se pueden implementar múltiples
Constructores Pueden tener constructores No pueden tener constructores
Campos Pueden tener campos y propiedades Solo propiedades (sin campos)
Modificadores de acceso Pueden usar cualquier modificador Todos los miembros son públicos
Cuándo usar Cuando hay código común a compartir Cuando se define un contrato común

Interfaces con implementación por defecto (C# 8.0+)

A partir de C# 8.0, las interfaces pueden proporcionar implementaciones por defecto para sus miembros:

public interface IRegistrable
{
    string Nombre { get; }
    DateTime FechaRegistro { get; }
    
    // Implementación por defecto
    void MostrarRegistro()
    {
        Console.WriteLine($"Registrado: {Nombre} el {FechaRegistro:dd/MM/yyyy}");
    }
    
    // Método con implementación por defecto que usa otros miembros
    bool EsReciente() => DateTime.Now.Subtract(FechaRegistro).Days <= 30;
}

public class Producto : IRegistrable
{
    public string Nombre { get; set; }
    public DateTime FechaRegistro { get; set; }
    public decimal Precio { get; set; }
    
    public Producto(string nombre, decimal precio)
    {
        Nombre = nombre;
        Precio = precio;
        FechaRegistro = DateTime.Now;
    }
    
    // No es necesario implementar MostrarRegistro() ni EsReciente()
    // Se usarán las implementaciones por defecto de la interfaz
}

Ejemplo práctico: Sistema de notificaciones

// Interface para servicios de notificación
public interface IServicioNotificacion
{
    bool EstaDisponible { get; }
    void EnviarNotificacion(string mensaje, string destinatario);
    void ConfigurarServicio(string configuracion);
}

// Implementación para email
public class ServicioEmail : IServicioNotificacion
{
    public bool EstaDisponible { get; private set; }
    private string servidorSmtp;
    
    public void ConfigurarServicio(string configuracion)
    {
        servidorSmtp = configuracion;
        EstaDisponible = !string.IsNullOrEmpty(servidorSmtp);
        Console.WriteLine($"Servicio de email configurado: {servidorSmtp}");
    }
    
    public void EnviarNotificacion(string mensaje, string destinatario)
    {
        if (EstaDisponible)
        {
            Console.WriteLine($"📧 Email enviado a {destinatario}: {mensaje}");
        }
        else
        {
            Console.WriteLine("❌ Servicio de email no disponible");
        }
    }
}

// Implementación para SMS
public class ServicioSMS : IServicioNotificacion
{
    public bool EstaDisponible { get; private set; }
    private string claveApi;
    
    public void ConfigurarServicio(string configuracion)
    {
        claveApi = configuracion;
        EstaDisponible = !string.IsNullOrEmpty(claveApi);
        Console.WriteLine($"Servicio SMS configurado con clave API");
    }
    
    public void EnviarNotificacion(string mensaje, string destinatario)
    {
        if (EstaDisponible)
        {
            Console.WriteLine($"📱 SMS enviado a {destinatario}: {mensaje}");
        }
        else
        {
            Console.WriteLine("❌ Servicio SMS no disponible");
        }
    }
}

// Gestor que usa las interfaces
public class GestorNotificaciones
{
    private List<IServicioNotificacion> servicios;
    
    public GestorNotificaciones()
    {
        servicios = new List<IServicioNotificacion>();
    }
    
    public void AgregarServicio(IServicioNotificacion servicio)
    {
        servicios.Add(servicio);
    }
    
    public void EnviarNotificacionATodos(string mensaje, string destinatario)
    {
        Console.WriteLine($"Enviando notificación: '{mensaje}'");
        
        foreach (var servicio in servicios)
        {
            if (servicio.EstaDisponible)
            {
                servicio.EnviarNotificacion(mensaje, destinatario);
            }
        }
        
        Console.WriteLine();
    }
}

// Ejemplo de uso
class Program
{
    static void Main()
    {
        var gestor = new GestorNotificaciones();
        
        // Configurar servicios
        var email = new ServicioEmail();
        email.ConfigurarServicio("smtp.miempresa.com");
        
        var sms = new ServicioSMS();
        sms.ConfigurarServicio("API_KEY_123456");
        
        // Agregar servicios al gestor
        gestor.AgregarServicio(email);
        gestor.AgregarServicio(sms);
        
        // Enviar notificaciones
        gestor.EnviarNotificacionATodos("Reunión cancelada", "juan@empresa.com");
        gestor.EnviarNotificacionATodos("Nuevo pedido recibido", "+34600123456");
    }
}

Resumen

Las clases abstractas e interfaces son herramientas esenciales para crear arquitecturas de software flexibles y mantenibles en C#. Las clases abstractas proporcionan una base común con implementación parcial, siendo ideales cuando queremos compartir código entre clases relacionadas. Por otro lado, las interfaces definen contratos que permiten que clases no relacionadas compartan funcionalidades comunes, facilitando el polimorfismo y la implementación múltiple.

La elección entre usar una clase abstracta o una interfaz depende de las necesidades específicas: utiliza clases abstractas cuando necesites compartir código común y tengas una relación jerárquica clara, y emplea interfaces cuando quieras definir capacidades que pueden ser implementadas por clases diversas. Ambos conceptos son fundamentales para aplicar principios sólidos de diseño orientado a objetos y crear código más modular y reutilizable.