Паттерн Заместитель в Python

Паттерн Прокси — это структурный паттерн проектирования, который предоставляет замену или заполнитель для другого объекта, чтобы контролировать доступ к нему. Проще говоря, прокси — это посредник, который выступает связующим звеном между клиентом и целевым объектом.

Метафоры прокси из реальной жизни

Представьте сценарий из реальной жизни:

  • Агент по недвижимости: вам не нужно общаться напрямую с владельцем дома; вы можете узнать о вариантах недвижимости и запланировать просмотры через агента.
  • Агент знаменитости: Если компания хочет пригласить знаменитость для рекламы, ей нужно обсудить детали сотрудничества через агента.
  • Сетевой прокси-сервер: Ваш сетевой запрос сначала проходит через прокси-сервер, а затем прокси-сервер перенаправляет его на целевой веб-сайт.

В этих примерах посредник/агент — это прокси, и он контролирует доступ к реальному объекту.


Основные компоненты паттерна «Прокси»

Паттерн «Прокси» обычно включает три ключевые роли:

1. Абстрактный субъект (Subject)

Определяет общий интерфейс для реального субъекта и прокси-субъекта, чтобы прокси-субъект можно было использовать везде, где используется реальный субъект.

2. Реальный субъект

Определяет реальный объект, который представляет прокси; это объект, к которому в конечном итоге будет происходить обращение.

3. Прокси

Хранит ссылку, которая позволяет прокси получать доступ к сущности, предоставляет интерфейс, идентичный интерфейсу реального субъекта, и контролирует доступ к реальному субъекту.


Типы паттерна «Прокси»

Паттерн Прокси (Proxy) имеет несколько вариантов, каждый с разными сценариями использования:

1. Виртуальный прокси

Откладывает создание дорогостоящих объектов до момента, когда они действительно понадобятся.

2. Защитный прокси

Контролирует доступ к исходному объекту; используется, когда объект должен иметь разные права доступа.

3. Удалённый прокси

Предоставляет локального представителя для объекта, находящегося в другом адресном пространстве.

4. Умный ссылочный прокси

Выполняет дополнительные операции при доступе к объекту, такие как подсчёт ссылок, ленивая загрузка и т. д.


Реализация паттерна Прокси в Python

Давайте разберём реализацию паттерна Прокси на конкретных примерах кода.

Определение базового интерфейса

Сначала мы определяем интерфейс абстрактного субъекта:

Пример

from abc import ABC, abstractmethod
from typing import Any

class Subject(ABC):
    """
Интерфейс абстрактного субъекта, определяющий общие операции реального субъекта и прокси
    """

   
    @abstractmethod
    def request(self) -> Any:
        """Основной метод для выполнения запроса"""
        pass

Реализация реального субъекта

Пример

class RealSubject(Subject):
    """
Класс реального субъекта, содержащий основную бизнес-логику
Обычно дорого создавать и инициализировать
    """

   
    def __init__(self, name: str):
        self.name = name
        print(f"Создание экземпляра RealSubject: {self.name} (это дорогостоящая операция)")
   
    def request(self) -> str:
        """Выполнить реальный запрос"""
        print(f"RealSubject: обработка запроса - {self.name}")
        return f"Ответ от {self.name}"

Реализация класса Proxy

Пример

class Proxy(Subject):
    """
Класс Proxy, контролирующий доступ к реальному субъекту
Можно добавить дополнительные возможности, такие как ленивая загрузка, контроль доступа и т. д.
    """

   
    def __init__(self, subject_name: str):
        self.subject_name = subject_name
        self._real_subject = None  # Ленивая инициализация
   
    def _lazy_init(self) -> None:
        """Ленивая загрузка объекта реального субъекта"""
        if self._real_subject is None:
            self._real_subject = RealSubject(self.subject_name)
   
    def request(self) -> str:
        """Метод request прокси, куда можно добавить дополнительную логику"""
        print("Прокси: предварительная обработка перед вызовом реального объекта")
       
        # Ленивая загрузка реального объекта
        self._lazy_init()
       
        # Вызов метода реального объекта
        result = self._real_subject.request()
       
        print("Прокси: постобработка после вызова реального объекта")
        return f"Улучшенный результат от прокси: {result}"

