Patrón Proxy en Python

El patrón Proxy es un patrón de diseño estructural que proporciona un sustituto o marcador de posición para otro objeto, con el fin de controlar el acceso a este objeto. En pocas palabras, un proxy es un intermediario que actúa como mediador entre el cliente y el objeto objetivo.

Analogía del proxy en la vida real

Imagina un escenario de la vida real:

  • Agente inmobiliarioNo necesitas comunicarte directamente con el propietario; a través del agente inmobiliario puedes conocer la información de la propiedad y programar visitas.
  • Agente de celebridadesSi una empresa quiere invitar a una celebridad como embajadora de marca, necesita pasar por un agente para negociar los detalles de la cooperación.
  • Servidor proxy de redTus solicitudes de red pasan primero por un servidor proxy, y luego el servidor proxy las reenvía al sitio web de destino.

En estos ejemplos, el intermediario/agente es el proxy, y controlan el acceso al objeto real.


Componentes principales del patrón Proxy

El patrón Proxy generalmente incluye tres roles clave:

1. Sujeto abstracto (Subject)

Define una interfaz común entre el sujeto real y el sujeto proxy, de modo que el sujeto proxy pueda usarse en cualquier lugar donde se use el sujeto real.

2. Sujeto Real (Real Subject)

Define el objeto real que representa el proxy; es el objeto al que finalmente se hace referencia.

3. Proxy

Mantiene una referencia que permite al proxy acceder a la entidad real, proporciona una interfaz idéntica a la del sujeto real y controla el acceso al sujeto real.


Tipos de patrones de proxy

El patrón Proxy tiene múltiples variantes, y cada una tiene diferentes escenarios de aplicación:

1. Proxy Virtual (Virtual Proxy)

Retrasa la creación de objetos de alto costo hasta que realmente se necesiten.

2. Proxy de Protección (Protection Proxy)

Controla el acceso al objeto original, utilizado cuando los objetos deben tener diferentes permisos de acceso.

3. Proxy Remoto (Remote Proxy)

Proporciona un representante local para un objeto que se encuentra en un espacio de direcciones diferente.

4. Proxy de Referencia Inteligente (Smart Reference Proxy)

Ejecuta operaciones adicionales al acceder al objeto, como recuento de referencias, carga diferida, etc.


Implementación del patrón Proxy en Python

Entendamos la implementación del patrón Proxy mediante ejemplos de código concretos.

Definición de interfaz base

Primero, definimos la interfaz de tema abstracto:

Ejemplos

from abc import ABC, abstractmethod
from typing import Any

class Subject(ABC):
    """
Interfaz de sujeto abstracto, define las operaciones comunes del sujeto real y el proxy.
    """

   
    @abstractmethod
    def request(self) -> Any:
        """Método principal para ejecutar la solicitud"""
        pass

Implementación del tema real

Ejemplos

class RealSubject(Subject):
    """
Clase de sujeto real, que contiene la lógica central de negocio.
Normalmente, la creación y la inicialización son costosas.
    """

   
    def __init__(self, name: str):
        self.name = name
        print(f"Creando instancia de RealSubject: {self.name} (esta es una operación costosa)")
   
    def request(self) -> str:
        """Ejecutar la solicitud real"""
        print(f"RealSubject: procesando solicitud - {self.name}")
        return f"Respuesta de {self.name}"

Implementación de la clase proxy

Ejemplos

class Proxy(Subject):
    """
Clase proxy, controla el acceso al sujeto real.
Se pueden añadir funcionalidades adicionales, como carga diferida, control de acceso, etc.
    """

   
    def __init__(self, subject_name: str):
        self.subject_name = subject_name
        self._real_subject = None  # Inicialización diferida
   
    def _lazy_init(self) -> None:
        """Carga diferida del objeto del sujeto real"""
        if self._real_subject is None:
            self._real_subject = RealSubject(self.subject_name)
   
    def request(self) -> str:
        """Método de solicitud del proxy, se puede añadir lógica adicional"""
        print("Proxy: realizar preprocesamiento antes de llamar al objeto real")
       
        # Carga diferida del objeto real
        self._lazy_init()
       
        # Llamar al método del objeto real
        result = self._real_subject.request()
       
        print("Proxy: realizar posprocesamiento después de llamar al objeto real")
        return f"Resultado mejorado por el proxy: {result}"

Código del cliente

Ejemplos

def client_code(subject: Subject) -> None:
    """
Código del cliente, que interactúa con el sujeto a través de la interfaz abstracta
No sabe ni le importa si se usa el sujeto real o el proxy
    """

    print("Cliente: iniciando la operación")
    result = subject.request()
    print(f"Cliente: resultado recibido - {result}")
    print()

