Interfaces y polimorfismo: variación controlada y extensibilidad

Capítulo 6

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

Polimorfismo: programar contra contratos y elegir comportamiento en tiempo de ejecución

En POO práctica, polimorfismo significa dos ideas complementarias: (1) programar contra un contrato (una interfaz o un tipo abstracto) en lugar de contra una clase concreta, y (2) permitir la selección dinámica del comportamiento en tiempo de ejecución según el objeto real que se inyecta o se pasa como dependencia.

Esto habilita variación controlada: el sistema cambia de comportamiento sin reescribir el código consumidor. El consumidor solo conoce el contrato; las implementaciones pueden crecer sin modificarlo (o con cambios mínimos y localizados).

Ejemplo mental rápido

Si un servicio necesita “pagar”, no debería depender de TarjetaCredito o PayPal directamente. Debería depender de MetodoDePago. En tiempo de ejecución, se elige la implementación concreta (tarjeta, transferencia, etc.).

Interfaces como contratos estables para desacoplar módulos

Una interfaz define un conjunto de operaciones que un módulo ofrece o requiere. Su valor principal es el desacoplamiento: el código que usa la interfaz no necesita conocer detalles internos ni dependencias de la implementación.

  • Estabilidad: el contrato debe cambiar poco; las implementaciones pueden cambiar más.
  • Intercambiabilidad: múltiples implementaciones pueden coexistir.
  • Testabilidad: puedes sustituir implementaciones reales por dobles (fakes/stubs) sin frameworks.

Interfaces en Java vs C#: similitudes y diferencias útiles

AspectoJavaC#
Declaracióninterfaceinterface
Implementación por claseclass X implements Iclass X : I
Múltiples interfaces
Miembros por defectoDesde Java 8: default methodsDesde C# 8: default interface members (con matices de runtime/versión)
Campos/estadoNo estado de instancia; constantes public static finalNo estado de instancia; constantes permitidas
VisibilidadMétodos de interfaz son public (implícito)Métodos de interfaz son public (implícito)

Miembros por defecto: cuándo usarlos (y cuándo no)

Los métodos por defecto en interfaces permiten añadir comportamiento sin romper implementaciones existentes. Úsalos con moderación:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • : para pequeñas utilidades coherentes con el contrato, o para evolución compatible.
  • No: para meter lógica compleja o dependencias; eso suele indicar que necesitas una clase (o un servicio) separado.

¿Interfaz o clase abstracta? Criterios prácticos

Ambas expresan contratos, pero sirven a propósitos distintos.

Prefiere interfaz cuando…

  • Quieres desacoplar consumidores de implementaciones.
  • Necesitas múltiples implementaciones sin relación jerárquica.
  • Quieres componer capacidades (una clase puede implementar varias interfaces).
  • El contrato es pequeño y estable.

Prefiere clase abstracta cuando…

  • Hay código compartido significativo y estable entre implementaciones.
  • Necesitas estado protegido común o una plantilla de algoritmo (template method).
  • Quieres imponer una estructura interna (no solo operaciones públicas).

Regla práctica: si lo que buscas es “puedo intercambiar implementaciones”, interfaz. Si lo que buscas es “comparto base y estructura”, clase abstracta.

Strategy: variación controlada con inyección de dependencias simple

El patrón Strategy encapsula una familia de algoritmos (o políticas) detrás de una interfaz. El consumidor recibe una estrategia y la usa sin conocer su implementación. Esto es polimorfismo aplicado.

Guía paso a paso (sin frameworks)

  • Paso 1: Define una interfaz con una sola responsabilidad (la variación).
  • Paso 2: Implementa estrategias concretas.
  • Paso 3: Inyecta la estrategia en el consumidor (constructor o parámetro).
  • Paso 4: Prueba el consumidor con un fake/stub de la interfaz.

Ejemplo 1: FormatoDeReporte (Java)

Paso 1: contrato

public interface FormatoDeReporte {    String formatear(Reporte reporte);}

Paso 2: estrategias concretas

public final class FormatoTextoPlano implements FormatoDeReporte {    @Override    public String formatear(Reporte reporte) {        return "REPORTE: " + reporte.titulo() + " | Total: " + reporte.total();    }}public final class FormatoJson implements FormatoDeReporte {    @Override    public String formatear(Reporte reporte) {        return "{\"titulo\":\"" + reporte.titulo() + "\",\"total\":" + reporte.total() + "}";    }}

Modelo simple

public record Reporte(String titulo, double total) {}

Paso 3: consumidor con inyección por constructor

public final class GeneradorDeReportes {    private final FormatoDeReporte formato;    public GeneradorDeReportes(FormatoDeReporte formato) {        this.formato = formato;    }    public String generar(Reporte reporte) {        return formato.formatear(reporte);    }}

Paso 4: prueba sin frameworks (fake)