Клиентский код

Пример

def client_code(subject: Subject) -> None:
    """
Клиентский код, взаимодействующий с субъектом через абстрактный интерфейс
Не знает и не заботится, использует ли он реальный субъект или прокси
    """

    print("Клиент: начало операции")
    result = subject.request()
    print(f"Клиент: получен результат - {result}")
    print()

# Пример использования
if __name__ == "__main__":
    print("Использование реального субъекта напрямую:")
    real_subject = RealSubject("Реальный объект A")
    client_code(real_subject)
   
    print("Использование прокси:")
    proxy = Proxy("Прокси-объект B")
    client_code(proxy)
   
    # Используйте тот же прокси снова, чтобы наблюдать эффект ленивой загрузки
    print("Использование того же прокси ещё раз:")
    client_code(proxy)

Запустив приведённый выше код, вы увидите следующий вывод:

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

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

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

Пример из реального мира: прокси загрузки изображений

Давайте рассмотрим более практичный пример — виртуальный прокси для загрузки изображений.

Система загрузки изображений

Пример

from pathlib import Path
import time

class Image:
    """Класс реального изображения, имитирующий затратную операцию загрузки большого изображения"""
   
    def __init__(self, filename: str):
        self.filename = filename
        self._load_image()
   
    def _load_image(self) -> None:
        """Имитация затратной операции загрузки большого изображения"""
        print(f"Загрузка изображения: {self.filename} (это может занять несколько секунд...)")
        time.sleep(2)  # Имитация времени загрузки
        print(f"Изображение {self.filename} успешно загружено!")
   
    def display(self) -> None:
        """Отобразить изображение"""
        print(f"Отображение изображения: {self.filename}")

class ImageProxy:
    """Прокси изображения, реализующий ленивую загрузку"""
   
    def __init__(self, filename: str):
        self.filename = filename
        self._image = None
   
    def display(self) -> None:
        """Отобразить изображение, при необходимости сначала загрузив его"""
        if self._image is None:
            print("Прокси: обнаружено, что изображение не загружено, запуск ленивой загрузки...")
            self._image = Image(self.filename)
        else:
            print("Прокси: изображение уже загружено, отображается напрямую")
       
        self._image.display()

