Módulo 13: Polimorfismo e interfaces
Objetivos del módulo
- Comprender el polimorfismo en profundidad
- Crear y utilizar interfaces
- Conocer el tipo
record - Dominar el pattern matching avanzado
1. Polimorfismo
📘 Concepto: Polimorfismo significa “muchas formas”. En POO, permite que un mismo método se comporte de forma diferente según el tipo real del objeto. Puedes tratar objetos de diferentes clases de la misma manera a través de una clase base o interfaz común.
abstract class Animal
{
public string Nombre { get; set; } = "";
public abstract string HacerSonido();
}
class Perro : Animal
{
public override string HacerSonido() => "¡Guau!";
}
class Gato : Animal
{
public override string HacerSonido() => "¡Miau!";
}
class Pato : Animal
{
public override string HacerSonido() => "¡Cuac!";
}
// Polimorfismo en acción: todos son Animal, pero cada uno actúa diferente
List<Animal> animales = new()
{
new Perro { Nombre = "Rex" },
new Gato { Nombre = "Luna" },
new Pato { Nombre = "Donald" }
};
foreach (Animal animal in animales)
{
// HacerSonido() llama al método correcto según el tipo REAL
Console.WriteLine($"{animal.Nombre}: {animal.HacerSonido()}");
}
// Rex: ¡Guau!
// Luna: ¡Miau!
// Donald: ¡Cuac!
2. Interfaces
📘 Concepto: Una interfaz es un contrato que define qué debe hacer una clase, pero no cómo. Define métodos y propiedades que la clase debe implementar. Una clase puede implementar múltiples interfaces (a diferencia de la herencia, que es simple).
Crear una interfaz
// Por convención, los nombres de interfaz empiezan con 'I'
interface IVolador
{
void Despegar();
void Aterrizar();
double AlturaActual { get; }
}
interface INadador
{
void Sumergirse();
void Salir();
}
Implementar interfaces
class Pato : Animal, IVolador, INadador // Hereda de Animal, implementa 2 interfaces
{
public double AlturaActual { get; private set; }
public override string HacerSonido() => "¡Cuac!";
// Implementación de IVolador
public void Despegar()
{
AlturaActual = 10;
Console.WriteLine($"{Nombre} despega");
}
public void Aterrizar()
{
AlturaActual = 0;
Console.WriteLine($"{Nombre} aterriza");
}
// Implementación de INadador
public void Sumergirse() => Console.WriteLine($"{Nombre} se sumerge");
public void Salir() => Console.WriteLine($"{Nombre} sale del agua");
}
class Avion : IVolador // Solo implementa IVolador (no hereda de Animal)
{
public double AlturaActual { get; private set; }
public void Despegar()
{
AlturaActual = 10000;
Console.WriteLine("El avión despega");
}
public void Aterrizar()
{
AlturaActual = 0;
Console.WriteLine("El avión aterriza");
}
}
Usar interfaces como tipo
// Puedes crear una lista de "cosas que vuelan" sin importar qué sean
List<IVolador> voladores = new()
{
new Pato { Nombre = "Donald" },
new Avion()
};
foreach (IVolador v in voladores)
{
v.Despegar();
Console.WriteLine($"Altura: {v.AlturaActual}m");
v.Aterrizar();
Console.WriteLine("---");
}
Interfaces con implementación por defecto (C# 8+)
interface IRegistrable
{
string Nombre { get; }
// Método con implementación por defecto
void Registrar()
{
Console.WriteLine($"Registrando: {Nombre} a las {DateTime.Now:HH:mm:ss}");
}
}
class Usuario : IRegistrable
{
public string Nombre { get; set; } = "";
// No necesita implementar Registrar() si le vale la implementación por defecto
}
3. Interfaces comunes de .NET
.NET incluye muchas interfaces útiles que puedes implementar:
IComparable<T>: ordenar objetos
class Alumno : IComparable<Alumno>
{
public string Nombre { get; set; } = "";
public double Nota { get; set; }
// Implementar CompareTo para ordenar por nota (descendente)
public int CompareTo(Alumno? otro)
{
if (otro is null) return 1;
return otro.Nota.CompareTo(Nota); // Descendente
}
public override string ToString() => $"{Nombre}: {Nota:F1}";
}
List<Alumno> alumnos = new()
{
new() { Nombre = "Ana", Nota = 8.5 },
new() { Nombre = "Luis", Nota = 9.2 },
new() { Nombre = "María", Nota = 7.0 }
};
alumnos.Sort(); // Usa CompareTo automáticamente
foreach (var a in alumnos)
Console.WriteLine(a);
// Luis: 9.2
// Ana: 8.5
// María: 7.0
IEquatable<T>: comparar objetos
class Punto : IEquatable<Punto>
{
public int X { get; set; }
public int Y { get; set; }
public bool Equals(Punto? otro)
{
if (otro is null) return false;
return X == otro.X && Y == otro.Y;
}
public override bool Equals(object? obj) => Equals(obj as Punto);
public override int GetHashCode() => HashCode.Combine(X, Y);
public override string ToString() => $"({X}, {Y})";
}
var p1 = new Punto { X = 3, Y = 4 };
var p2 = new Punto { X = 3, Y = 4 };
Console.WriteLine(p1.Equals(p2)); // True (sin IEquatable sería False)
IDisposable: liberar recursos
class ConexionBD : IDisposable
{
public string Servidor { get; set; } = "";
private bool _disposed = false;
public void Abrir() => Console.WriteLine($"Conexión abierta a {Servidor}");
public void Dispose()
{
if (!_disposed)
{
Console.WriteLine($"Conexión cerrada a {Servidor}");
_disposed = true;
}
}
}
// 'using' llama a Dispose automáticamente
using var conn = new ConexionBD { Servidor = "localhost" };
conn.Abrir();
// Al salir del ámbito → conn.Dispose() → "Conexión cerrada a localhost"
4. Records: clases de datos inmutables
📘 Concepto: Un
recordes un tipo diseñado para almacenar datos inmutables. Genera automáticamenteEquals(),GetHashCode(),ToString(), y una sintaxis de copia conwith.
// Declaración concisa de un record
record Coordenada(double Latitud, double Longitud);
// Uso
var madrid = new Coordenada(40.4168, -3.7038);
var barcelona = new Coordenada(41.3879, 2.1699);
Console.WriteLine(madrid); // Coordenada { Latitud = 40.4168, Longitud = -3.7038 }
// Igualdad por valor (no por referencia)
var madrid2 = new Coordenada(40.4168, -3.7038);
Console.WriteLine(madrid == madrid2); // True (con class sería False)
// Copia con modificación usando 'with'
var madridCerca = madrid with { Latitud = 40.42 };
Console.WriteLine(madridCerca); // Coordenada { Latitud = 40.42, Longitud = -3.7038 }
Record con más miembros
record Persona(string Nombre, int Edad)
{
// Propiedad calculada
public bool EsMayorDeEdad => Edad >= 18;
// Métodos adicionales
public string Saludar() => $"Hola, soy {Nombre}";
}
var ana = new Persona("Ana", 25);
Console.WriteLine(ana.Saludar()); // Hola, soy Ana
Console.WriteLine(ana.EsMayorDeEdad); // True
// Los records son inmutables: no puedes cambiar propiedades
// ana.Nombre = "María"; // ERROR: init-only
record struct (tipo valor)
record struct Punto(int X, int Y);
var p = new Punto(3, 4);
Console.WriteLine(p); // Punto { X = 3, Y = 4 }
5. Pattern matching avanzado
C# ofrece pattern matching muy potente para trabajar con jerarquías de tipos:
Switch con tipos
abstract class Forma { }
class Circulo : Forma { public double Radio { get; init; } }
class Rectangulo : Forma { public double Ancho { get; init; } public double Alto { get; init; } }
class Triangulo : Forma { public double Base { get; init; } public double Altura { get; init; } }
static double CalcularArea(Forma forma) => forma switch
{
Circulo c => Math.PI * c.Radio * c.Radio,
Rectangulo r when r.Ancho == r.Alto => r.Ancho * r.Alto, // Cuadrado
Rectangulo r => r.Ancho * r.Alto,
Triangulo t => t.Base * t.Altura / 2,
_ => throw new ArgumentException("Forma desconocida")
};
var formas = new Forma[]
{
new Circulo { Radio = 5 },
new Rectangulo { Ancho = 4, Alto = 6 },
new Triangulo { Base = 3, Altura = 8 }
};
foreach (var f in formas)
{
Console.WriteLine($"{f.GetType().Name}: área = {CalcularArea(f):F2}");
}
Patrones relacionales y lógicos
static string ClasificarEdad(int edad) => edad switch
{
< 0 => throw new ArgumentException("Edad no válida"),
0 => "Recién nacido",
>= 1 and <= 3 => "Bebé",
>= 4 and <= 11 => "Niño",
>= 12 and <= 17 => "Adolescente",
>= 18 and <= 64 => "Adulto",
>= 65 => "Jubilado"
};
Console.WriteLine(ClasificarEdad(25)); // Adulto
Console.WriteLine(ClasificarEdad(8)); // Niño
Patrones de propiedad
record Pedido(string Cliente, double Total, string Pais);
static double CalcularEnvio(Pedido pedido) => pedido switch
{
{ Total: > 100, Pais: "España" } => 0, // Envío gratis en España +100€
{ Pais: "España" } => 4.99, // España < 100€
{ Pais: "Portugal" or "Francia" } => 9.99, // Países vecinos
{ Total: > 200 } => 14.99, // Gran pedido internacional
_ => 19.99 // Resto del mundo
};
var pedido = new Pedido("Ana", 150, "España");
Console.WriteLine($"Envío: {CalcularEnvio(pedido):C2}"); // 0,00 €
6. Interfaces vs clases abstractas
| Característica | Interfaz | Clase abstracta |
|---|---|---|
| Herencia múltiple | Sí (varias interfaces) | No (solo una clase) |
| Constructores | No | Sí |
| Campos | No | Sí |
| Implementación por defecto | Sí (C# 8+) | Sí |
| Estado (propiedades con backing field) | No | Sí |
| Uso | “Puede hacer X” (capacidad) | “Es un X” (identidad) |
Regla general:
- Usa interfaz cuando quieres definir una capacidad:
IVolador,IGuardable,IComparable - Usa clase abstracta cuando quieres definir una familia:
Animal,Figura,Empleado
7. Ejercicios
Ejercicio 1: Sistema de notificaciones
Crea una interfaz INotificable con método EnviarNotificacion(string mensaje). Implementa: NotificacionEmail, NotificacionSMS, NotificacionPush. Crea un servicio que envíe notificaciones a una lista de INotificable.
Ejercicio 2: Procesador de pagos
Interfaz IProcesadorPago con Pagar(double monto) que devuelve bool. Implementa: PagoTarjeta, PagoPayPal, PagoBizum. Usa pattern matching para aplicar comisiones según el tipo de pago.
Ejercicio 3: Sistema de formas con records
Usa records para definir formas: record Circulo(double Radio), record Rectangulo(double Ancho, double Alto), etc. Implementa un método con switch expression que calcule el área de cualquier forma.
Ejercicio 4: Catálogo multimedia
Crea: IReproducible (Play, Pause, Stop), IDescargable (Descargar, Progreso). Clases: Cancion, Video, Podcast. Un Cancion es IReproducible, un Video es IReproducible y IDescargable, etc. Crea un reproductor que maneje una lista de IReproducible.
Resumen
| Concepto | Ejemplo |
|---|---|
| Polimorfismo | Un Animal puede ser Perro, Gato, etc. |
| Interfaz | interface IVolador { void Volar(); } |
| Implementar interfaz | class Pato : IVolador { } |
| Múltiples interfaces | class X : IVolador, INadador { } |
| Record | record Persona(string Nombre, int Edad); |
Copia con with | var p2 = p1 with { Edad = 30 }; |
| Pattern matching tipo | forma switch { Circulo c => ... } |
| Pattern matching relacional | edad switch { >= 18 => "Adulto" } |
| Pattern matching propiedad | pedido switch { { Total: > 100 } => ... } |