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.