# Ejemplo de uso
if __name__ == "__main__":
    print("Usando directamente el sujeto real:")
    real_subject = RealSubject("Objeto real A")
    client_code(real_subject)
   
    print("Usando el proxy:")
    proxy = Proxy("Objeto proxy B")
    client_code(proxy)
   
    # Usar el mismo proxy de nuevo para observar el efecto de carga diferida
    print("Usando el mismo proxy nuevamente:")
    client_code(proxy)

Al ejecutar el código anterior, verás la siguiente salida:

直接使用真实主题:
创建 RealSubject 实例: 真实对象A (这是一个昂贵的操作)
客户端: 开始执行操作
RealSubject: 处理请求 - 真实对象A
客户端: 收到结果 - 来自 真实对象A 的响应

使用代理:
客户端: 开始执行操作
Proxy: 在调用真实对象前进行预处理
创建 RealSubject 实例: 代理对象B (这是一个昂贵的操作)
RealSubject: 处理请求 - 代理对象B
Proxy: 在调用真实对象后进行后处理
客户端: 收到结果 - 代理增强的结果: 来自 代理对象B 的响应

再次使用同一个代理:
客户端: 开始执行操作
Proxy: 在调用真实对象前进行预处理
RealSubject: 处理请求 - 代理对象B
Proxy: 在调用真实对象后进行后处理
客户端: 收到结果 - 代理增强的结果: 来自 代理对象B 的响应

Ejemplo de aplicación práctica: proxy de carga de imágenes

Veamos un ejemplo más cercano a la realidad: el proxy virtual para la carga de imágenes.

Sistema de carga de imágenes

Ejemplos

from pathlib import Path
import time

class Image:
    """Clase de imagen real que simula la costosa operación de cargar una imagen grande"""
   
    def __init__(self, filename: str):
        self.filename = filename
        self._load_image()
   
    def _load_image(self) -> None:
        """Simula la costosa operación de cargar una imagen grande"""
        print(f"Cargando imagen: {self.filename} (esto puede tardar unos segundos...)")
        time.sleep(2)  # Simular el tiempo de carga
        print(f"¡Imagen {self.filename} cargada!")
   
    def display(self) -> None:
        """Muestra la imagen"""
        print(f"Mostrando imagen: {self.filename}")

class ImageProxy:
    Proxy de imagen que implementa carga diferida
   
    def __init__(self, filename: str):
        self.filename = filename
        self._image = None
   
    def display(self) -> None:
        Muestra la imagen; si es necesario, cárgala antes
        if self._image is None:
            print(Proxy: se ha detectado que la imagen no está cargada; iniciando la carga diferida...)
            self._image = Image(self.filename)
        else:
            print(Proxy: la imagen ya está cargada; se muestra directamente)
       
        self._image.display()

