handbookНаписать

patterns/structural/proxy/README.md · тема · 4 языка · 19 тестов

+ практика: кэш репозитория товаров

Заместитель (Proxy) ​

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

Проблема ​

Архив документов DocumentArchive умеет читать и удалять. Читать можно всем, удалять — только администраторам. Если проверку прав вписать в сам архив, ему придётся знать о пользователях, сессиях и ролях, хотя его дело — хранить. Если проверять в каждом месте вызова, одно место обязательно забудут, и удаление пройдёт без проверки.

Клиентский код при этом не хочет знать, с чем он работает: ему нужен объект с методами read и delete.

Решение ​

Ввести общий интерфейс Documents и поставить перед настоящим архивом заместителя GuardedDocuments. Заместитель реализует тот же интерфейс, держит ссылку на архив и сам решает, передать ли вызов: read уходит дальше сразу, delete — только если пользователь администратор, иначе AccessDenied. Клиент получает заместителя вместо архива и не замечает подмены.

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

Структура ​

forwards to

checks

«interface»

Documents

+read(id) : string

+delete(id)

DocumentArchive

-documents: map

+read(id) : string

+delete(id)

GuardedDocuments

-archive: Documents

-user: User

+read(id) : string

+delete(id)

User

+name: string

+isAdmin: bool

Интерфейс — единственное, что знает клиент. Рядом с ним лежат User и ошибка AccessDenied: они часть контракта, потому что клиент должен уметь отличить «нельзя» от «нет такого документа». Настоящий объект про пользователей не знает вовсе: его можно тестировать и использовать без всякой защиты, например в миграциях.

Как это собирается ​

Архив в приложении один, а заместитель создаётся на каждый запрос. В примере обработчик сначала находит пользователя по сессии: нет пользователя - 401, до документов дело не доходит. Потом строит GuardedDocuments вокруг общего DocumentArchive с этим пользователем и отдаёт его функции handle, которая знает только интерфейс Documents. В фреймворке это место занимает middleware или контейнер с областью жизни "запрос".

handle переводит исход в статус ответа: AccessDenied от заместителя - 403, "нет документа" от самого архива - 404, ошибки архива проходят сквозь заместителя без изменений. Видно и то, что проверка не мешает чтению: ann не смогла удалить договор, но прочитала его, а после удаления админом получила 404.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

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

ЯзыкОсобенность
PHPinterface + final readonly заместитель; отказ — собственное исключение AccessDenied
GoЗаместитель встраивает интерфейс: Read пробрасывается сам, переопределён только Delete; отказ — ErrAccessDenied, обёрнутый через %w
PythonProtocol вместо базового класса: заместитель совместим структурно; AccessDenied наследует PermissionError
TypeScriptimplements Documents и параметры-свойства конструктора; User — просто интерфейс с readonly полями

Запуск: make test T=patterns/structural/proxy

Плюсы и минусы ​

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

  • ✅ Правило доступа живёт в одном месте, настоящий объект о нём не знает.
  • ✅ Клиент не меняется: заместитель подставляется там, где собирают объекты.
  • ✅ Заместители складываются в цепочку и тестируются по одному.
  • ❌ Лишний уровень вызова и лишний класс на каждый интерфейс.
  • ❌ Защита работает, только пока никто не держит ссылку на настоящий объект: вызов мимо заместителя её обходит.
  • ❌ Каждый новый метод интерфейса надо не забыть пробросить или защитить.

Когда не применять ​

  • Проверка нужна в одном месте вызова: обычный if там понятнее отдельного класса.
  • Правила доступа сложные и зависят от данных (владелец документа, отдел, время): это политика доступа, которую вызывают из сервиса или заместителя, а не логика внутри заместителя.
  • Нужно добавить объекту новое поведение, а не контролировать доступ к старому: это Декоратор. Устроены они одинаково, различаются намерением.
  • Во фреймворке уже есть механизм: middleware, атрибуты авторизации, ленивые сервисы контейнера.

Отличия между языками ​

  • PHP — заместитель пишется руками: каждый метод интерфейса проброшен явно. Ленивые заместители умеют генерировать контейнеры (Symfony, Doctrine), а с PHP 8.4 есть встроенные ленивые объекты (ReflectionClass::newLazyProxy).
  • Go — встраивание интерфейса в структуру пробрасывает все методы автоматически, и переопределить достаточно защищаемые. Цена удобства: новый метод в Documents пройдёт мимо проверки молча, компилятор не напомнит.
  • Python — утиная типизация делает заместителем любой объект с нужными методами. Соблазн написать универсальный __getattr__, который пробросит всё, есть, но тогда проверка типов не видит методов, а защищаемый метод легко пропустить.
  • TypeScript — встроенный Proxy из JavaScript перехватывает любой вызов, но теряет типизацию перехватчиков и усложняет отладку. Для заместителя с известным интерфейсом класс с implements проще и строже.

Практика ​

Учебный заместитель только пропускает или не пропускает вызов. В проекте заместитель чаще хранит состояние: кэш перед медленным репозиторием, и с состоянием приходят вопросы, которых тут нет — когда данные устаревают, кто их сбрасывает и что будет, если писать мимо заместителя. Практика: кэш репозитория товаров показывает кэширующего заместителя с TTL на подменяемых часах, сброс при записи и честную границу приёма.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS