Перейти к содержанию

Заместитель

Также известен как: Proxy

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

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

Проблема

Для чего вообще контролировать доступ к объектам? Рассмотрим такой пример: у вас есть внешний ресурсоёмкий объект, который нужен не все время, а изредка.

Проблема, которую решает Заместитель

Запросы к базе данных могут быть очень медленными.

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

В идеале, этот код хотелось бы поместить прямо в служебный класс, но это не всегда возможно. Например, код класса может находиться в закрытой сторонней библиотеке.

Решение

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

Решение с помощью Заместителя

Заместитель «притворяется» базой данных, ускоряя работу за счёт ленивой инициализации и кеширования повторяющихся запросов.

Но в чём же здесь польза? Вы могли бы поместить в класс заместителя какую-то промежуточную логику, которая выполнялась бы до (или после) вызовов этих же методов в настоящем объекте. А благодаря одинаковому интерфейсу, объект-заместитель можно передать в любой код, ожидающий сервисный объект.

Аналогия из жизни

Платёжная карта — это заместитель пачки наличных

Платёжной картой можно расплачиваться, как и наличными.

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

Структура

Структура классов паттерна Заместитель

  1. Интерфейс сервиса определяет общий интерфейс для сервиса и заместителя. Благодаря этому, объект заместителя можно использовать там, где ожидается объект сервиса.
  2. Сервис содержит полезную бизнес-логику.
  3. Заместитель хранит ссылку на объект сервиса. После того как заместитель заканчивает свою работу (например, инициализацию, логирование, защиту или другое), он передаёт вызовы вложенному сервису.
    Заместитель может сам отвечать за создание и удаление объекта сервиса.
  4. Клиент работает с объектами через интерфейс сервиса. Благодаря этому, его можно «одурачить», подменив объект сервиса объектом заместителя.

Псевдокод

В этом примере Заместитель помогает добавить в программу механизм ленивой инициализации и кеширования результатов работы библиотеки интеграции с YouTube.

Структура классов примера паттерна Заместитель

Пример кеширования результатов работы реального сервиса с помощью заместителя.

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

// Интерфейс удалённого сервиса.
interface ThirdPartyYouTubeLib is
    method listVideos()
    method getVideoInfo(id)
    method downloadVideo(id)

// Конкретная реализация сервиса. Методы этого класса
// запрашивают у YouTube различную информацию. Скорость запроса
// зависит не только от качества интернет-канала пользователя,
// но и от состояния самого YouTube. Значит, чем больше будет
// вызовов к сервису, тем менее отзывчивой станет программа.
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib is
    method listVideos() is
        // Получить список видеороликов с помощью API YouTube.

    method getVideoInfo(id) is
        // Получить детальную информацию о каком-то видеоролике.

    method downloadVideo(id) is
        // Скачать видео с YouTube.

// С другой стороны, можно кешировать запросы к YouTube и не
// повторять их какое-то время, пока кеш не устареет. Но внести
// этот код напрямую в сервисный класс нельзя, так как он
// находится в сторонней библиотеке. Поэтому мы поместим логику
// кеширования в отдельный класс-обёртку. Он будет делегировать
// запросы к сервисному объекту, только если нужно
// непосредственно выслать запрос.
class CachedYouTubeClass implements ThirdPartyYouTubeLib is
    private field service: ThirdPartyYouTubeLib
    private field listCache, videoCache
    field needReset

    constructor CachedYouTubeClass(service: ThirdPartyYouTubeLib) is
        this.service = service

    method listVideos() is
        if (listCache == null || needReset)
            listCache = service.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id)
        return videoCache

    method downloadVideo(id) is
        if (!downloadExists(id) || needReset)
            service.downloadVideo(id)

// Класс GUI, который использует сервисный объект. Вместо
// реального сервиса, мы подсунем ему объект-заместитель. Клиент
// ничего не заметит, так как заместитель имеет тот же
// интерфейс, что и сервис.
class YouTubeManager is
    protected field service: ThirdPartyYouTubeLib

    constructor YouTubeManager(service: ThirdPartyYouTubeLib) is
        this.service = service

    method renderVideoPage(id) is
        info = service.getVideoInfo(id)
        // Отобразить страницу видеоролика.

    method renderListPanel() is
        list = service.listVideos()
        // Отобразить список превьюшек видеороликов.

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()

// Конфигурационная часть приложения создаёт и передаёт клиентам
// объект заместителя.
class Application is
    method init() is
        YouTubeService = new ThirdPartyYouTubeClass()
        YouTubeProxy = new CachedYouTubeClass(YouTubeService)
        manager = new YouTubeManager(YouTubeProxy)
        manager.reactOnUserInput()

Применимость

Ленивая инициализация (виртуальный прокси). Когда у вас есть тяжёлый объект, грузящий данные из файловой системы или базы данных.

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

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

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

