Saltar al contenido principal

Patrones de Diseño 🟡

SOLID — Los 5 principios

S — Single Responsibility Principle

// ❌ Violación: una clase hace demasiadas cosas
public class Usuario
{
public string Nombre { get; set; }
public void GuardarEnBaseDeDatos() { /* SQL aquí */ }
public void EnviarEmail() { /* SMTP aquí */ }
public string GenerarReporte() { /* lógica de reporte */ }
}

// ✅ Correcto: separación de responsabilidades
public class Usuario { public string Nombre { get; set; } }
public class UsuarioRepository { public void Guardar(Usuario u) { } }
public class EmailService { public void Enviar(string destinatario) { } }
public class ReporteService { public string Generar(Usuario u) => ""; }

O — Open/Closed Principle

// ❌ Violación: modificar la clase para agregar descuentos
public class Descuento
{
public decimal Calcular(string tipo, decimal precio) => tipo switch
{
"navidad" => precio * 0.9m,
"black-friday" => precio * 0.7m,
_ => precio
};
}

// ✅ Abierto para extensión, cerrado para modificación
public abstract class Descuento
{
public abstract decimal Calcular(decimal precio);
}
public class DescuentoNavidad : Descuento
{
public override decimal Calcular(decimal precio) => precio * 0.9m;
}
public class DescuentoBlackFriday : Descuento
{
public override decimal Calcular(decimal precio) => precio * 0.7m;
}

L — Liskov Substitution Principle

// ❌ Violación: Cuadrado hereda de Rectángulo pero viola el contrato
public class Rectangulo
{
public virtual int Ancho { get; set; }
public virtual int Alto { get; set; }
public int Area() => Ancho * Alto;
}

public class Cuadrado : Rectangulo
{
public override int Ancho { set => base.Ancho = base.Alto = value; }
public override int Alto { set => base.Ancho = base.Alto = value; }
}
// Si hago: Rectangulo r = new Cuadrado(); r.Ancho = 5; r.Alto = 3;
// Esperaría Area() = 15, pero obtengo 9 — viola LSP

// ✅ Usar composición o interfaces separadas
public interface IForma { int Area(); }
public class Rectangulo : IForma { /* ... */ }
public class Cuadrado : IForma { /* ... */ }

I — Interface Segregation Principle

// ❌ Interfaz "gorda"
public interface IAnimal
{
void Caminar();
void Nadar();
void Volar();
}
// Los peces tienen que implementar Volar() aunque no vuelan

// ✅ Interfaces pequeñas y específicas
public interface ICaminante { void Caminar(); }
public interface INadador { void Nadar(); }
public interface IVolador { void Volar(); }

public class Pato : ICaminante, INadador, IVolador { /* puede todo */ }
public class Pez : INadador { /* solo nada */ }

D — Dependency Inversion Principle

// ❌ Dependencia de implementación concreta
public class NotificacionService
{
private readonly SmtpEmailSender _emailSender = new(); // depende del concreto

public void Notificar(string msg) => _emailSender.Send(msg);
}

// ✅ Depender de abstracciones
public class NotificacionService
{
private readonly INotificador _notificador; // depende de la abstracción

public NotificacionService(INotificador notificador)
{
_notificador = notificador;
}

public void Notificar(string msg) => _notificador.Enviar(msg);
}

Patrones de Creación

Repository Pattern

public interface IProductoRepository
{
Task<Producto?> ObtenerPorIdAsync(int id);
Task<IEnumerable<Producto>> ObtenerTodosAsync();
Task<Producto> CrearAsync(Producto producto);
Task ActualizarAsync(Producto producto);
Task EliminarAsync(int id);
}

public class ProductoRepository : IProductoRepository
{
private readonly AppDbContext _context;
public ProductoRepository(AppDbContext context) => _context = context;

public async Task<Producto?> ObtenerPorIdAsync(int id) =>
await _context.Productos.FindAsync(id);

// ... implementaciones
}

Unit of Work

public interface IUnitOfWork : IDisposable
{
IProductoRepository Productos { get; }
ICategoriaRepository Categorias { get; }
Task<int> GuardarAsync();
}

public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;

public IProductoRepository Productos { get; }
public ICategoriaRepository Categorias { get; }

public UnitOfWork(AppDbContext context)
{
_context = context;
Productos = new ProductoRepository(context);
Categorias = new CategoriaRepository(context);
}

public Task<int> GuardarAsync() => _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}

Patrones de Comportamiento

Strategy Pattern

public interface IEstrategiaOrdenamiento
{
IEnumerable<Producto> Ordenar(IEnumerable<Producto> productos);
}

public class OrdenarPorPrecio : IEstrategiaOrdenamiento
{
public IEnumerable<Producto> Ordenar(IEnumerable<Producto> p) =>
p.OrderBy(x => x.Precio);
}

public class OrdenarPorNombre : IEstrategiaOrdenamiento
{
public IEnumerable<Producto> Ordenar(IEnumerable<Producto> p) =>
p.OrderBy(x => x.Nombre);
}

public class CatalogoService
{
private IEstrategiaOrdenamiento _estrategia = new OrdenarPorNombre();

public void CambiarEstrategia(IEstrategiaOrdenamiento e) => _estrategia = e;

public IEnumerable<Producto> ObtenerCatalogo(IEnumerable<Producto> prods) =>
_estrategia.Ordenar(prods);
}

Observer Pattern (Events)

// En .NET, los events son la implementación nativa del patrón Observer
public class PedidoService
{
public event EventHandler<PedidoCreadoEventArgs>? PedidoCreado;

public async Task<Pedido> CrearPedidoAsync(CrearPedidoDto dto)
{
var pedido = /* crear pedido */;

// Notificar a todos los suscriptores
PedidoCreado?.Invoke(this, new PedidoCreadoEventArgs(pedido));

return pedido;
}
}

// Suscriptores
pedidoService.PedidoCreado += async (s, e) => await emailService.NotificarClienteAsync(e.Pedido);
pedidoService.PedidoCreado += async (s, e) => await inventarioService.ReservarStockAsync(e.Pedido);

Preguntas frecuentes de entrevista 🎯

1. ¿Cuál es la diferencia entre Factory Method y Abstract Factory?

Factory Method: define una interfaz para crear un objeto, pero las subclases deciden qué clase instanciar. Abstract Factory: crea familias de objetos relacionados sin especificar sus clases concretas.

2. ¿Cuándo usarías el patrón Decorator?

Para agregar responsabilidades a un objeto dinámicamente sin usar herencia. Por ejemplo: logging, caching, validación sobre un repositorio existente.

3. Explica el patrón Mediator y su uso con MediatR en .NET.

Mediator desacopla el emisor del receptor de una solicitud. En .NET con MediatR: en vez de que el controller llame directamente al service, envía un Command o Query que MediatR enruta al handler correcto. Reduce el acoplamiento.

4. ¿Qué diferencia hay entre Composition y Inheritance?

Herencia ("es un"): fuerte acoplamiento, difícil de cambiar en runtime. Composición ("tiene un"): más flexible, favorece el SRP y OCP. La regla general: "Preferir composición sobre herencia".