Patron Procuration en Python

Le patron Proxy est un patron de conception structurel qui fournit un substitut ou un espace réservé pour un autre objet afin de contrôler l'accès à celui-ci. En termes simples, un proxy est un intermédiaire qui fait le lien entre le client et l'objet cible.

Métaphores du proxy dans la vie réelle

Imaginez un scénario de la vie réelle :

  • Agent immobilier: Vous n'avez pas besoin de communiquer directement avec le propriétaire ; vous pouvez découvrir les annonces immobilières et planifier des visites grâce à l'agent.
  • Agent de célébrité: Si une entreprise souhaite inviter une célébrité pour un endossement, elle doit négocier les détails de la coopération par l'intermédiaire de l'agent.
  • Serveur proxy réseau: Votre requête réseau passe d'abord par le serveur proxy, puis le serveur proxy la transmet au site web cible.

Dans ces exemples, l'intermédiaire/l'agent est le proxy, et il contrôle l'accès à l'objet réel.


Composants principaux du patron Proxy

Le patron Proxy implique généralement trois rôles clés :

1. Sujet abstrait (Subject)

Définit une interface commune pour le sujet réel et le sujet proxy, afin que le sujet proxy puisse être utilisé partout où le sujet réel est utilisé.

2. Sujet réel

Définit l'objet réel que le proxy représente ; c'est l'objet qui sera finalement référencé.

3. Proxy

Détient une référence qui permet au proxy d'accéder à l'entité, fournit une interface identique au sujet réel et contrôle l'accès au sujet réel.


Types du patron Proxy

Le patron Proxy possède plusieurs variantes, chacune avec des cas d'utilisation différents :

1. Proxy virtuel

Retarde la création d'objets coûteux jusqu'à ce qu'ils soient réellement nécessaires.

2. Proxy de protection

Contrôle l'accès à l'objet original, utilisé lorsque l'objet doit avoir différentes autorisations d'accès.

3. Proxy distant

Fournit un représentant local pour un objet situé dans un espace d'adressage différent.

4. Proxy de référence intelligent

Effectue des opérations supplémentaires lors de l'accès à l'objet, telles que le comptage de références, le chargement paresseux, etc.


Implémenter le patron Proxy en Python

Comprenons la mise en œuvre du patron Proxy à travers des exemples de code spécifiques.

Définition de l'interface de base

Tout d'abord, nous définissons l'interface de sujet abstrait :

Exemple

from abc import ABC, abstractmethod
from typing import Any

class Subject(ABC):
    """
Interface de sujet abstrait, définissant les opérations communes du sujet réel et du proxy
    """

   
    @abstractmethod
    def request(self) -> Any:
        """Méthode principale pour exécuter une requête"""
        pass

Implémentation du sujet réel

Exemple

class RealSubject(Subject):
    """
Classe du sujet réel, contenant la logique métier principale
Généralement coûteux à créer et à initialiser
    """

   
    def __init__(self, name: str):
        self.name = name
        print(f"Création d'une instance de RealSubject : {self.name} (cette opération est coûteuse)")
   
    def request(self) -> str:
        """Exécute la requête réelle"""
        print(f"RealSubject : traitement de la requête - {self.name}")
        return f"Réponse de {self.name}"

Implémentation de la classe Proxy

Exemple

class Proxy(Subject):
    """
Classe Proxy, contrôlant l'accès au sujet réel
Des fonctionnalités supplémentaires peuvent être ajoutées, telles que le chargement paresseux, le contrôle d'accès, etc.
    """

   
    def __init__(self, subject_name: str):
        self.subject_name = subject_name
        self._real_subject = None  # Initialisation paresseuse
   
    def _lazy_init(self) -> None:
        """Charger paresseusement l'objet sujet réel"""
        if self._real_subject is None:
            self._real_subject = RealSubject(self.subject_name)
   
    def request(self) -> str:
        """Méthode request du Proxy, où une logique supplémentaire peut être ajoutée"""
        print("Proxy : prétraitement avant d'appeler l'objet réel")
       
        # Charger l'objet réel de manière paresseuse
        self._lazy_init()
       
        # Appeler la méthode de l'objet réel
        result = self._real_subject.request()
       
        print("Proxy : post-traitement après l'appel de l'objet réel")
        return f"Résultat amélioré du proxy : {result}"