Локальный запуск сервиса (удалённый прокси). Когда настоящий сервисный объект находится на удалённом сервере.

В этом случае заместитель транслирует запросы клиента в вызовы по сети в протоколе, понятном удалённому сервису.


Логирование запросов (логирующий прокси). Когда требуется хранить историю обращений к сервисному объекту.

Заместитель может сохранять историю обращения клиента к сервисному объекту.

Кеширование объектов («умная» ссылка). Когда нужно кешировать результаты запросов клиентов и управлять их жизненным циклом.

Заместитель может подсчитывать количество ссылок на сервисный объект, которые были отданы клиенту и остаются активными. Когда все ссылки освобождаются, можно будет освободить и сам сервисный объект (например, закрыть подключение к базе данных).

Кроме того, Заместитель может отслеживать, не менял ли клиент сервисный объект. Это позволит использовать объекты повторно и здóрово экономить ресурсы, особенно если речь идёт о больших прожорливых сервисах.

Шаги реализации

  1. Определите интерфейс, который бы сделал заместитель и оригинальный объект взаимозаменяемыми.
  2. Создайте класс заместителя. Он должен содержать ссылку на сервисный объект. Чаще всего, сервисный объект создаётся самим заместителем. В редких случаях заместитель получает готовый сервисный объект от клиента через конструктор.
  3. Реализуйте методы заместителя в зависимости от его предназначения. В большинстве случаев, проделав какую-то полезную работу, методы заместителя должны передать запрос сервисному объекту.
  4. Подумайте о введении фабрики, которая решала бы, какой из объектов создавать — заместитель или реальный сервисный объект. Но, с другой стороны, эта логика может быть помещена в создающий метод самого заместителя.
  5. Подумайте, не реализовать ли вам ленивую инициализацию сервисного объекта при первом обращении клиента к методам заместителя.

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

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

  • Позволяет контролировать сервисный объект незаметно для клиента.
  • Может работать, даже если сервисный объект ещё не создан.
  • Может контролировать жизненный цикл служебного объекта.

Недостатки

  • Усложняет код программы из-за введения дополнительных классов.
  • Увеличивает время отклика от сервиса.

Примеры реализации паттерна

Python

  • Сложность: 2/3
  • Популярность: 1/3
  • Применимость: Паттерн Заместитель применяется в Python коде тогда, когда надо заменить настоящий объект его суррогатом, причём незаметно для клиентов настоящего объекта. Это позволит выполнить какие-то добавочные поведения до или после основного поведения настоящего объекта.
  • Признаки применения паттерна: Класс заместителя чаще всего делегирует всю настоящую работу своему реальному объекту. Заместители часто сами следят за жизненным циклом своего реального объекта.

Этот пример показывает структуру паттерна Заместитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

from abc import ABC, abstractmethod


class Subject(ABC):
    """
    Интерфейс Субъекта объявляет общие операции как для Реального
    Субъекта, так и для Заместителя. Пока клиент работает с Реальным
    Субъектом, используя этот интерфейс, вы сможете передать ему
    заместителя вместо реального субъекта.
    """

    @abstractmethod
    def request(self) -> None:
        pass


class RealSubject(Subject):
    """
    Реальный Субъект содержит некоторую базовую бизнес-логику. Как
    правило, Реальные Субъекты способны выполнять некоторую полезную
    работу, которая к тому же может быть очень медленной или точной –
    например, коррекция входных данных. Заместитель может решить эти
    задачи без каких-либо изменений в коде Реального Субъекта.
    """

    def request(self) -> None:
        print("RealSubject: Handling request.")


class Proxy(Subject):
    """
    Интерфейс Заместителя идентичен интерфейсу Реального Субъекта.
    """

    def __init__(self, real_subject: RealSubject) -> None:
        self._real_subject = real_subject

    def request(self) -> None:
        """
        Наиболее распространёнными областями применения паттерна
        Заместитель являются ленивая загрузка, кэширование, контроль
        доступа, ведение журнала и т.д. Заместитель может выполнить
        одну из этих задач, а затем, в зависимости от результата,
        передать выполнение одноимённому методу в связанном объекте
        класса Реального Субъекта.
        """

        if self.check_access():
            self._real_subject.request()
            self.log_access()

    def check_access(self) -> bool:
        print("Proxy: Checking access prior to firing a real "
            + "request.")
        return True

    def log_access(self) -> None:
        print("Proxy: Logging the time of request.", end="")


def client_code(subject: Subject) -> None:
    """
    Клиентский код должен работать со всеми объектами (как с
    реальными, так и заместителями) через интерфейс Субъекта, чтобы
    поддерживать как реальные субъекты, так и заместителей. В реальной
    жизни, однако, клиенты в основном работают с реальными субъектами
    напрямую. В этом случае, для более простой реализации паттерна,
    можно расширить заместителя из класса реального субъекта.
    """

    # ...

    subject.request()

    # ...


