Заместитель — это структурный паттерн проектирования, который позволяет подставлять вместо реальных объектов специальные объекты-заменители. Эти объекты перехватывают вызовы к оригинальному объекту, позволяя сделать что-то до или после передачи вызова оригиналу.
Проблема
Для чего вообще контролировать доступ к объектам? Рассмотрим такой пример: у вас есть внешний ресурсоёмкий объект, который нужен не все время, а изредка.
Запросы к базе данных могут быть очень медленными.
Мы могли бы создавать этот объект не в самом начале программы, а только тогда, когда он кому-то реально понадобится. Каждый клиент объекта получил бы некий код отложенной инициализации. Но, вероятно, это привело бы к множественному дублированию кода.
В идеале, этот код хотелось бы поместить прямо в служебный класс, но это не всегда возможно. Например, код класса может находиться в закрытой сторонней библиотеке.
Решение
Паттерн Заместитель предлагает создать новый класс-дублёр, имеющий тот же интерфейс, что и оригинальный служебный объект. При получении запроса от клиента объект-заместитель сам бы создавал экземпляр служебного объекта и переадресовывал бы ему всю реальную работу.
Заместитель «притворяется» базой данных, ускоряя работу за счёт ленивой инициализации и кеширования повторяющихся запросов.
Но в чём же здесь польза? Вы могли бы поместить в класс заместителя какую-то промежуточную логику, которая выполнялась бы до (или после) вызовов этих же методов в настоящем объекте. А благодаря одинаковому интерфейсу, объект-заместитель можно передать в любой код, ожидающий сервисный объект.
Аналогия из жизни
Платёжной картой можно расплачиваться, как и наличными.
Платёжная карточка — это заместитель пачки наличных. И карточка, и наличные имеют общий интерфейс — ими можно оплачивать товары. Для покупателя польза в том, что не надо таскать с собой тонны наличных, а владелец магазина рад, что ему не нужно делать дорогостоящую инкассацию наличности в банк — деньги поступают к нему на счёт напрямую.
Структура
Интерфейс сервиса определяет общий интерфейс для сервиса и заместителя. Благодаря этому, объект заместителя можно использовать там, где ожидается объект сервиса.
Сервис содержит полезную бизнес-логику.
Заместитель хранит ссылку на объект сервиса. После того как заместитель заканчивает свою работу (например, инициализацию, логирование, защиту или другое), он передаёт вызовы вложенному сервису. Заместитель может сам отвечать за создание и удаление объекта сервиса.
Клиент работает с объектами через интерфейс сервиса. Благодаря этому, его можно «одурачить», подменив объект сервиса объектом заместителя.
Псевдокод
В этом примере Заместитель помогает добавить в программу механизм ленивой инициализации и кеширования результатов работы библиотеки интеграции с 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()
Применимость
Ленивая инициализация (виртуальный прокси). Когда у вас есть тяжёлый объект, грузящий данные из файловой системы или базы данных.
Вместо того, чтобы грузить данные сразу после старта программы, можно сэкономить ресурсы и создать объект тогда, когда он действительно понадобится.
Защита доступа (защищающий прокси). Когда в программе есть разные типы пользователей, и вам хочется защищать объект от неавторизованного доступа. Например, если ваши объекты — это важная часть операционной системы, а пользователи — сторонние программы (хорошие или вредоносные).
Прокси может проверять доступ при каждом вызове и передавать выполнение служебному объекту, если доступ разрешён.
Локальный запуск сервиса (удалённый прокси). Когда настоящий сервисный объект находится на удалённом сервере.
В этом случае заместитель транслирует запросы клиента в вызовы по сети в протоколе, понятном удалённому сервису.
Логирование запросов (логирующий прокси). Когда требуется хранить историю обращений к сервисному объекту.
Заместитель может сохранять историю обращения клиента к сервисному объекту.
Кеширование объектов («умная» ссылка). Когда нужно кешировать результаты запросов клиентов и управлять их жизненным циклом.
Заместитель может подсчитывать количество ссылок на сервисный объект, которые были отданы клиенту и остаются активными. Когда все ссылки освобождаются, можно будет освободить и сам сервисный объект (например, закрыть подключение к базе данных).
Кроме того, Заместитель может отслеживать, не менял ли клиент сервисный объект. Это позволит использовать объекты повторно и здóрово экономить ресурсы, особенно если речь идёт о больших прожорливых сервисах.
Шаги реализации
Определите интерфейс, который бы сделал заместитель и оригинальный объект взаимозаменяемыми.
Создайте класс заместителя. Он должен содержать ссылку на сервисный объект. Чаще всего, сервисный объект создаётся самим заместителем. В редких случаях заместитель получает готовый сервисный объект от клиента через конструктор.
Реализуйте методы заместителя в зависимости от его предназначения. В большинстве случаев, проделав какую-то полезную работу, методы заместителя должны передать запрос сервисному объекту.
Подумайте о введении фабрики, которая решала бы, какой из объектов создавать — заместитель или реальный сервисный объект. Но, с другой стороны, эта логика может быть помещена в создающий метод самого заместителя.
Подумайте, не реализовать ли вам ленивую инициализацию сервисного объекта при первом обращении клиента к методам заместителя.
Преимущества и недостатки
Преимущества
Позволяет контролировать сервисный объект незаметно для клиента.
Может работать, даже если сервисный объект ещё не создан.
Может контролировать жизненный цикл служебного объекта.
Недостатки
Усложняет код программы из-за введения дополнительных классов.
Увеличивает время отклика от сервиса.
Примеры реализации паттерна
Python
Сложность: 2/3
Популярность: 1/3
Применимость: Паттерн Заместитель применяется в Python коде тогда, когда надо заменить настоящий объект его суррогатом, причём незаметно для клиентов настоящего объекта. Это позволит выполнить какие-то добавочные поведения до или после основного поведения настоящего объекта.
Признаки применения паттерна: Класс заместителя чаще всего делегирует всю настоящую работу своему реальному объекту. Заместители часто сами следят за жизненным циклом своего реального объекта.
Этот пример показывает структуру паттерна Заместитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
fromabcimportABC,abstractmethodclassSubject(ABC):""" Интерфейс Субъекта объявляет общие операции как для Реального Субъекта, так и для Заместителя. Пока клиент работает с Реальным Субъектом, используя этот интерфейс, вы сможете передать ему заместителя вместо реального субъекта. """@abstractmethoddefrequest(self)->None:passclassRealSubject(Subject):""" Реальный Субъект содержит некоторую базовую бизнес-логику. Как правило, Реальные Субъекты способны выполнять некоторую полезную работу, которая к тому же может быть очень медленной или точной – например, коррекция входных данных. Заместитель может решить эти задачи без каких-либо изменений в коде Реального Субъекта. """defrequest(self)->None:print("RealSubject: Handling request.")classProxy(Subject):""" Интерфейс Заместителя идентичен интерфейсу Реального Субъекта. """def__init__(self,real_subject:RealSubject)->None:self._real_subject=real_subjectdefrequest(self)->None:""" Наиболее распространёнными областями применения паттерна Заместитель являются ленивая загрузка, кэширование, контроль доступа, ведение журнала и т.д. Заместитель может выполнить одну из этих задач, а затем, в зависимости от результата, передать выполнение одноимённому методу в связанном объекте класса Реального Субъекта. """ifself.check_access():self._real_subject.request()self.log_access()defcheck_access(self)->bool:print("Proxy: Checking access prior to firing a real "+"request.")returnTruedeflog_access(self)->None:print("Proxy: Logging the time of request.",end="")defclient_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)
Применимость: Паттерн Заместитель применяется в PHP коде тогда, когда надо заменить настоящий объект его суррогатом, причём незаметно для клиентов настоящего объекта. Это позволит выполнить какие-то добавочные поведения до или после основного поведения настоящего объекта.
Признаки применения паттерна: Класс заместителя чаще всего делегирует всю настоящую работу своему реальному объекту. Заместители часто сами следят за жизненным циклом своего реального объекта.
Этот пример показывает структуру паттерна Заместитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
<?php/*** Интерфейс Субъекта объявляет общие операции как для Реального Субъекта, так и* для Заместителя. Пока клиент работает с Реальным Субъектом, используя этот* интерфейс, вы сможете передать ему заместителя вместо реального субъекта.*/interfaceSubject{publicfunctionrequest():void;}/*** Реальный Субъект содержит некоторую базовую бизнес-логику. Как правило,* Реальные Субъекты способны выполнять некоторую полезную работу, которая к* тому же может быть очень медленной или точной – например, коррекция входных* данных. Заместитель может решить эти задачи без каких-либо изменений в коде* Реального Субъекта.*/classRealSubjectimplementsSubject{publicfunctionrequest():void{echo"RealSubject: Handling request.\n";}}/*** Интерфейс Заместителя идентичен интерфейсу Реального Субъекта.*/classProxyimplementsSubject{/** * @var RealSubject */private$realSubject;/** * Заместитель хранит ссылку на объект класса РеальныйСубъект. Клиент может * либо лениво загрузить его, либо передать Заместителю. */publicfunction__construct(RealSubject$realSubject){$this->realSubject=$realSubject;}/** * Наиболее распространёнными областями применения паттерна Заместитель * являются ленивая загрузка, кэширование, контроль доступа, ведение журнала * и т.д. Заместитель может выполнить одну из этих задач, а затем, в * зависимости от результата, передать выполнение одноимённому методу в * связанном объекте класса Реального Субъект. */publicfunctionrequest():void{if($this->checkAccess()){$this->realSubject->request();$this->logAccess();}}privatefunctioncheckAccess():bool{// Некоторые реальные проверки должны проходить здесь.echo"Proxy: Checking access prior to firing a real request.\n";returntrue;}privatefunctionlogAccess():void{echo"Proxy: Logging the time of request.\n";}}/*** Клиентский код должен работать со всеми объектами (как с реальными, так и* заместителями) через интерфейс Субъекта, чтобы поддерживать как реальные* субъекты, так и заместителей. В реальной жизни, однако, клиенты в основном* работают с реальными субъектами напрямую. В этом случае, для более простой* реализации паттерна, можно расширить заместителя из класса реального* субъекта.*/functionclientCode(Subject$subject){// ...$subject->request();// ...}echo"Client: Executing the client code with a real subject:\n";$realSubject=newRealSubject();clientCode($realSubject);echo"\n";echo"Client: Executing the same client code with a proxy:\n";$proxy=newProxy($realSubject);clientCode($proxy);
<?php/*** Интерфейс Субъекта описывает интерфейс реального объекта.** Дело в том, что у большинства приложений нет чётко определённого интерфейса.* В этом случае лучше было бы расширить Заместителя за счёт существующего* класса приложения. Если это неудобно, тогда первым шагом должно быть* извлечение правильного интерфейса.*/interfaceDownloader{publicfunctiondownload(string$url):string;}/*** Реальный Субъект делает реальную работу, хотя и не самым эффективным* способом. Когда клиент пытается загрузить тот же самый файл во второй раз,* наш загрузчик именно это и делает, вместо того, чтобы извлечь результат из* кэша.*/classSimpleDownloaderimplementsDownloader{publicfunctiondownload(string$url):string{echo"Downloading a file from the Internet.\n";$result=file_get_contents($url);echo"Downloaded bytes: ".strlen($result)."\n";return$result;}}/*** Класс Заместителя – это попытка сделать загрузку более эффективной. Он* обёртывает реальный объект загрузчика и делегирует ему первые запросы на* скачивание. Затем результат кэшируется, что позволяет последующим вызовам* возвращать уже имеющийся файл вместо его повторной загрузки.*/classCachingDownloaderimplementsDownloader{/** * @var SimpleDownloader */private$downloader;/** * @var string[] */private$cache=[];publicfunction__construct(SimpleDownloader$downloader){$this->downloader=$downloader;}publicfunctiondownload(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];}}/*** Клиентский код может выдать несколько похожих запросов на загрузку. В этом* случае кэширующий заместитель экономит время и трафик, подавая результаты из* кэша.** Клиент не знает, что он работает с заместителем, потому что он работает с* загрузчиками через абстрактный интерфейс.*/functionclientCode(Downloader$subject){// ...$result=$subject->download("http://example.com/");// Повторяющиеся запросы на загрузку могут кэшироваться для увеличения// скорости.$result=$subject->download("http://example.com/");// ...}echo"Executing client code with real subject:\n";$realSubject=newSimpleDownloader();clientCode($realSubject);echo"\n";echo"Executing the same client code with a proxy:\n";$proxy=newCachingDownloader($realSubject);clientCode($proxy);
Реализуйте защитный проксиSecureFileProxy для интерфейса FileService. Прокси проверяет права пользователя перед каждой операцией: read — требует роль reader или admin, write — только admin. При отсутствии прав бросать исключение. Клиентский код работает с интерфейсом и не знает о проверках.
<?phpclassUser{publicfunction__construct(publicstring$name,publicstring$role,// "reader", "writer", "admin"){}}interfaceFileService{publicfunctionread(string$filename):string;publicfunctionwrite(string$filename,string$content):void;publicfunctiondelete(string$filename):void;}classRealFileServiceimplementsFileService{privatearray$storage=[];publicfunctionread(string$filename):string{$content=$this->storage[$filename]??'';echo"[File] Читаю '{$filename}': ".substr($content,0,30)."\n";return$content;}publicfunctionwrite(string$filename,string$content):void{$this->storage[$filename]=$content;echo"[File] Записываю '{$filename}' (".strlen($content)." байт)\n";}publicfunctiondelete(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 изображений реальная загрузка происходит только при обращении.
fromabcimportABC,abstractmethodclassImage(ABC):@abstractmethoddefdisplay(self)->None:pass@abstractmethoddefget_info(self)->dict:passclassHighResImage(Image):def__init__(self,filepath:str):print(f"[Image] Загрузка из файла: {filepath}")# дорогая операцияself._filepath=filepathself._data=f"pixel_data_of_{filepath}"# симуляция загрузкиself._width=3840self._height=2160defdisplay(self)->None:print(f"[Image] Отрисовка {self._filepath} ({self._width}x{self._height})")defget_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)}")
<?phpinterfaceImage{publicfunctiondisplay():void;publicfunctiongetInfo():array;}classHighResImageimplementsImage{privatestring$data;privateint$width=3840;privateint$height=2160;publicfunction__construct(privatestring$filepath){echo"[Image] Загрузка из файла: {$filepath}\n";$this->data="pixel_data_of_{$filepath}";}publicfunctiondisplay():void{echo"[Image] Отрисовка {$this->filepath} ({$this->width}x{$this->height})\n";}publicfunctiongetInfo():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. Клиентский код не должен меняться.
<?phpclassUser{publicfunction__construct(publicint$id,publicstring$name,publicstring$email,publicbool$active=true,){}}interfaceUserService{publicfunctiongetById(int$userId):?User;publicfunctiongetAll():array;publicfunctioncreate(string$name,string$email):User;publicfunctiondeactivate(int$userId):void;}classRealUserServiceimplementsUserService{privatearray$db;privateint$nextId=3;publicfunction__construct(){$this->db=[1=>newUser(1,'Анна','anna@example.com'),2=>newUser(2,'Иван','ivan@example.com'),];}publicfunctiongetById(int$userId):?User{usleep(10000);return$this->db[$userId]??null;}publicfunctiongetAll():array{usleep(20000);returnarray_values($this->db);}publicfunctioncreate(string$name,string$email):User{usleep(10000);$user=newUser($this->nextId,$name,$email);$this->db[$this->nextId++]=$user;return$user;}publicfunctiondeactivate(int$userId):void{usleep(10000);if(!isset($this->db[$userId]))thrownewInvalidArgumentException("Пользователь #{$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('Мария', 'maria@example.com');// try {// $service->deactivate(999);// } catch (InvalidArgumentException $e) {}
Задача 4. Кэширующий прокси для API погоды
Реализуйте кэширующий проксиCachedWeatherProxy для интерфейса WeatherService. Прокси кэширует результаты запросов на заданное время (TTL). Повторный запрос для того же города в течение TTL возвращает кэшированные данные без обращения к реальному сервису. Добавьте метод get_cache_stats() для отслеживания попаданий и промахов.
<?phpclassWeatherData{publicfunction__construct(publicstring$city,publicfloat$tempCelsius,publicint$humidity,publicstring$description,){}}interfaceWeatherService{publicfunctiongetCurrent(string$city):WeatherData;publicfunctiongetForecast(string$city,int$days):array;}classRealWeatherServiceimplementsWeatherService{privateint$callCount=0;publicfunctiongetCurrent(string$city):WeatherData{$this->callCount++;usleep(50000);echo"[API] Запрос погоды для '{$city}' (вызов #{$this->callCount})\n";returnnewWeatherData($city,20.5,65,'Ясно');}publicfunctiongetForecast(string$city,int$days):array{$this->callCount++;usleep(100000);echo"[API] Запрос прогноза для '{$city}' на {$days} дней (вызов #{$this->callCount})\n";returnarray_map(fn($i)=>newWeatherData($city,18.0+$i,60+$i,'Переменная облачность'),range(0,$days-1));}publicfunctiongetCallCount():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) ограничивать максимальное число одновременных подключений. При превышении лимита бросать исключение.
importtimeimportthreadingfromabcimportABC,abstractmethodclassDatabase(ABC):@abstractmethoddefconnect(self)->None:pass@abstractmethoddefdisconnect(self)->None:pass@abstractmethoddefexecute(self,query:str)->list[dict]:pass@abstractmethoddefis_connected(self)->bool:passclassRealDatabase(Database):def__init__(self,dsn:str):self._dsn=dsnself._connected=Falsedefconnect(self)->None:time.sleep(0.05)self._connected=Trueprint(f"[DB] Подключено к {self._dsn}")defdisconnect(self)->None:time.sleep(0.02)self._connected=Falseprint(f"[DB] Отключено от {self._dsn}")defexecute(self,query:str)->list[dict]:ifnotself._connected:raiseRuntimeError("Нет подключения к БД")time.sleep(0.01)print(f"[DB] Выполнен запрос: {query[:40]}")return[{"result":"ok"}]defis_connected(self)->bool:returnself._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