El patrón Proxy define un intermediario que controla el acceso a un objeto real, ya sea para diferir su creación, agregar verificaciones o realizar optimizaciones transversales. El proxy expone la misma interfaz que el objeto representado, por lo que el cliente desconoce si está interactuando con la instancia real o con su apoderado.
Al incorporar un punto de delegación explícito, el patrón permite encapsular aspectos como la seguridad, la distribución remota, la gestión de recursos o la instrumentación sin invadir la lógica del objeto principal. Es una forma de cumplir el principio de responsabilidad única al aislar funciones de soporte en un colaborador especializado.
En diseños reales surgen situaciones donde la instanciación o el uso directo de un objeto es costoso o riesgoso. Acceder a un documento pesado, establecer conexiones remotas o permitir operaciones sensibles son ejemplos que requieren controles adicionales. Introducir esas preocupaciones dentro del objeto concreto genera código intrusivo, viola la cohesión y dificulta pruebas.
Proxy soluciona el problema interponiendo un representante que decide cuándo y cómo llegar al objeto real. El cliente conserva una interfaz uniforme mientras que el apoderado gestiona la vida útil, el acceso concurrente, los permisos o cualquier lógica transversal sin que el objeto real se entere.
La intención del patrón es proporcionar un substituto controlado de otro objeto para mediar sus solicitudes. Es especialmente útil cuando:
El apoderado se convierte en un lugar idóneo para centralizar políticas de acceso, instrumentación y optimizaciones, manteniendo el concepto principal limpio y enfocado en su responsabilidad de negocio.
La estructura clásica del patrón involucra los siguientes participantes:
El proxy y el objeto verdadero comparten la misma interfaz, por lo que la sustitución es transparente. El proxy está autorizado a crear el objeto real bajo demanda, a reutilizarlo o incluso a delegar en otro proxy encadenado si se requiere una cadena de responsabilidades.
A lo largo de la literatura se describen varias especializaciones del patrón:
Un mismo apoderado puede mezclar varias motivaciones: cachear resultados, verificar credenciales y llevar métricas en una sola implementación, siempre que mantenga la claridad y no se convierta en un monolito de responsabilidades.
Supongamos una plataforma de streaming que consulta metadatos en un proveedor remoto con latencia variable. Las cuentas gratuitas solo pueden acceder a trailers, mientras que las cuentas premium consultan el catálogo completo. Además, se desea limitar la cantidad de solicitudes consecutivas para prevenir abusos e incorporar caché para evitar costosas llamadas repetidas.
El patrón Proxy es ideal: un apoderado controla a quién se permite el acceso, regula el ritmo de peticiones, reutiliza datos cacheados y delega en el objeto real cuando la información no está disponible localmente.
El siguiente fragmento muestra la estructura completa del ejemplo anterior:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
public interface ICatalogoVideos
{
Video ObtenerVideo(string id, Usuario solicitante);
}
public class CatalogoVideosRemoto : ICatalogoVideos
{
public Video ObtenerVideo(string id, Usuario solicitante)
{
SimularLatencia();
Console.WriteLine($"Consultando proveedor externo para {id}");
return new Video(id, $"Documental {id}", 720);
}
private void SimularLatencia()
{
Thread.Sleep(150);
}
}
public class CatalogoVideosProxy : ICatalogoVideos
{
private readonly ICatalogoVideos _remoto;
private readonly TimeSpan _intervaloEntreSolicitudes;
private readonly ConcurrentDictionary<string, Video> _cache = new ConcurrentDictionary<string, Video>();
private readonly ConcurrentDictionary<string, DateTime> _ultimoAccesoPorUsuario = new ConcurrentDictionary<string, DateTime>();
private readonly ISet<string> _catalogoPromocional;
public CatalogoVideosProxy(ICatalogoVideos remoto, TimeSpan intervaloEntreSolicitudes, ISet<string> catalogoPromocional)
{
_remoto = remoto;
_intervaloEntreSolicitudes = intervaloEntreSolicitudes;
_catalogoPromocional = new HashSet<string>(catalogoPromocional);
}
public Video ObtenerVideo(string id, Usuario solicitante)
{
ValidarAcceso(id, solicitante);
ControlarRitmo(solicitante);
var video = _cache.GetOrAdd(id, clave => _remoto.ObtenerVideo(clave, solicitante));
RegistrarMetrica(id, solicitante, video);
return video;
}
private void ValidarAcceso(string id, Usuario solicitante)
{
if (!solicitante.EsPremium && !_catalogoPromocional.Contains(id))
{
throw new AccesoDenegadoException("Contenido disponible solo para cuentas premium");
}
}
private void ControlarRitmo(Usuario solicitante)
{
var ahora = DateTime.UtcNow;
var ultimoAcceso = _ultimoAccesoPorUsuario.GetOrAdd(solicitante.Id, _ => DateTime.MinValue);
var diferencia = ahora - ultimoAcceso;
if (diferencia < _intervaloEntreSolicitudes)
{
var espera = _intervaloEntreSolicitudes - diferencia;
Thread.Sleep(espera);
}
_ultimoAccesoPorUsuario[solicitante.Id] = DateTime.UtcNow;
}
private void RegistrarMetrica(string id, Usuario solicitante, Video video)
{
Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] {solicitante.Id} solicitó {id} ({video.DuracionEnSegundos} segundos)");
}
}
public record Video(string Id, string Titulo, int DuracionEnSegundos);
public class Usuario
{
public Usuario(string id, bool esPremium)
{
Id = id;
EsPremium = esPremium;
}
public string Id { get; }
public bool EsPremium { get; }
}
public class AccesoDenegadoException : Exception
{
public AccesoDenegadoException(string mensaje) : base(mensaje)
{
}
}
public static class DemoProxy
{
public static void Main()
{
ICatalogoVideos remoto = new CatalogoVideosRemoto();
var catalogoPromocional = new HashSet<string> { "VID-TRAILER" };
var proxy = new CatalogoVideosProxy(remoto, TimeSpan.FromMilliseconds(200), catalogoPromocional);
var premium = new Usuario("premium-123", true);
var invitada = new Usuario("guest-456", false);
Console.WriteLine(proxy.ObtenerVideo("VID-TRAILER", invitada));
try
{
proxy.ObtenerVideo("VID-ALBUM", invitada);
}
catch (AccesoDenegadoException ex)
{
Console.WriteLine($"Acceso denegado para invitada: {ex.Message}");
}
Console.WriteLine(proxy.ObtenerVideo("VID-ALBUM", premium));
Console.WriteLine(proxy.ObtenerVideo("VID-ALBUM", premium));
}
}
El cliente recibe una referencia de tipo CatalogoVideos
e ignora si se trata de un proxy o del objeto concreto. El proxy intercepta cada llamada, valida si el video solicitado está disponible para el tipo de cuenta, comprueba que no se supere la cuota de uso y, solo cuando es necesario, invoca al servicio remoto. Almacena la respuesta en caché y registra una métrica, demostrando cómo encapsular varios aspectos transversales sin recompilar el objeto real.
Este diseño prepara el terreno para reemplazar el servicio remoto por otro proveedor, realizar pruebas unitarias con dobles de prueba o incorporar nuevas reglas de negocio sin tocar la implementación original.
Las bibliotecas y plataformas de .NET emplean proxies con frecuencia. La clase DispatchProxy permite generar proxies dinámicos para cualquier interfaz, mientras que RealProxy ofrece un punto de extensión basado en herencia que sigue vigente en escenarios de infraestructura.
Frameworks como ASP.NET Core, Entity Framework Core o los interceptores de contenedores IoC (Autofac, Castle DynamicProxy) aprovechan proxies para habilitar carga perezosa, aplicar políticas de resiliencia con IHttpClientFactory
, registrar telemetría o agregar seguridad declarativa. Comprender el patrón ayuda a diagnosticar comportamientos del pipeline, elegir el ciclo de vida apropiado de las dependencias y diseñar adaptadores seguros para servicios externos.
Un abuso del patrón puede introducir capas innecesarias que degradan el rendimiento o hacen difícil seguir el flujo de llamadas. También es habitual duplicar lógica de negocio dentro del proxy, lo que genera inconsistencias con el objeto real.
Otro riesgo radica en no manejar adecuadamente los fallos del objeto real: si el apoderado no controla reintentos, tiempo de espera o la liberación de recursos, puede transformarse en un cuello de botella. Finalmente, es crucial definir claramente el contrato del Subject; si evoluciona sin mantener compatibilidad, clientes y proxies pueden quedar desincronizados.
Proxy comparte similitudes con Decorator, pero mientras Decorator agrega responsabilidades al objeto real, Proxy controla el acceso. Puede combinarse con Facade para exponer un punto de entrada simplificado y con Chain of Responsibility para encadenar verificaciones. Cuando se necesita adaptar la interfaz de un servicio heredado antes de delegar en el proxy, se recurre a Adapter. Asimismo, usar Proxy junto a Flyweight permite compartir instancias reales costosas y administrarlas desde un intermediario liviano.