if __name__ == "__main__":
    print("Client: Executing the client code with a real subject:")
    real_subject = RealSubject()
    client_code(real_subject)

    print("")

    print("Client: Executing the same client code with a proxy:")
    proxy = Proxy(real_subject)
    client_code(proxy)
Client: Executing the client code with a real subject:
RealSubject: Handling request.

Client: Executing the same client code with a proxy:
Proxy: Checking access prior to firing a real request.
RealSubject: Handling request.
Proxy: Logging the time of request.

PHP

  • Сложность: 2/3
  • Популярность: 1/3
  • Применимость: Паттерн Заместитель применяется в PHP коде тогда, когда надо заменить настоящий объект его суррогатом, причём незаметно для клиентов настоящего объекта. Это позволит выполнить какие-то добавочные поведения до или после основного поведения настоящего объекта.
  • Признаки применения паттерна: Класс заместителя чаще всего делегирует всю настоящую работу своему реальному объекту. Заместители часто сами следят за жизненным циклом своего реального объекта.

Этот пример показывает структуру паттерна Заместитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

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

<?php
/**
* Интерфейс Субъекта объявляет общие операции как для Реального Субъекта, так и
* для Заместителя. Пока клиент работает с Реальным Субъектом, используя этот
* интерфейс, вы сможете передать ему заместителя вместо реального субъекта.
*/

interface Subject
{
    public function request(): void;
}

/**
* Реальный Субъект содержит некоторую базовую бизнес-логику. Как правило,
* Реальные Субъекты способны выполнять некоторую полезную работу, которая к
* тому же может быть очень медленной или точной – например, коррекция входных
* данных. Заместитель может решить эти задачи без каких-либо изменений в коде
* Реального Субъекта.
*/

class RealSubject implements Subject
{
    public function request(): void
    {
        echo "RealSubject: Handling request.\n";
    }
}

/**
* Интерфейс Заместителя идентичен интерфейсу Реального Субъекта.
*/

class Proxy implements Subject
{
    /**
    * @var RealSubject
    */
    private $realSubject;

    /**
    * Заместитель хранит ссылку на объект класса РеальныйСубъект. Клиент может
    * либо лениво загрузить его, либо передать Заместителю.
    */
    public function __construct(RealSubject $realSubject)
    {
        $this->realSubject = $realSubject;
    }

    /**
    * Наиболее распространёнными областями применения паттерна Заместитель
    * являются ленивая загрузка, кэширование, контроль доступа, ведение журнала
    * и т.д. Заместитель может выполнить одну из этих задач, а затем, в
    * зависимости от результата, передать выполнение одноимённому методу в
    * связанном объекте класса Реального Субъект.
    */
    public function request(): void
    {
        if ($this->checkAccess()) {
            $this->realSubject->request();
            $this->logAccess();
        }
    }

    private function checkAccess(): bool
    {
        // Некоторые реальные проверки должны проходить здесь.
        echo "Proxy: Checking access prior to firing a real request.\n";

        return true;
    }

    private function logAccess(): void
    {
        echo "Proxy: Logging the time of request.\n";
    }
}

/**
* Клиентский код должен работать со всеми объектами (как с реальными, так и
* заместителями) через интерфейс Субъекта, чтобы поддерживать как реальные
* субъекты, так и заместителей. В реальной жизни, однако, клиенты в основном
* работают с реальными субъектами напрямую. В этом случае, для более простой
* реализации паттерна, можно расширить заместителя из класса реального
* субъекта.
*/

function clientCode(Subject $subject)
{
    // ...

    $subject->request();

    // ...
}

echo "Client: Executing the client code with a real subject:\n";
$realSubject = new RealSubject();
clientCode($realSubject);

echo "\n";

echo "Client: Executing the same client code with a proxy:\n";
$proxy = new Proxy($realSubject);
clientCode($proxy);
Client: Executing the client code with a real subject:
RealSubject: Handling request.

Client: Executing the same client code with a proxy:
Proxy: Checking access prior to firing a real request.
RealSubject: Handling request.
Proxy: Logging the time of request.
<?php
/**
* Интерфейс Субъекта описывает интерфейс реального объекта.
*
* Дело в том, что у большинства приложений нет чётко определённого интерфейса.
* В этом случае лучше было бы расширить Заместителя за счёт существующего
* класса приложения. Если это неудобно, тогда первым шагом должно быть
* извлечение правильного интерфейса.
*/

interface Downloader
{
    public function download(string $url): string;
}