Code client

Exemple

def client_code(subject: Subject) -> None:
    """
Code client, interagissant avec le sujet via l'interface abstraite
Ne sait pas et ne se soucie pas de savoir s'il utilise le sujet réel ou le proxy
    """

    print("Client : démarrage de l'opération")
    result = subject.request()
    print(f"Client : résultat reçu - {result}")
    print()

# Exemple d'utilisation
if __name__ == "__main__":
    print("Utilisation directe du sujet réel :")
    real_subject = RealSubject("Objet réel A")
    client_code(real_subject)
   
    print("Utilisation du proxy :")
    proxy = Proxy("Objet proxy B")
    client_code(proxy)
   
    # Utilisez à nouveau le même proxy pour observer l'effet de chargement paresseux
    print("Utilisation du même proxy à nouveau :")
    client_code(proxy)

En exécutant le code ci-dessus, vous verrez la sortie suivante :

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

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

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

Exemple concret : proxy de chargement d'images

Regardons un exemple plus pratique — un proxy virtuel pour le chargement d'images.

Système de chargement d'images

Exemple

from pathlib import Path
import time

class Image:
    """Classe d'image réelle, simulant l'opération coûteuse de chargement d'une grande image"""
   
    def __init__(self, filename: str):
        self.filename = filename
        self._load_image()
   
    def _load_image(self) -> None:
        """Simuler l'opération coûteuse de chargement d'une grande image"""
        print(f"Chargement de l'image : {self.filename} (cela peut prendre quelques secondes...)")
        time.sleep(2)  # Simuler le temps de chargement
        print(f"Image {self.filename} chargée avec succès !")
   
    def display(self) -> None:
        """Afficher l'image"""
        print(f"Affichage de l'image : {self.filename}")

class ImageProxy:
    """Proxy d'image, implémentant le chargement différé"""
   
    def __init__(self, filename: str):
        self.filename = filename
        self._image = None
   
    def display(self) -> None:
        """Afficher l'image, en la chargeant d'abord si nécessaire"""
        if self._image is None:
            print("Proxy : image non chargée détectée, démarrage du chargement différé...")
            self._image = Image(self.filename)
        else:
            print("Proxy : l'image est déjà chargée, affichage direct")
       
        self._image.display()