# Пример использования
def demo_image_proxy():
    print("Создание прокси изображения (изображение не будет загружено немедленно)")
    proxy = ImageProxy("large_photo.jpg")
   
    print("\n"Первый вызов display() - запускает ленивую загрузку:")
    proxy.display()
   
    print("\n"Второй вызов display() - напрямую использует уже загруженное изображение:")
    proxy.display()

# Запуск примера
demo_image_proxy()

Пример защитного прокси: контроль доступа

Защитные прокси используются для контроля доступа к конфиденциальным ресурсам.

Пример

class SensitiveData:
    """Класс конфиденциальных данных"""
   
    def __init__(self, data: str):
        self.data = data
   
    def read_data(self) -> str:
        """Чтение конфиденциальных данных"""
        return f"Конфиденциальные данные: {self.data}"

class ProtectionProxy:
    """Защитный прокси, реализующий контроль доступа"""
   
    def __init__(self, sensitive_data: SensitiveData, user_role: str):
        self._sensitive_data = sensitive_data
        self.user_role = user_role
   
    def read_data(self) -> str:
        """Чтение данных с проверкой прав доступа"""
        if self.user_role != "admin":
            return "Ошибка: недостаточно прав, только администраторы могут получить доступ к конфиденциальным данным"
       
        return self._sensitive_data.read_data()

# Пример использования
def demo_protection_proxy():
    sensitive_data = SensitiveData("Конфиденциальная информация: бюджет проекта составляет 1 миллион юаней")
   
    # Обычный пользователь пытается получить доступ
    user_proxy = ProtectionProxy(sensitive_data, "user")
    print("Обычный пользователь пытается получить доступ:")
    print(user_proxy.read_data())
   
    # Доступ администратора
    admin_proxy = ProtectionProxy(sensitive_data, "admin")
    print("\nДоступ администратора:)
    print(admin_proxy.read_data())

demo_protection_proxy()

Преимущества и недостатки паттерна «Прокси»

Преимущества

  1. Контроль доступа к объекту: Прокси может контролировать, как и когда клиент обращается к реальному объекту
  2. Оптимизация ленивой загрузки: Виртуальные прокси могут отложить создание дорогостоящих объектов, повышая производительность
  3. Повышенная безопасность: Защитный прокси может добавлять логику контроля доступа
  4. Принцип открытости/закрытости: Новые прокси можно вводить без изменения клиентского кода
  5. Разделение ответственности: Прокси может обрабатывать вспомогательные функции, не связанные с основной бизнес-логикой

Недостатки

  1. Повышенная сложность: Введение нового слоя абстракции делает структуру кода более сложной
  2. Задержка отклика: Прокси может увеличить время обработки запроса
  3. Возможность избыточного проектирования: В простых сценариях использование прокси может показаться ненужным

Сценарии применения паттерна «Прокси»

Паттерн «Прокси» особенно полезен в следующих сценариях:

1. Ленивая загрузка

Когда создание объекта дорогостоящее, но объект может не использоваться сразу.

2. Контроль доступа

Когда необходимо ограничить права доступа к определённым объектам.

3. Локальный представитель (удалённое представление)

Предоставить локальный интерфейс для удалённых объектов, например, для вызовов RPC.

4. Логирование

Добавить логирование до и после вызовов методов.

5. Кэширование

Обеспечить кэширование результатов дорогостоящих операций.


Практические упражнения

Чтобы закрепить понимание паттерна «Прокси», попробуйте выполнить следующие упражнения:

Упражнение 1: Кэширующий прокси

Создайте кэширующий прокси, который добавляет кэширование функции, вычисляющей последовательность Фибоначчи:

Пример

class FibonacciCalculator:
    """Вычислить последовательность Фибоначчи"""
   
    def fibonacci(self, n: int) -> int:
        if n <= 1:
            return n
        return self.fibonacci(n - 1) + self.fibonacci(n - 2)

# Ваша задача: реализовать класс FibonacciCacheProxy
# Требование: добавить кэширование к вычислению чисел Фибоначчи, чтобы избежать повторных вычислений

class FibonacciCacheProxy:
    def __init__(self):
        self._calculator = FibonacciCalculator()
        self._cache = {}  # Реализуйте здесь логику кэширования
   
    def fibonacci(self, n: int) -> int:
        # Реализуйте логику кэширования
        pass

# Тестовый код
def test_cache_proxy():
    proxy = FibonacciCacheProxy()
   
    print("Первое вычисление fib(10):")
    result1 = proxy.fibonacci(10)
    print(f"Результат: {result1}")
   
    print("Второе вычисление fib(10) (должно быть получено из кэша):")
    result2 = proxy.fibonacci(10)
    print(f"Результат: {result2}")
   
    print(f"Оба результата совпадают: {result1 == result2}")

Упражнение 2: Логирующий прокси

Создайте логирующий прокси, который записывает информацию о вызовах методов:

Пример

class DatabaseService:
    """Служба базы данных"""
   
    def query(self, sql: str) -> str:
        return f"Выполнение запроса: {sql}"
   
    def update(self, sql: str) -> str:
        return f"Выполняется обновление: {sql}"

# Ваша задача: реализовать класс LoggingProxy
# Требование: записывать время вызова, параметры и результат каждого метода

class LoggingProxy:
    def __init__(self):
        self._service = DatabaseService()
   
    def query(self, sql: str) -> str:
        # Добавить логику логирования
        pass
   
    def update(self, sql: str) -> str:
        # Добавить логику логирования
        pass

Итог

Паттерн Proxy — мощный паттерн проектирования, который контролирует доступ к реальным объектам, вводя промежуточный слой. Реализация паттерна Proxy в Python относительно проста, в основном благодаря динамическим особенностям Python.

Ключевые моменты

  1. Прокси — это посредник: выступает промежуточным звеном между клиентом и реальным объектом
  2. Единый интерфейс: прокси и реальный объект реализуют один и тот же интерфейс
  3. Контроль доступа: прокси может добавлять дополнительную логику контроля, например ленивую загрузку, проверку прав и т. д.
  4. Гибкое применение: выбирайте разные типы прокси в соответствии с потребностями
Другие расширения