/**
* Реальный Субъект делает реальную работу, хотя и не самым эффективным
* способом. Когда клиент пытается загрузить тот же самый файл во второй раз,
* наш загрузчик именно это и делает, вместо того, чтобы извлечь результат из
* кэша.
*/

class SimpleDownloader implements Downloader
{
    public function download(string $url): string
    {
        echo "Downloading a file from the Internet.\n";
        $result = file_get_contents($url);
        echo "Downloaded bytes: " . strlen($result) . "\n";

        return $result;
    }
}

/**
* Класс Заместителя – это попытка сделать загрузку более эффективной. Он
* обёртывает реальный объект загрузчика и делегирует ему первые запросы на
* скачивание. Затем результат кэшируется, что позволяет последующим вызовам
* возвращать уже имеющийся файл вместо его повторной загрузки.
*/

class CachingDownloader implements Downloader
{
    /**
    * @var SimpleDownloader
    */
    private $downloader;

    /**
    * @var string[]
    */
    private $cache = [];

    public function __construct(SimpleDownloader $downloader)
    {
        $this->downloader = $downloader;
    }

    public function download(string $url): string
    {
        if (!isset($this->cache[$url])) {
            echo "CacheProxy MISS. ";
            $result = $this->downloader->download($url);
            $this->cache[$url] = $result;
        } else {
            echo "CacheProxy HIT. Retrieving result from cache.\n";
        }
        return $this->cache[$url];
    }
}

/**
* Клиентский код может выдать несколько похожих запросов на загрузку. В этом
* случае кэширующий заместитель экономит время и трафик, подавая результаты из
* кэша.
*
* Клиент не знает, что он работает с заместителем, потому что он работает с
* загрузчиками через абстрактный интерфейс.
*/

function clientCode(Downloader $subject)
{
    // ...
    $result = $subject->download("http://example.com/");
    // Повторяющиеся запросы на загрузку могут кэшироваться для увеличения
    // скорости.

    $result = $subject->download("http://example.com/");

    // ...
}

echo "Executing client code with real subject:\n";
$realSubject = new SimpleDownloader();
clientCode($realSubject);

echo "\n";

echo "Executing the same client code with a proxy:\n";
$proxy = new CachingDownloader($realSubject);
clientCode($proxy);
Executing client code with real subject:
Downloading a file from the Internet.
Downloaded bytes: 1270
Downloading a file from the Internet.
Downloaded bytes: 1270

Executing the same client code with a proxy:
CacheProxy MISS. Downloading a file from the Internet.
Downloaded bytes: 1270
CacheProxy HIT. Retrieving result from cache.

Задачи

Задача 1. Защитный прокси для доступа к файлам

Реализуйте защитный прокси SecureFileProxy для интерфейса FileService. Прокси проверяет права пользователя перед каждой операцией: read — требует роль reader или admin, write — только admin. При отсутствии прав бросать исключение. Клиентский код работает с интерфейсом и не знает о проверках.

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class User:
    name: str
    role: str  # "reader", "writer", "admin"

class FileService(ABC):
    @abstractmethod
    def read(self, filename: str) -> str: pass

    @abstractmethod
    def write(self, filename: str, content: str) -> None: pass

    @abstractmethod
    def delete(self, filename: str) -> None: pass

class RealFileService(FileService):
    def __init__(self):
        self._storage: dict[str, str] = {}

    def read(self, filename: str) -> str:
        content = self._storage.get(filename, "")
        print(f"[File] Читаю '{filename}': {content[:30]}")
        return content

    def write(self, filename: str, content: str) -> None:
        self._storage[filename] = content
        print(f"[File] Записываю '{filename}' ({len(content)} байт)")

    def delete(self, filename: str) -> None:
        self._storage.pop(filename, None)
        print(f"[File] Удаляю '{filename}'")

# TODO: реализовать SecureFileProxy(FileService):
# - __init__(service: FileService, user: User)
# - read(filename)   — разрешено для "reader", "writer", "admin"
#                      иначе PermissionError(f"Нет прав на чтение для роли '{user.role}'")
# - write(filename)  — разрешено для "writer", "admin"
#                      иначе PermissionError(...)
# - delete(filename) — только "admin"
#                      иначе PermissionError(...)

# Клиентский код
# def use_file_service(svc: FileService, filename: str, content: str):
#     svc.write(filename, content)
#     print(svc.read(filename))

# real = RealFileService()
# admin   = SecureFileProxy(real, User("Алиса", "admin"))
# reader  = SecureFileProxy(real, User("Боб",   "reader"))
# writer  = SecureFileProxy(real, User("Вера",  "writer"))

# admin.write("report.txt", "Секретный отчёт")
# print(reader.read("report.txt"))

# try:
#     reader.write("report.txt", "Попытка записи")
# except PermissionError as e:
#     print(f"Отказано: {e}")

