Введение
Иногда нужен объект, который выглядит и используется точно так же, как реальный, целевой объект, но при этом контролирует доступ к этому целевому объекту — откладывая его реальное создание до момента действительной необходимости (lazy loading), ограничивая доступ согласно определённым правам, или добавляя дополнительную логику логирования/кеширования прозрачно для вызывающего кода. Proxy решает именно эту задачу, разбираемую в этой статье.
Концепция
Proxy реализует тот же интерфейс, что и реальный, оборачиваемый объект (Real Subject), и клиентский код взаимодействует с Proxy так, будто это и есть реальный объект, не подозревая о наличии промежуточного слоя контроля — внутри Proxy решает, как именно обработать каждый вызов: создать реальный объект при первом реальном обращении (lazy/virtual proxy), проверить права доступа перед делегированием вызова (protection proxy), или выполнить дополнительное логирование/кеширование вокруг делегированного вызова к реальному объекту. Это концептуально похоже на Decorator , но с принципиально иным НАЗНАЧЕНИЕМ — Decorator добавляет дополнительное, видимое для клиента поведение, тогда как Proxy контролирует ДОСТУП к объекту, обычно прозрачно для клиента, который часто даже не должен явно знать о наличии прокси-слоя.
Пример кода
#include <iostream>
#include <memory>
class Image
{
public:
virtual void display() const = 0;
virtual ~Image() = default;
};
class HighResolutionImage : public Image // "тяжёлый", дорогой в создании реальный объект
{
public:
explicit HighResolutionImage(const std::string &filename) : m_filename(filename)
{
std::cout << "Загрузка тяжёлого изображения " << filename << "..." << std::endl; // ДОРОГАЯ операция
}
void display() const override { std::cout << "Отображение " << m_filename << std::endl; }
private:
std::string m_filename;
};
// Virtual Proxy — ОТКЛАДЫВАЕТ дорогую загрузку до реальной первой необходимости
class LazyImageProxy : public Image
{
public:
explicit LazyImageProxy(const std::string &filename) : m_filename(filename) {}
void display() const override
{
if (!m_realImage) { // создание РЕАЛЬНОГО объекта только при первом ДЕЙСТВИТЕЛЬНОМ обращении
m_realImage = std::make_unique<HighResolutionImage>(m_filename);
}
m_realImage->display();
}
private:
std::string m_filename;
mutable std::unique_ptr<HighResolutionImage> m_realImage; // mutable
};
int main()
{
std::cout << "Создание прокси (изображение НЕ загружено)..." << std::endl;
LazyImageProxy proxy("photo.jpg"); // СОЗДАНИЕ proxy — БЕЗ дорогой загрузки изображения
std::cout << "Прокси создан, изображение пока не используется" << std::endl;
// ... много другого кода, который может НЕ дойти до реального отображения вовсе ...
proxy.display(); // ИМЕННО ЗДЕСЬ происходит реальная, дорогая загрузка — отложенная до этого момента
return 0;
}
Пояснения к коду
LazyImageProxy::display(), проверяющий if (!m_realImage) перед фактическим созданием HighResolutionImage, показывает суть virtual proxy — дорогая операция (загрузка изображения) откладывается до момента, когда она РЕАЛЬНО необходима, а не выполняется сразу при создании самого proxy-объекта, что особенно ценно для случаев, где реальное использование объекта может оказаться вовсе не нужным (программа могла бы завершиться или пойти по другому пути выполнения до фактического вызова display()). Вывод программы показывает явное, наблюдаемое разделение момента создания LazyImageProxy (быстрое, без дорогой загрузки) и момента, когда реальная, дорогая загрузка действительно происходит (только при первом вызове display()), что наглядно демонстрирует ценность паттерна для оптимизации производительности в случаях, где немедленная, «жадная» инициализация может быть избыточной.
Подводные камни
- Применение Proxy (особенно lazy-loading варианта) для объектов с действительно дёшевой инициализацией, где выгода от отложенной загрузки не оправдывает дополнительную сложность и косвенность прокси-слоя — Proxy ценен именно для ДОРОГИХ операций (загрузка крупных ресурсов, сетевые запросы, тяжёлые вычисления), и для простых, быстро создаваемых объектов введение прокси-слоя добавляет накладные расходы (дополнительная проверка
if (!m_realImage)при каждом обращении) без соразмерной практической выгоды. - Незабота о потокобезопасности проверки и создания реального, «лениво» создаваемого объекта при использовании Proxy в многопоточном контексте (основной цикл статей про многопоточность) — без должной синхронизации одновременный, первый вызов
display()из нескольких потоков может привести к состоянию гонки при проверкеif (!m_realImage)и попытке создания реального объекта несколькими потоками одновременно, что требует явной защиты (мьютекс, или, для более простого случая инициализации единственного объекта, аналогичные подходуstd::call_onceили потокобезопасной локальной статической переменной из статья про Singleton). - Создание Proxy, реализующего тот же интерфейс, что и реальный объект, но с существенно отличающейся, неожидаемой клиентом семантикой поведения (например, protection proxy, который тихо игнорирует операцию вместо явного отказа при недостаточных правах доступа, без какого-либо сигнала клиентскому коду об этом отличии) — клиент, ожидающий поведение реального объекта согласно его обычному, документированному контракту, может быть удивлён неожиданным, отличающимся поведением через прокси, и качественная реализация Proxy должна явно документировать любые отличия в наблюдаемом поведении по сравнению с прямым взаимодействием с реальным объектом.
- Путаница между Proxy и Adapter — оба паттерна структурно похожи (обёртывание объекта другим объектом, реализующим определённый интерфейс), но решают принципиально разные задачи: Adapter согласует НЕСОВМЕСТИМЫЕ интерфейсы (оборачиваемый объект реализует ДРУГОЙ интерфейс), тогда как Proxy реализует ТОТ ЖЕ интерфейс, что и оборачиваемый объект, лишь контролируя доступ к нему — понимание этого различия в назначении, при структурном сходстве, важно для правильного выбора подходящего паттерна для конкретной задачи.