# Exemple d'utilisation
def demo_image_proxy():
    print("Création du proxy d'image (l'image ne sera pas chargée immédiatement)")
    proxy = ImageProxy("large_photo.jpg")
   
    print("\n"Premier appel à display() - déclenche le chargement différé :")
    proxy.display()
   
    print("\n"Deuxième appel à display() - utilise directement l'image déjà chargée :")
    proxy.display()

# Exécuter l'exemple
demo_image_proxy()

Exemple de proxy de protection : contrôle d'accès

Les proxys de protection sont utilisés pour contrôler l'accès aux ressources sensibles.

Exemple

class SensitiveData:
    """Classe de données sensibles"""
   
    def __init__(self, data: str):
        self.data = data
   
    def read_data(self) -> str:
        """Lire les données sensibles"""
        return f"Données sensibles : {self.data}"

class ProtectionProxy:
    """Proxy de protection, implémentant le contrôle d'accès"""
   
    def __init__(self, sensitive_data: SensitiveData, user_role: str):
        self._sensitive_data = sensitive_data
        self.user_role = user_role
   
    def read_data(self) -> str:
        """Lire les données, mais effectuer une vérification des autorisations"""
        if self.user_role != "admin":
            return "Erreur : autorisations insuffisantes, seuls les administrateurs peuvent accéder aux données sensibles"
       
        return self._sensitive_data.read_data()

# Exemple d'utilisation
def demo_protection_proxy():
    sensitive_data = SensitiveData("Informations confidentielles : le budget du projet est de 1 million yuan")
   
    # Un utilisateur ordinaire tente d'accéder
    user_proxy = ProtectionProxy(sensitive_data, "user")
    print("Un utilisateur ordinaire tente d'accéder :")
    print(user_proxy.read_data())
   
    # L'administrateur accède
    admin_proxy = ProtectionProxy(sensitive_data, "admin")
    print("\nAccès administrateur :)
    print(admin_proxy.read_data())

demo_protection_proxy()

Avantages et inconvénients du patron Proxy

Avantages

  1. Contrôler l'accès aux objets: Le proxy peut contrôler comment et quand le client accède à l'objet réel
  2. Optimisation du chargement différé: Les proxies virtuels peuvent retarder la création d'objets coûteux, améliorant ainsi les performances
  3. Sécurité renforcée: Le proxy de protection peut ajouter une logique de contrôle d'accès
  4. Principe ouvert/fermé: De nouveaux proxies peuvent être introduits sans modifier le code client
  5. Séparation des préoccupations: Le proxy peut gérer des fonctions auxiliaires sans rapport avec la logique métier principale

Inconvénients

  1. Complexité accrue: L'introduction d'une nouvelle couche d'abstraction rend la structure du code plus complexe
  2. Latence de réponse: Le proxy peut augmenter le temps de traitement des requêtes
  3. Possibilité de sur-ingénierie: Pour des scénarios simples, l'utilisation d'un proxy peut sembler inutile

Scénarios d'application du patron Proxy

Le patron Proxy est particulièrement utile dans les scénarios suivants :

1. Chargement différé

Lorsque la création d'un objet est coûteuse mais que l'objet peut ne pas être utilisé immédiatement.

2. Contrôle d'accès

Lorsque les droits d'accès à certains objets doivent être restreints.

3. Représentant local (représentation distante)

Fournir une interface locale pour des objets distants, tels que les appels RPC.

4. Journalisation

Ajouter une journalisation avant et après les appels de méthode.

5. Mise en cache

Fournir une mise en cache pour les résultats d'opérations coûteuses.


Exercices pratiques

Pour renforcer votre compréhension du patron Proxy, essayez de réaliser les exercices suivants :

Exercice 1 : Proxy de mise en cache

Créez un proxy de mise en cache qui ajoute une fonctionnalité de mise en cache à une fonction qui calcule la suite de Fibonacci :

Exemple

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

# Votre tâche : implémenter la classe FibonacciCacheProxy
# Exigence : ajouter une mise en cache au calcul de Fibonacci pour éviter les calculs répétés

class FibonacciCacheProxy:
    def __init__(self):
        self._calculator = FibonacciCalculator()
        self._cache = {}  # Implémenter la logique de mise en cache ici
   
    def fibonacci(self, n: int) -> int:
        # Implémenter la logique de mise en cache
        pass

# Code de test
def test_cache_proxy():
    proxy = FibonacciCacheProxy()
   
    print("Premier calcul de fib(10) :")
    result1 = proxy.fibonacci(10)
    print(f"Résultat : {result1}")
   
    print("Deuxième calcul de fib(10) (devrait être obtenu à partir du cache) :")
    result2 = proxy.fibonacci(10)
    print(f"Résultat : {result2}")
   
    print(f"Les deux résultats sont identiques : {result1 == result2}")

Exercice 2 : Proxy de journalisation

Créez un proxy de journalisation qui enregistre les informations sur les appels de méthode :

Exemple

class DatabaseService:
    """Service de base de données"""
   
    def query(self, sql: str) -> str:
        return f"Exécution de la requête : {sql}"
   
    def update(self, sql: str) -> str:
        return f"Exécution de la mise à jour : {sql}"

# Votre tâche : implémenter la classe LoggingProxy
# Exigence : enregistrer l'heure d'appel, les paramètres et le résultat de chaque méthode

class LoggingProxy:
    def __init__(self):
        self._service = DatabaseService()
   
    def query(self, sql: str) -> str:
        # Ajouter la logique de journalisation
        pass
   
    def update(self, sql: str) -> str:
        # Ajouter la logique de journalisation
        pass

Résumé

Le patron Proxy est un patron de conception puissant qui contrôle l'accès aux objets réels en introduisant une couche intermédiaire. L'implémentation du patron Proxy en Python est relativement simple, principalement grâce aux fonctionnalités dynamiques de Python.

Points clés

  1. Le proxy est un médiateur: Il agit comme un intermédiaire entre le client et l'objet réel
  2. Interface cohérente: Le proxy et l'objet réel implémentent la même interface
  3. Contrôle d'accès: Le proxy peut ajouter une logique de contrôle supplémentaire, comme le chargement paresseux, les vérifications de permissions, etc.
  4. Application flexible: Choisir différents types de proxies selon les besoins
Autres extensions