# try:
#     writer.delete("report.txt")
# except PermissionError as e:
#     print(f"Отказано: {e}")
<?php
class User {
    public function __construct(
        public string $name,
        public string $role,  // "reader", "writer", "admin"
    ) {}
}

interface FileService {
    public function read(string $filename): string;
    public function write(string $filename, string $content): void;
    public function delete(string $filename): void;
}

class RealFileService implements FileService {
    private array $storage = [];

    public function read(string $filename): string {
        $content = $this->storage[$filename] ?? '';
        echo "[File] Читаю '{$filename}': " . substr($content, 0, 30) . "\n";
        return $content;
    }

    public function write(string $filename, string $content): void {
        $this->storage[$filename] = $content;
        echo "[File] Записываю '{$filename}' (" . strlen($content) . " байт)\n";
    }

    public function delete(string $filename): void {
        unset($this->storage[$filename]);
        echo "[File] Удаляю '{$filename}'\n";
    }
}

// TODO: реализовать SecureFileProxy implements FileService:
// - __construct(FileService $service, User $user)
// - read()   — разрешено для "reader", "writer", "admin"
//              иначе RuntimeException("Нет прав на чтение для роли '{$user->role}'")
// - write()  — разрешено для "writer", "admin"
// - delete() — только "admin"

// Клиентский код
// $real   = new RealFileService();
// $admin  = new SecureFileProxy($real, new User('Алиса', 'admin'));
// $reader = new SecureFileProxy($real, new User('Боб',   'reader'));
// $writer = new SecureFileProxy($real, new User('Вера',  'writer'));

// $admin->write('report.txt', 'Секретный отчёт');
// echo $reader->read('report.txt') . "\n";

// try {
//     $reader->write('report.txt', 'Попытка записи');
// } catch (RuntimeException $e) {
//     echo "Отказано: " . $e->getMessage() . "\n";
// }

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

Реализуйте виртуальный прокси LazyImageProxy для интерфейса Image. Реальное изображение HighResImage загружается из файла — это дорогая операция. Прокси откладывает загрузку до первого вызова display() или get_info(). Покажите, что при создании коллекции из 100 изображений реальная загрузка происходит только при обращении.

from abc import ABC, abstractmethod

class Image(ABC):
    @abstractmethod
    def display(self) -> None: pass

    @abstractmethod
    def get_info(self) -> dict: pass

class HighResImage(Image):
    def __init__(self, filepath: str):
        print(f"[Image] Загрузка из файла: {filepath}")  # дорогая операция
        self._filepath = filepath
        self._data = f"pixel_data_of_{filepath}"  # симуляция загрузки
        self._width = 3840
        self._height = 2160

    def display(self) -> None:
        print(f"[Image] Отрисовка {self._filepath} ({self._width}x{self._height})")

    def get_info(self) -> dict:
        return {"file": self._filepath, "width": self._width, "height": self._height}

# TODO: реализовать LazyImageProxy(Image):
# - __init__(filepath: str) — сохраняет путь, НЕ загружает изображение
# - _load() — приватный метод: загружает HighResImage если ещё не загружено
# - display() — вызывает _load(), затем делегирует в реальный объект
# - get_info() — вызывает _load(), затем делегирует
# - property is_loaded -> bool

# Ожидаемый клиентский код:
# images = [LazyImageProxy(f"photo_{i}.jpg") for i in range(100)]
# print(f"Создано {len(images)} прокси, реальных загрузок: 0")

# # Обращаемся только к первым трём
# for img in images[:3]:
#     img.display()

# loaded_count = sum(1 for img in images if img.is_loaded)
# print(f"Загружено реально: {loaded_count} из {len(images)}")
<?php
interface Image {
    public function display(): void;
    public function getInfo(): array;
}

class HighResImage implements Image {
    private string $data;
    private int $width  = 3840;
    private int $height = 2160;

    public function __construct(private string $filepath) {
        echo "[Image] Загрузка из файла: {$filepath}\n";
        $this->data = "pixel_data_of_{$filepath}";
    }

    public function display(): void {
        echo "[Image] Отрисовка {$this->filepath} ({$this->width}x{$this->height})\n";
    }

    public function getInfo(): array {
        return ['file' => $this->filepath, 'width' => $this->width, 'height' => $this->height];
    }
}

// TODO: реализовать LazyImageProxy implements Image:
// - __construct(string $filepath) — сохраняет путь, НЕ загружает изображение
// - private load(): void — загружает HighResImage если ещё не загружено
// - display() — вызывает load(), затем делегирует
// - getInfo() — вызывает load(), затем делегирует
// - isLoaded(): bool

// Ожидаемый клиентский код:
// $images = array_map(fn($i) => new LazyImageProxy("photo_{$i}.jpg"), range(0, 99));
// echo "Создано " . count($images) . " прокси, реальных загрузок: 0\n";

