Arquitectura de software

¿Por qué abstraer todo puede hacer un sistema peor?

Interfaces con una sola implementación, capas que solo reenvían y genéricos que nadie entiende. Cuándo una abstracción ayuda y cuándo solo estorba.

Por Leyton Manuel Espitia Diaz 5 min de lectura

Fragmento de código C# con varias capas de interfaces que solo reenvían una llamada

Para entender por qué un descuento se calculó mal, hay que pasar por OrderController, IOrderService, OrderService, IDiscountStrategyFactory, DiscountStrategyFactory, IDiscountStrategy, PercentageDiscountStrategy y IDiscountRepository. Ocho archivos. La lógica real son cuatro líneas en uno de ellos.

Cada una de esas abstracciones se agregó con una buena intención: desacoplar, facilitar las pruebas, prepararse para el futuro. Juntas hacen que un cambio simple requiera entender un sistema completo.

Una abstracción es una apuesta

Toda abstracción apuesta a que algo va a cambiar o va a variar: otro proveedor, otra estrategia, otro almacenamiento. Si la apuesta acierta, el cambio es fácil. Si no, pagaste el costo sin recibir nada:

  • Más archivos que leer.
  • Navegación indirecta: "Ir a la definición" lleva a una interfaz, no al código.
  • Más registros en el contenedor y más configuración.
  • Pruebas que simulan comportamientos en lugar de verificarlos.

Señales de que una abstracción sobra

1. Interfaces con una sola implementación que nunca tendrán otra

public interface IOrderService { Task<int> CreateAsync(CreateOrderRequest request, CancellationToken ct); }
public class OrderService : IOrderService { ... }

Si la única razón es "para poder simularlo en pruebas", pregúntate qué estás probando. El que llama a OrderService suele ser un controlador que no tiene lógica propia. Simular el servicio para probar el controlador verifica poco.

2. Capas que solo reenvían

public class OrderService(IOrderRepository repository) : IOrderService
{
    public Task<Order?> GetAsync(int id, CancellationToken ct) => repository.GetAsync(id, ct);
}

public class OrderRepository(AppDbContext db) : IOrderRepository
{
    public Task<Order?> GetAsync(int id, CancellationToken ct) => db.Orders.FindAsync([id], ct).AsTask();
}

Dos capas y dos interfaces para una línea de EF Core. Lo vimos en el caso del Repository Pattern sobre EF Core.

3. Genéricos y configuraciones para casos que no existen

Un IProcessor<TInput, TOutput, TContext> con un sistema de plugins configurable para un proceso que, en tres años, ha tenido siempre la misma entrada y la misma salida.

4. Abstracciones que filtran

Un IStorage que esconde el sistema de archivos, pero cuyos métodos devuelven rutas de Windows. O un repositorio que expone IQueryable<T>. La abstracción promete independencia y no la cumple: el código que la usa sigue atado a la implementación, solo que ahora con un intermediario.

Abstracciones que sí suelen valer la pena

No se trata de evitar las abstracciones, sino de ponerlas donde aciertan:

// ✅ Un servicio externo: cambia de proveedor y necesitas simularlo en pruebas
public interface IPaymentGateway { Task<PaymentResult> ChargeAsync(PaymentRequest request, CancellationToken ct); }

// ✅ El reloj: sin esto, probar reglas que dependen de la fecha es casi imposible
public class SubscriptionRenewal(TimeProvider clock) { ... } // TimeProvider viene en .NET 8

// ✅ Variación real que ya existe: tres transportadoras con reglas distintas
public interface IShippingCalculator { decimal Calculate(Order order); }

El patrón común: límites del sistema (lo que está fuera de tu control) y variaciones que ya existen, no las que podrían existir.

La regla de tres

Una heurística útil: no abstraigas hasta la tercera vez.

  1. La primera vez, escribe el código directamente.
  2. La segunda, nota la duplicación, pero tolérala.
  3. La tercera, ya sabes qué varía y qué no. Ahora la abstracción tiene una forma basada en casos reales.

Abstraer en la primera vez significa adivinar la forma de la variación. Y una abstracción equivocada es peor que la duplicación: obliga a que los casos nuevos encajen en un molde que no les corresponde, con parámetros opcionales y banderas que crecen con cada excepción.

Extraer después es más barato que eliminar

Agregar una interfaz a una clase concreta cuando aparece la necesidad es un cambio mecánico: el IDE lo hace en segundos. Quitar una abstracción que se usa en 40 lugares, con su fábrica y sus registros, es un proyecto. Esa asimetría es el argumento más fuerte para empezar concreto.

El trade-off

Menos abstracciones significa código más directo, pero también más acoplado. Si aparece la variación que no anticipaste, habrá que refactorizar. Ese refactor cuesta, pero lo pagas una vez, cuando hace falta, con la forma real del problema a la vista. Las abstracciones prematuras se pagan en cada lectura del código, sea que la variación llegue o no.

Cuándo sí abstraer pronto

  • Límites con sistemas externos: pagos, correo, almacenamiento, APIs de terceros.
  • Bibliotecas que usarán otros equipos y cuyo contrato es difícil de cambiar después.
  • Requisitos conocidos de variación, por ejemplo, un producto que se venderá a clientes con proveedores distintos desde el primer día.

Conclusión

Una buena abstracción hace que el código sea más fácil de cambiar en la dirección en que realmente cambia. Una abstracción prematura lo hace más difícil de leer en todas las direcciones. Antes de crear una interfaz, intenta nombrar el cambio concreto que va a facilitar. Si no puedes, probablemente no la necesitas. Lo mismo aplica a los principios de diseño: SOLID en proyectos reales es una herramienta, no un requisito.

Leyton Manuel Espitia Diaz

Escrito por

Leyton Manuel Espitia Diaz

CEO y fundador de Nakanaba

Desarrollador de software y fundador de Nakanaba. Lidera la arquitectura de Vitrin: backend en .NET, aplicaciones en Angular y .NET MAUI, SQL Server y servicios en Cloudflare.

¿Tu sistema necesita una revisión de arquitectura?

Te ayudamos a decidir qué estructura necesita tu software hoy y cómo prepararlo para crecer, sin sobreingeniería.

Ver desarrollo a medida