# Ejemplo de uso
def demo_image_proxy():
    print(Crear proxy de imagen (no cargará la imagen inmediatamente))
    proxy = ImageProxy("large_photo.jpg")
   
    print("\nPrimera llamada a display() - se dispara la carga diferida:)
    proxy.display()
   
    print("\nSegunda llamada a display() - se utiliza directamente la imagen ya cargada:)
    proxy.display()

# Ejecutar el ejemplo
demo_image_proxy()

Ejemplo de proxy de protección: control de acceso

El proxy de protección se utiliza para controlar el acceso a recursos sensibles.

Ejemplos

class SensitiveData:
    Clase de datos sensibles
   
    def __init__(self, data: str):
        self.data = data
   
    def read_data(self) -> str:
        Leer datos sensibles
        return fDatos sensibles: {self.data}

class ProtectionProxy:
    Proxy de protección que implementa control de acceso
   
    def __init__(self, sensitive_data: SensitiveData, user_role: str):
        self._sensitive_data = sensitive_data
        self.user_role = user_role
   
    def read_data(self) -> str:
        Leer los datos, pero comprobar los permisos
        if self.user_role != "admin":
            return Error: permisos insuficientes, solo el administrador puede acceder a los datos sensibles
       
        return self._sensitive_data.read_data()

# Ejemplo de uso
def demo_protection_proxy():
    sensitive_data = SensitiveData(Información confidencial: el presupuesto del proyecto es de 1 millón de yuanes)
   
    # El usuario normal intenta acceder
    user_proxy = ProtectionProxy(sensitive_data, "user")
    print(El usuario normal intenta acceder:)
    print(user_proxy.read_data())
   
    # Acceso del administrador
    admin_proxy = ProtectionProxy(sensitive_data, "admin")
    print("\nAcceso del administrador:)
    print(admin_proxy.read_data())

demo_protection_proxy()

Ventajas y desventajas del patrón Proxy

Ventajas

  1. Control de acceso a objetosEl proxy puede controlar cómo y cuándo el cliente accede al objeto real.
  2. Optimización de carga diferidaEl proxy virtual puede retrasar la creación de objetos costosos, mejorando el rendimiento.
  3. Mejorar la seguridad: El proxy de protección puede agregar lógica de control de acceso
  4. Principio abierto/cerrado: Se pueden introducir nuevos proxies sin modificar el código del cliente
  5. Separación de responsabilidades: El proxy puede manejar funciones auxiliares no relacionadas con la lógica de negocio principal

Desventajas

  1. Aumenta la complejidad: Introduce una nueva capa de abstracción, haciendo la estructura del código más compleja
  2. Latencia de respuesta: El proxy puede aumentar el tiempo de procesamiento de las solicitudes
  3. Posible sobre-diseño: Para escenarios simples, usar un proxy puede parecer innecesario

Escenarios de aplicación del patrón Proxy

El patrón Proxy es especialmente útil en los siguientes escenarios:

1. Carga diferida (Lazy Loading)

Cuando el costo de crear un objeto es alto, pero es posible que no se use de inmediato.

2. Control de acceso (Access Control)

Cuando se necesita restringir los permisos de acceso a ciertos objetos.

3. Representante local (Remote Representation)

Proporciona una interfaz local para objetos remotos, como llamadas RPC.

4. Registro de logs (Logging)

Agregar registro de logs antes y después de la llamada al método.

5. Caché (Caching)

Proporcionar caché para los resultados de operaciones costosas.


Práctica

Para consolidar la comprensión del patrón Proxy, intenta completar los siguientes ejercicios:

Ejercicio 1: proxy de caché

Crea un proxy de caché que agregue funcionalidad de caché a una función que calcula la secuencia de Fibonacci:

Ejemplos

class FibonacciCalculator:
    """Calcula la secuencia de Fibonacci"""
   
    def fibonacci(self, n: int) -> int:
        if n <= 1:
            return n
        return self.fibonacci(n - 1) + self.fibonacci(n - 2)

# Tu tarea: implementar la clase FibonacciCacheProxy
# Requisito: agregar caché al cálculo de Fibonacci para evitar cálculos repetidos

class FibonacciCacheProxy:
    def __init__(self):
        self._calculator = FibonacciCalculator()
        self._cache = {}  # Implementa la lógica de caché aquí
   
    def fibonacci(self, n: int) -> int:
        # Implementar la lógica de caché
        pass

# Código de prueba
def test_cache_proxy():
    proxy = FibonacciCacheProxy()
   
    print("Primer cálculo de fib(10):")
    result1 = proxy.fibonacci(10)
    print(f"Resultado: {result1}")
   
    print("Segundo cálculo de fib(10) (debería obtenerse de la caché):")
    result2 = proxy.fibonacci(10)
    print(f"Resultado: {result2}")
   
    print(f"Ambos resultados son iguales: {result1 == result2}")

Ejercicio 2: proxy de logs

Crea un proxy de registro que registre la información de las llamadas a métodos:

Ejemplos

class DatabaseService:
    """Servicio de base de datos"""
   
    def query(self, sql: str) -> str:
        return f"Ejecutar consulta: {sql}"
   
    def update(self, sql: str) -> str:
        return f"Ejecutar actualización: {sql}"

# Tu tarea: implementar la clase LoggingProxy
# Requisito: registrar el tiempo de llamada, los parámetros y el resultado de cada método

class LoggingProxy:
    def __init__(self):
        self._service = DatabaseService()
   
    def query(self, sql: str) -> str:
        # Agregar lógica de registro de logs
        pass
   
    def update(self, sql: str) -> str:
        # Agregar lógica de registro de logs
        pass

Resumen

El patrón Proxy es un patrón de diseño poderoso que controla el acceso a los objetos reales mediante la introducción de una capa intermedia. Implementar el patrón Proxy en Python es relativamente simple, gracias principalmente a las características dinámicas de Python.

Puntos clave

  1. El proxy es un intermediario: Actúa como intermediario entre el cliente y el objeto real
  2. Interfaz consistente: El proxy y el objeto real implementan la misma interfaz
  3. Control de acceso: El proxy puede agregar lógica de control adicional, como carga diferida, verificación de permisos, etc.
  4. Aplicación flexible: Elegir diferentes tipos de proxy según los requisitos
Otras extensiones