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

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

Категория: структурный паттерн.

Проблема

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

Встраивать эту логику прямо в реальный класс - значит смешивать его основную ответственность с побочными задачами (кеширование, безопасность, логирование).

Решение

Создаётся класс-заместитель (Proxy), который: 1. Реализует тот же интерфейс, что и реальный объект (RealSubject). 2. Хранит ссылку на реальный объект (создавая его сразу, лениво или обращаясь к нему удалённо). 3. Перед или вместо вызова реального объекта выполняет дополнительную логику: проверку, кеширование, логирование, отложенное создание.

Клиентский код работает с прокси через общий интерфейс и не обязан знать, что перед ним не реальный объект, а его заместитель.

Виды Proxy

  • Virtual Proxy - откладывает создание "дорогого" объекта до первого реального обращения (ленивая инициализация).
  • Remote Proxy - представляет объект, который физически находится в другом процессе или на другой машине, скрывая детали сетевого вызова.
  • Protection Proxy - проверяет права доступа перед тем, как разрешить вызов реального объекта.
  • Caching Proxy - кеширует результаты вызовов к реальному объекту, чтобы не выполнять их повторно.
  • Logging/Smart Reference Proxy - добавляет учёт вызовов, подсчёт ссылок, логирование.

Структура

  • Subject - общий интерфейс RealSubject и Proxy.
  • RealSubject - объект, доступ к которому контролируется.
  • Proxy - хранит ссылку на RealSubject, реализует Subject, добавляет свою логику вокруг вызовов.

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

  • Создание реального объекта дорогое, а нужен он не всегда - есть смысл отложить создание до первого использования.
  • Нужно добавить контроль доступа, кеширование или логирование к объекту, не изменяя его код.
  • Реальный объект находится в другом процессе, домене или на другом сервере.

Плюсы

  • Позволяет управлять жизненным циклом и доступом к объекту, не трогая ни его код, ни код клиента (оба продолжают работать с общим интерфейсом).
  • Ленивая инициализация через Virtual Proxy экономит ресурсы, если объект нужен не при каждом запуске программы.

Минусы

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

Отличие от Decorator

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

Пример в .NET Framework / BCL

  • Lazy loading навигационных свойств в Entity Framework - EF Core на лету генерирует прокси-класс поверх сущности, который подгружает связанные данные при первом обращении к свойству-навигации.
  • System.Runtime.Remoting.Proxies.RealProxy (legacy .NET Remoting) - явная реализация Remote Proxy.
  • DispatchProxy в System.Reflection - базовый класс для построения собственных динамических прокси (часто используется в AOP-библиотеках и мокинг-фреймворках).
  • Lazy<T> можно рассматривать как частный случай идеи Virtual Proxy применительно к одному значению.

Пример реализации на C#

Proxy.cs
using System;
using System.Collections.Generic;

namespace DesignPatterns.Structural.Proxy
{
    // Общий интерфейс реального объекта и прокси - клиент не различает их
    public interface IImage
    {
        void Display();
    }

    // Реальный объект - "дорогой" в создании (например, загрузка большого файла с диска)
    public sealed class HighResolutionImage : IImage
    {
        private readonly string _path;

        public HighResolutionImage(string path)
        {
            _path = path;
            LoadFromDisk();
        }

        private void LoadFromDisk() =>
            Console.WriteLine($"Загружаем тяжёлое изображение {_path} с диска...");

        public void Display() =>
            Console.WriteLine($"Отображаем {_path}");
    }

    // Proxy откладывает создание реального объекта до первого реального обращения (lazy loading)
    public sealed class LazyImageProxy : IImage
    {
        private readonly string _path;
        private HighResolutionImage? _realImage;

        public LazyImageProxy(string path)
        {
            _path = path;
        }

        public void Display()
        {
            // Реальный объект создаётся только тогда, когда он действительно нужен
            _realImage ??= new HighResolutionImage(_path);
            _realImage.Display();
        }
    }

    // Ещё один вид Proxy - кеширующий, добавляет проверку кеша перед реальным вызовом
    public sealed class CachingImageProxy : IImage
    {
        private static readonly Dictionary<string, IImage> Cache = new();
        private readonly string _path;

        public CachingImageProxy(string path)
        {
            _path = path;
        }

        public void Display()
        {
            if (!Cache.TryGetValue(_path, out var image))
            {
                image = new HighResolutionImage(_path);
                Cache[_path] = image;
            }
            else
            {
                Console.WriteLine($"Берём {_path} из кеша, повторная загрузка не нужна");
            }

            image.Display();
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            Console.WriteLine("-- Lazy proxy --");
            IImage lazy = new LazyImageProxy("photo.png");
            Console.WriteLine("Прокси создан, изображение ещё не загружено");
            lazy.Display(); // Загрузка произойдёт только сейчас

            Console.WriteLine("-- Caching proxy --");
            IImage cached1 = new CachingImageProxy("banner.png");
            IImage cached2 = new CachingImageProxy("banner.png");
            cached1.Display(); // Реальная загрузка
            cached2.Display(); // Из кеша
        }
    }
}

Открыть Proxy.cs отдельно Скачать Proxy.cs