Перейти к содержимому
Фёдор Башинский

Заместитель

6 min read
Обложка статьи «Заместитель»

Заместитель (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(), не зная и не заботясь о том, заместитель перед ним или реальная фотография.

ЗаместительДекоратор
ЦельКонтролировать доступ к объекту (отложенная инициализация, права, кэш, удалённость)Добавить объекту новое поведение поверх уже имеющегося
Количество обёртокОбычно одна, зафиксированная заранееЛюбое, свободно комбинируются клиентом
Осведомлённость клиентаКлиент обычно не знает, что работает через заместителяКлиент сознательно собирает цепочку декораторов

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

  • Создание или инициализация объекта дорогая (файловый или сетевой ввод-вывод, тяжёлые вычисления), а сам объект нужен не всегда и не сразу — виртуальный заместитель откладывает эту работу до первого реального обращения.
  • Нужно проверять права доступа перед выполнением операции, не встраивая эту проверку в сам объект и не дублируя её в каждом месте вызова.
  • Нужно добавить кэширование результата или логирование обращений к объекту так, чтобы клиентский код не отличал заместителя от реального объекта и не менялся при добавлении этой логики.

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

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

Минус — лишний уровень косвенности на каждый вызов: вместо прямого обращения к объекту клиент всегда проходит через заместителя, даже когда его контроль не требуется. Если заместителей в цепочке становится несколько или логика внутри заместителя усложняется, разобраться, что на самом деле происходит при вызове метода, становится труднее, чем при прямом обращении к объекту.

Итог

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