final class FormatoFake implements FormatoDeReporte {    String ultimoTitulo;    @Override    public String formatear(Reporte reporte) {        ultimoTitulo = reporte.titulo();        return "OK";    }}public class GeneradorDeReportesTest {    public static void main(String[] args) {        FormatoFake fake = new FormatoFake();        GeneradorDeReportes gen = new GeneradorDeReportes(fake);        String salida = gen.generar(new Reporte("Ventas", 120.5));        if (!"OK".equals(salida)) throw new AssertionError("Salida inesperada");        if (!"Ventas".equals(fake.ultimoTitulo)) throw new AssertionError("No se llamó al contrato");    }}

Observa que el test valida el comportamiento del consumidor sin depender de JSON real ni de texto plano real. Eso es desacoplamiento + polimorfismo.

Ejemplo 2: MetodoDePago (C#)

Paso 1: contrato

public interface IMetodoDePago{    string Pagar(decimal monto);}

Paso 2: estrategias concretas

public sealed class PagoConTarjeta : IMetodoDePago{    public string Pagar(decimal monto) => $"Tarjeta aprobada: {monto}";}public sealed class PagoConTransferencia : IMetodoDePago{    public string Pagar(decimal monto) => $"Transferencia iniciada: {monto}";}

Paso 3: consumidor con inyección por parámetro (útil cuando cambia por operación)

public sealed class CheckoutService{    public string Cobrar(IMetodoDePago metodo, decimal monto)    {        return metodo.Pagar(monto);    }}

Paso 4: prueba sin frameworks (stub)

public sealed class MetodoDePagoStub : IMetodoDePago{    public decimal UltimoMonto { get; private set; }    public string Pagar(decimal monto)    {        UltimoMonto = monto;        return "OK";    }}public static class CheckoutServiceTest{    public static void Main()    {        var stub = new MetodoDePagoStub();        var service = new CheckoutService();        var r = service.Cobrar(stub, 99.90m);        if (r != "OK") throw new Exception("Respuesta inesperada");        if (stub.UltimoMonto != 99.90m) throw new Exception("No se pasó el monto correctamente");    }}

La inyección por parámetro es una forma simple de DI: el método recibe la dependencia. La inyección por constructor es mejor cuando la dependencia es fija para la vida del objeto.

Diseño de interfaces pequeñas y coherentes

Criterios concretos

  • Una razón principal de cambio: la interfaz debe agrupar operaciones que cambian por el mismo motivo.
  • Evita “comodines”: si una implementación deja métodos “vacíos” o lanza UnsupportedOperationException/NotSupportedException, la interfaz está mal segmentada.
  • Nombres orientados a capacidad: FormateadorDeReporte, MetodoDePago, CalculadoraDeImpuestos. Evita nombres genéricos tipo Manager o Helper.
  • Parámetros mínimos: pasa objetos de dominio (p. ej., Reporte, Pago) en lugar de listas largas de primitivos.
  • Retornos claros: devuelve resultados que el consumidor pueda usar sin conocer detalles internos (p. ej., ResultadoPago en vez de códigos mágicos).

Cómo evitar “interfaces paraguas” (god interfaces)

Una “interfaz paraguas” mezcla responsabilidades: por ejemplo, IProcesador con métodos para validar, persistir, notificar, auditar y exportar. Esto crea acoplamiento y obliga a implementaciones a depender de cosas que no usan.

Señales de alerta:

  • La interfaz tiene muchos métodos con verbos distintos y dominios distintos.
  • Las implementaciones típicamente usan solo un subconjunto.
  • Los cambios en un área rompen implementaciones de otra.

Refactor típico: divide por capacidades y compón en el consumidor.

// En lugar de una interfaz enorme:interface ProcesadorTodo {    void validar();    void guardar();    void notificar();    void exportar();}// Divide por capacidades:interface Validador { void validar(); }interface Repositorio { void guardar(); }interface Notificador { void notificar(); }interface Exportador { void exportar();}// El consumidor recibe lo que necesita:final class CasoDeUso {    private final Validador validador;    private final Repositorio repo;    CasoDeUso(Validador v, Repositorio r) { this.validador = v; this.repo = r; }    void ejecutar() { validador.validar(); repo.guardar(); }}

Polimorfismo en la práctica: checklist de extensibilidad

  • ¿El consumidor depende de una interfaz y no de una clase concreta?
  • ¿La interfaz es pequeña y sus métodos se usan juntos?
  • ¿Puedes añadir una nueva estrategia (nuevo formato, nuevo método de pago) sin tocar el consumidor?
  • ¿Puedes probar el consumidor con un fake/stub sin infraestructura externa?
  • ¿Los métodos por defecto (si existen) son mínimos y no esconden dependencias?

Ahora responde el ejercicio sobre el contenido:

¿Qué enfoque describe mejor el polimorfismo aplicado con interfaces para lograr variación controlada y extensibilidad sin modificar el código consumidor?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

El polimorfismo práctico consiste en depender de un contrato y seleccionar dinámicamente la implementación (p. ej., por inyección). Así se intercambian estrategias sin cambiar el consumidor y se facilita la testabilidad con fakes/stubs.

Siguiente capítulo

Métodos virtuales, override y dispatch: cómo funciona el polimorfismo en la práctica

Arrow Right Icon
Portada de libro electrónico gratuitaPOO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)
60%

POO práctica y clara: clases, encapsulación, herencia y polimorfismo (con ejemplos en Java y C#)

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.