// foreach (array_slice($images, 0, 3) as $img) {
//     $img->display();
// }

// $loadedCount = count(array_filter($images, fn($img) => $img->isLoaded()));
// echo "Загружено реально: {$loadedCount} из " . count($images) . "\n";

Задача 3. Логирующий прокси для сервиса пользователей

Реализуйте логирующий прокси LoggingUserServiceProxy для интерфейса UserService. Прокси логирует каждый вызов метода: имя метода, аргументы, время выполнения и результат (или исключение). Реальная логика делегируется в RealUserService. Клиентский код не должен меняться.

import time
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str
    active: bool = True

class UserService(ABC):
    @abstractmethod
    def get_by_id(self, user_id: int) -> User | None: pass

    @abstractmethod
    def get_all(self) -> list[User]: pass

    @abstractmethod
    def create(self, name: str, email: str) -> User: pass

    @abstractmethod
    def deactivate(self, user_id: int) -> None: pass

class RealUserService(UserService):
    def __init__(self):
        self._db = {
            1: User(1, "Анна",  "[email protected]"),
            2: User(2, "Иван",  "[email protected]"),
        }
        self._next_id = 3

    def get_by_id(self, user_id: int) -> User | None:
        time.sleep(0.01)
        return self._db.get(user_id)

    def get_all(self) -> list[User]:
        time.sleep(0.02)
        return list(self._db.values())

    def create(self, name: str, email: str) -> User:
        time.sleep(0.01)
        user = User(self._next_id, name, email)
        self._db[self._next_id] = user
        self._next_id += 1
        return user

    def deactivate(self, user_id: int) -> None:
        time.sleep(0.01)
        if user_id not in self._db:
            raise ValueError(f"Пользователь #{user_id} не найден")
        self._db[user_id].active = False

# TODO: реализовать LoggingUserServiceProxy(UserService):
# - __init__(service: UserService)
# - Каждый метод должен:
#   1. Логировать: "[LOG] Вызов {method}({args})"
#   2. Замерить время выполнения
#   3. Делегировать в реальный сервис
#   4. Логировать: "[LOG] {method} → {result} ({elapsed:.3f}с)"
#      или "[LOG] {method} → ОШИБКА: {exception} ({elapsed:.3f}с)" и пробросить исключение

# Клиентский код
# service = LoggingUserServiceProxy(RealUserService())
# user = service.get_by_id(1)
# all_users = service.get_all()
# new_user = service.create("Мария", "[email protected]")
# try:
#     service.deactivate(999)
# except ValueError:
#     pass
<?php
class User {
    public function __construct(
        public int    $id,
        public string $name,
        public string $email,
        public bool   $active = true,
    ) {}
}

interface UserService {
    public function getById(int $userId): ?User;
    public function getAll(): array;
    public function create(string $name, string $email): User;
    public function deactivate(int $userId): void;
}

class RealUserService implements UserService {
    private array $db;
    private int $nextId = 3;

    public function __construct() {
        $this->db = [
            1 => new User(1, 'Анна',  '[email protected]'),
            2 => new User(2, 'Иван',  '[email protected]'),
        ];
    }

    public function getById(int $userId): ?User {
        usleep(10000);
        return $this->db[$userId] ?? null;
    }

    public function getAll(): array { usleep(20000); return array_values($this->db); }

    public function create(string $name, string $email): User {
        usleep(10000);
        $user = new User($this->nextId, $name, $email);
        $this->db[$this->nextId++] = $user;
        return $user;
    }

    public function deactivate(int $userId): void {
        usleep(10000);
        if (!isset($this->db[$userId])) throw new InvalidArgumentException("Пользователь #{$userId} не найден");
        $this->db[$userId]->active = false;
    }
}

// TODO: реализовать LoggingUserServiceProxy implements UserService:
// - __construct(UserService $service)
// - Каждый метод:
//   1. Логирует: "[LOG] Вызов {method}({args})"
//   2. Замеряет время
//   3. Делегирует в $service
//   4. Логирует результат или исключение с временем выполнения

// Клиентский код
// $service = new LoggingUserServiceProxy(new RealUserService());
// $user = $service->getById(1);
// $all  = $service->getAll();
// $new  = $service->create('Мария', '[email protected]');
// try {
//     $service->deactivate(999);
// } catch (InvalidArgumentException $e) {}

Задача 4. Кэширующий прокси для API погоды

Реализуйте кэширующий прокси CachedWeatherProxy для интерфейса WeatherService. Прокси кэширует результаты запросов на заданное время (TTL). Повторный запрос для того же города в течение TTL возвращает кэшированные данные без обращения к реальному сервису. Добавьте метод get_cache_stats() для отслеживания попаданий и промахов.

