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 record es un tipo diseñado para almacenar datos inmutables. Genera automáticamente Equals(), GetHashCode(), ToString(), y una sintaxis de copia con with.

// 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
Campos No
Implementación por defecto Sí (C# 8+)
Estado (propiedades con backing field) No
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 } => ... }