Заместитель

Заместитель (Proxy) — это структурный паттерн проектирования, который предоставляет объект-подменыш с тем же интерфейсом, что и у реального объекта, и контролирует доступ к нему: откладывает дорогую инициализацию, проверяет права или добавляет кэширование, оставаясь для клиента неотличимым от настоящего объекта.
Проблема
Отчёт о диагностике автомобиля содержит фотографии повреждений кузова — файлы по несколько десятков мегабайт каждый. Фотография представлена интерфейсом:
interface DamagePhoto
{
public function render(): string; // бинарные данные изображения
}Реальная реализация читает файл с диска прямо в конструкторе:
class RealDamagePhoto implements DamagePhoto
{
private string $pixelData;
public function __construct(private readonly string $filePath)
{
$this->pixelData = file_get_contents($filePath); // тяжёлая операция ввода-вывода
}
public function render(): string
{
return $this->pixelData;
}
}Отчёт хранит список таких фотографий:
class DiagnosticReport
{
/** @var DamagePhoto[] */
private array $photos = [];
public function __construct(array $filePaths)
{
foreach ($filePaths as $path) {
$this->photos[] = new RealDamagePhoto($path);
}
}
}Проблема проявляется на экране со списком отчётов: чтобы показать список из пятисот отчётов с их номерами и датами, конструктор DiagnosticReport каждый раз создаёт RealDamagePhoto для всех прикреплённых фотографий — а значит, читает с диска все файлы, хотя список отчётов не показывает ни одной фотографии и открывать большинство из этих пятисот отчётов пользователь вообще не будет. Дорогая операция выполняется заранее и почти всегда впустую.
Решение
Заместитель реализует тот же интерфейс, что и реальный объект, но сам не выполняет тяжёлую работу — он либо откладывает её, либо добавляет вокруг нее дополнительную проверку, делегируя настоящему объекту только тогда, когда это действительно нужно.
Для отложенной инициализации создаём заместителя, который хранит только путь к файлу и создаёт RealDamagePhoto не раньше первого реального обращения:
class DamagePhotoProxy implements DamagePhoto
{
private ?RealDamagePhoto $realPhoto = null;
public function __construct(private readonly string $filePath) {}
public function render(): string
{
return ($this->realPhoto ??= new RealDamagePhoto($this->filePath))->render();
}
}DiagnosticReport меняется одной строкой — вместо реального объекта создаётся заместитель:
class DiagnosticReport
{
/** @var DamagePhoto[] */
private array $photos = [];
public function __construct(array $filePaths)
{
foreach ($filePaths as $path) {
$this->photos[] = new DamagePhotoProxy($path);
}
}
}Теперь построение списка из пятисот отчётов не читает с диска ни одного файла — создаются только лёгкие объекты DamagePhotoProxy с путями. Файл прочитается и попадёт в память только в тот момент, когда кто-то реально вызовет render() — например, когда пользователь откроет конкретный отчёт и попросит показать конкретную фотографию:
$report = new DiagnosticReport(['photo1.jpg', 'photo2.jpg']);
// ...отчёт лежит в списке, файлы ещё не читались...
$firstPhotoBytes = $report->photos[0]->render(); // только теперь читается photo1.jpgИз чего состоит паттерн
- Субъект (
DamagePhoto) — общий интерфейс, через который клиент работает и с реальным объектом, и с заместителем. - Реальный субъект (
RealDamagePhoto) — объект, выполняющий настоящую работу, доступ к которому контролируется. - Заместитель (
DamagePhotoProxy) — реализует тот же интерфейс, хранит ссылку на реальный субъект (или данные для его создания) и контролирует обращение к нему. - Клиент (
DiagnosticReport) — работает через интерфейс субъекта и не различает, обращается он к реальному объекту напрямую или через заместителя.
Заместитель контроля доступа
Отложенная инициализация — не единственная задача заместителя. Тем же способом можно проверять права перед выполнением операции. Допустим, списание запчасти со склада должно быть доступно только сотрудникам с ролью механика:
interface PartsWarehouse
{
public function writeOff(string $article, int $quantity): void;
}
final class PartsWarehouseConnection implements PartsWarehouse
{
public function writeOff(string $article, int $quantity): void
{
// реальное списание со склада
}
}
final class PartsWarehouseAccessProxy implements PartsWarehouse
{
public function __construct(
private readonly PartsWarehouse $warehouse,
private readonly Employee $currentUser,
) {}
public function writeOff(string $article, int $quantity): void
{
if (!$this->currentUser->hasRole('mechanic')) {
throw new \RuntimeException('Недостаточно прав для списания со склада');
}
$this->warehouse->writeOff($article, $quantity);
}
}Структура кода та же, что и у DamagePhotoProxy: реализация интерфейса, ссылка на реальный объект, дополнительная логика вокруг делегирования. Меняется только цель — не отложить работу, а не пустить к ней тех, кому нельзя. Так же этим способом добавляют кэширование результата дорогого вызова или логирование каждого обращения к объекту, оставляя интерфейс и клиентский код нетронутыми.
Заместитель vs Декоратор
Оба паттерна оборачивают объект в другой объект с тем же интерфейсом, поэтому структурно код выглядит почти одинаково — но у них разное назначение. Декоратор добавляет объекту новое поведение поверх уже имеющегося, и клиент сознательно решает, в какие декораторы и в каком порядке обернуть объект — например, обернуть логирование в кэширование или наоборот, комбинируя их в любом количестве. Заместитель же контролирует доступ к объекту — откладывает его создание, проверяет права, кэширует результат или прячет за собой удалённый вызов, — и обычно существует в единственном экземпляре, о котором клиент даже не подозревает: DiagnosticReport в примере выше просто вызывает render(), не зная и не заботясь о том, заместитель перед ним или реальная фотография.
| Заместитель | Декоратор | |
|---|---|---|
| Цель | Контролировать доступ к объекту (отложенная инициализация, права, кэш, удалённость) | Добавить объекту новое поведение поверх уже имеющегося |
| Количество обёрток | Обычно одна, зафиксированная заранее | Любое, свободно комбинируются клиентом |
| Осведомлённость клиента | Клиент обычно не знает, что работает через заместителя | Клиент сознательно собирает цепочку декораторов |
Когда применять
- Создание или инициализация объекта дорогая (файловый или сетевой ввод-вывод, тяжёлые вычисления), а сам объект нужен не всегда и не сразу — виртуальный заместитель откладывает эту работу до первого реального обращения.
- Нужно проверять права доступа перед выполнением операции, не встраивая эту проверку в сам объект и не дублируя её в каждом месте вызова.
- Нужно добавить кэширование результата или логирование обращений к объекту так, чтобы клиентский код не отличал заместителя от реального объекта и не менялся при добавлении этой логики.
Плюсы и минусы
Заместитель контролирует доступ к объекту, не меняя ни сам объект, ни клиентский код, который продолжает работать с тем же интерфейсом. Отложенная инициализация экономит ресурсы там, где объект чаще всего не нужен, а вынесенная в заместителя проверка прав или кэш не дублируется по всем местам вызова.
Минус — лишний уровень косвенности на каждый вызов: вместо прямого обращения к объекту клиент всегда проходит через заместителя, даже когда его контроль не требуется. Если заместителей в цепочке становится несколько или логика внутри заместителя усложняется, разобраться, что на самом деле происходит при вызове метода, становится труднее, чем при прямом обращении к объекту.
Итог
Заместитель нужен, когда к объекту требуется контролируемый доступ — отложить его создание, проверить права или закэшировать результат, — а клиентский код при этом не должен ни знать об этом контроле, ни меняться. В отличие от Декоратора, который добавляет объекту новые возможности, Заместитель ничего не добавляет к самой функциональности — он лишь решает, когда и на каких условиях к ней можно обратиться.