import time
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class WeatherData:
    city: str
    temp_celsius: float
    humidity: int
    description: str

class WeatherService(ABC):
    @abstractmethod
    def get_current(self, city: str) -> WeatherData: pass

    @abstractmethod
    def get_forecast(self, city: str, days: int) -> list[WeatherData]: pass

class RealWeatherService(WeatherService):
    def __init__(self):
        self._call_count = 0

    def get_current(self, city: str) -> WeatherData:
        self._call_count += 1
        time.sleep(0.05)  # симуляция HTTP-запроса
        print(f"[API] Запрос погоды для '{city}' (вызов #{self._call_count})")
        return WeatherData(city=city, temp_celsius=20.5, humidity=65, description="Ясно")

    def get_forecast(self, city: str, days: int) -> list[WeatherData]:
        self._call_count += 1
        time.sleep(0.1)
        print(f"[API] Запрос прогноза для '{city}' на {days} дней (вызов #{self._call_count})")
        return [WeatherData(city, 18.0 + i, 60 + i, "Переменная облачность") for i in range(days)]

    @property
    def call_count(self) -> int:
        return self._call_count

# TODO: реализовать CachedWeatherProxy(WeatherService):
# - __init__(service: WeatherService, ttl: float = 60.0)
# - get_current(city) — кэшировать по ключу ("current", city)
# - get_forecast(city, days) — кэшировать по ключу ("forecast", city, days)
# - При cache hit: "[CACHE HIT] current:{city}"
# - При cache miss: делегировать, сохранить с меткой времени
# - get_cache_stats() -> dict  — {"hits": N, "misses": N, "cached_keys": N}
# - invalidate(city) -> None  — удалить все записи для города из кэша

# Ожидаемый клиентский код:
# real = RealWeatherService()
# proxy = CachedWeatherProxy(real, ttl=1.0)

# proxy.get_current("Москва")    # MISS — реальный запрос
# proxy.get_current("Москва")    # HIT  — из кэша
# proxy.get_current("СПб")       # MISS
# proxy.get_forecast("Москва", 3) # MISS
# proxy.get_forecast("Москва", 3) # HIT

# print(proxy.get_cache_stats())
# print(f"Реальных вызовов API: {real.call_count}")  # должно быть 3

# time.sleep(1.1)
# proxy.get_current("Москва")    # MISS — TTL истёк
<?php
class WeatherData {
    public function __construct(
        public string $city,
        public float  $tempCelsius,
        public int    $humidity,
        public string $description,
    ) {}
}

interface WeatherService {
    public function getCurrent(string $city): WeatherData;
    public function getForecast(string $city, int $days): array;
}

class RealWeatherService implements WeatherService {
    private int $callCount = 0;

    public function getCurrent(string $city): WeatherData {
        $this->callCount++;
        usleep(50000);
        echo "[API] Запрос погоды для '{$city}' (вызов #{$this->callCount})\n";
        return new WeatherData($city, 20.5, 65, 'Ясно');
    }

    public function getForecast(string $city, int $days): array {
        $this->callCount++;
        usleep(100000);
        echo "[API] Запрос прогноза для '{$city}' на {$days} дней (вызов #{$this->callCount})\n";
        return array_map(fn($i) => new WeatherData($city, 18.0 + $i, 60 + $i, 'Переменная облачность'), range(0, $days - 1));
    }

    public function getCallCount(): int { return $this->callCount; }
}

// TODO: реализовать CachedWeatherProxy implements WeatherService:
// - __construct(WeatherService $service, float $ttl = 60.0)
// - getCurrent($city) — кэшировать по ключу "current:{$city}"
// - getForecast($city, $days) — кэшировать по ключу "forecast:{$city}:{$days}"
// - При cache hit: "[CACHE HIT] {key}"
// - getCacheStats(): array — ['hits' => N, 'misses' => N, 'cached_keys' => N]
// - invalidate(string $city): void — удалить все записи для города

// Ожидаемый клиентский код:
// $real  = new RealWeatherService();
// $proxy = new CachedWeatherProxy($real, ttl: 1.0);

// $proxy->getCurrent('Москва');     // MISS
// $proxy->getCurrent('Москва');     // HIT
// $proxy->getCurrent('СПб');        // MISS
// $proxy->getForecast('Москва', 3); // MISS
// $proxy->getForecast('Москва', 3); // HIT

// print_r($proxy->getCacheStats());
// echo "Реальных вызовов: " . $real->getCallCount() . "\n"; // 3

// sleep(2);
// $proxy->getCurrent('Москва');     // MISS — TTL истёк

Задача 5. Умный прокси с подсчётом ссылок

