16. Proxy (Apoderado) - Patrón Estructural

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.

16.1 Problema y contexto

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.

16.2 Intención y motivación

La intención del patrón es proporcionar un substituto controlado de otro objeto para mediar sus solicitudes. Es especialmente útil cuando:

  • Se necesita retrasar la carga de recursos pesados hasta que sean estrictamente necesarios.
  • El objeto vive en otro proceso o servidor y conviene encapsular la comunicación.
  • Existen reglas de seguridad o de auditoría que deben ejecutarse antes de delegar en el objeto real.
  • Se requiere agregar caché, logging o contadores sin modificar la implementación existente.

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.

16.3 Participantes y estructura

La estructura clásica del patrón involucra los siguientes participantes:

  • Subject: interfaz o clase abstracta que declara las operaciones esperadas por el cliente.
  • RealSubject: implementación concreta que contiene la lógica de negocio y gestiona el estado verdadero.
  • Proxy: implementación alternativa que mantiene una referencia al objeto real y decide si delega, difiere o reemplaza la llamada.
  • Cliente: interactúa con el Subject sin distinguir si recibe un proxy o el objeto real.

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.

16.4 Tipos comunes de Proxy

A lo largo de la literatura se describen varias especializaciones del patrón:

  • Virtual Proxy: difiere la creación de objetos costosos hasta que se solicitan, cargando datos incrementalmente.
  • Remote Proxy: representa un objeto ubicado en otra máquina y maneja la serialización, las reconexiones y los tiempos de espera.
  • Protection Proxy: valida permisos, roles o cuotas antes de permitir la operación.
  • Caching Proxy: almacena respuestas para reducir el tiempo de acceso y la carga sobre el objeto real.
  • Smart Reference: ejecuta lógica adicional tras cada acceso (por ejemplo, conteo de referencias o registro de auditoría).

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.

16.5 Ejemplo: control y caché para un catálogo de video

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.

16.6 Implementación en C#

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));
    }
}

16.7 Explicación del flujo

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.

16.8 Integración con el ecosistema .NET

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.

16.9 Riesgos y malas prácticas

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.

16.10 Buenas prácticas para aplicar Proxy

  • Documente el propósito del proxy para que el equipo entienda qué preocupación transversal está encapsulando.
  • Evite mezclar responsabilidades dispares; considere delegar en varios proxies encadenados si la lógica se vuelve compleja.
  • Implemente estrategias de invalidez de caché cuando represente datos mutables.
  • Instrumente el proxy con mediciones de latencia y fallos para detectar anomalías en el objeto real o en la red.

16.11 Relación con otros patrones

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.