Реализуйте умный прокси SmartDatabaseProxy для интерфейса Database. Прокси должен: (1) считать количество активных подключений; (2) откладывать закрытие реального соединения до тех пор, пока счётчик не достигнет нуля; (3) логировать все операции с временем выполнения; (4) ограничивать максимальное число одновременных подключений. При превышении лимита бросать исключение.

import time
import threading
from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self) -> None: pass

    @abstractmethod
    def disconnect(self) -> None: pass

    @abstractmethod
    def execute(self, query: str) -> list[dict]: pass

    @abstractmethod
    def is_connected(self) -> bool: pass

class RealDatabase(Database):
    def __init__(self, dsn: str):
        self._dsn = dsn
        self._connected = False

    def connect(self) -> None:
        time.sleep(0.05)
        self._connected = True
        print(f"[DB] Подключено к {self._dsn}")

    def disconnect(self) -> None:
        time.sleep(0.02)
        self._connected = False
        print(f"[DB] Отключено от {self._dsn}")

    def execute(self, query: str) -> list[dict]:
        if not self._connected:
            raise RuntimeError("Нет подключения к БД")
        time.sleep(0.01)
        print(f"[DB] Выполнен запрос: {query[:40]}")
        return [{"result": "ok"}]

    def is_connected(self) -> bool:
        return self._connected

# TODO: реализовать SmartDatabaseProxy(Database):
# - __init__(dsn: str, max_connections: int = 5)
#   создаёт RealDatabase, но НЕ подключается
# - connect()
#   1. Проверить, не превышен ли max_connections (иначе RuntimeError)
#   2. Если это первое подключение — вызвать real.connect()
#   3. Увеличить счётчик активных соединений
#   4. Логировать: "[Proxy] Открыто соединение #{N}, всего активных: {count}"
# - disconnect()
#   1. Уменьшить счётчик
#   2. Логировать: "[Proxy] Закрыто соединение, осталось активных: {count}"
#   3. Если счётчик == 0 — вызвать real.disconnect()
# - execute(query) — замерить время, делегировать, залогировать
# - property active_connections -> int
# - Должен быть потокобезопасным (threading.Lock)

# Ожидаемый клиентский код:
# proxy = SmartDatabaseProxy("postgresql://localhost/app", max_connections=3)

# proxy.connect()   # реальное подключение к БД
# proxy.connect()   # +1 к счётчику, без нового подключения
# proxy.connect()   # +1 к счётчику

# proxy.execute("SELECT * FROM users WHERE active = true")

# proxy.disconnect()  # счётчик: 2
# proxy.disconnect()  # счётчик: 1
# proxy.disconnect()  # счётчик: 0 → реальное отключение

# try:
#     for _ in range(4):
#         proxy.connect()
# except RuntimeError as e:
#     print(f"Лимит превышен: {e}")

```php linenums="1" <?php interface Database { public function connect(): void; public function disconnect(): void; public function execute(string $query): array; public function isConnected(): bool; }

class RealDatabase implements Database { private bool $connected = false;

public function __construct(private string $dsn) {}

public function connect(): void {
    usleep(50000);
    $this->connected = true;
    echo "[DB] Подключено к {$this->dsn}\n";
}

public function disconnect(): void {
    usleep(20000);
    $this->connected = false;
    echo "[DB] Отключено от {$this->dsn}\n";
}

public function execute(string $query): array {
    if (!$this->connected) throw new RuntimeException('Нет подключения к БД');
    usleep(10000);
    echo "[DB] Выполнен запрос: " . substr($query, 0, 40) . "\n";
    return [['result' => 'ok']];
}

public function isConnected(): bool { return $this->connected; }

}

// TODO: реализовать SmartDatabaseProxy implements Database: // - __construct(string $dsn, int \(maxConnections = 5) // создаёт RealDatabase, но НЕ подключается // - connect() // 1. Проверить лимит (иначе RuntimeException) // 2. Если первое подключение — вызвать real->connect() // 3. Увеличить счётчик, залогировать // - disconnect() // 1. Уменьшить счётчик, залогировать // 2. Если счётчик == 0 — вызвать real->disconnect() // - execute(\)query) — замерить время, делегировать, залогировать // - getActiveConnections(): int

// Ожидаемый клиентский код: // $proxy = new SmartDatabaseProxy('postgresql://localhost/app', maxConnections: 3);

// $proxy->connect(); // реальное подключение // $proxy->connect(); // +1 к счётчику // $proxy->connect(); // +1 к счётчику

// $proxy->execute('SELECT * FROM users WHERE active = true');

// $proxy->disconnect(); // счётчик: 2 // $proxy->disconnect(); // счётчик: 1 // $proxy->disconnect(); // счётчик: 0 → реальное отключение

// try { // for ($i = 0; $i < 4; $i++) $proxy->connect(); // } catch (RuntimeException $e) { // echo "Лимит превышен: " . $e->getMessage() . "\n"; // }