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

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

Суть паттерна

Заместитель — это структурный паттерн проектирования, который позволяет подставлять вместо реальных объектов специальные объекты-заменители. Эти объекты перехватывают вызовы к оригинальному объекту, позволяя сделать что-то до или после передачи вызова оригиналу.

Проблема

Для чего вообще контролировать доступ к объектам? Рассмотрим такой пример: у вас есть внешний ресурсоёмкий объект, который нужен не все время, а изредка.

Мы могли бы создавать этот объект не в самом начале программы, а только тогда, когда он кому-то реально понадобится. Каждый клиент объекта получил бы некий код отложенной инициализации. Но, вероятно, это привело бы к множественному дублированию кода.

В идеале, этот код хотелось бы поместить прямо в служебный класс, но это не всегда возможно. Например, код класса может находиться в закрытой сторонней библиотеке.

Решение

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

Но в чём же здесь польза? Вы могли бы поместить в класс заместителя какую-то промежуточную логику, которая выполнялась бы до (или после) вызовов этих же методов в настоящем объекте. А благодаря одинаковому интерфейсу, объект-заместитель можно передать в любой код, ожидающий сервисный объект.

Аналогия из жизни

Платёжная карточка — это заместитель пачки наличных. И карточка, и наличные имеют общий интерфейс — ими можно оплачивать товары. Для покупателя польза в том, что не надо таскать с собой тонны наличных, а владелец магазина рад, что ему не нужно делать дорогостоящую инкассацию наличности в банк — деньги поступают к нему на счёт напрямую.

Структура

  1. Интерфейс сервиса определяет общий интерфейс для сервиса и заместителя. Благодаря этому, объект заместителя можно использовать там, где ожидается объект сервиса.

  2. Сервис содержит полезную бизнес-логику.

  3. Заместитель хранит ссылку на объект сервиса. После того как заместитель заканчивает свою работу (например, инициализацию, логирование, защиту или другое), он передаёт вызовы вложенному сервису.

    Заместитель может сам отвечать за создание и удаление объекта сервиса.

  4. Клиент работает с объектами через интерфейс сервиса. Благодаря этому, его можно «одурачить», подменив объект сервиса объектом заместителя.

Применимость

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

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

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

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

Локальный запуск сервиса (удалённый прокси). Когда настоящий сервисный объект находится на удалённом сервере.

В этом случае заместитель транслирует запросы клиента в вызовы по сети в протоколе, понятном удалённому сервису.

Логирование запросов (логирующий прокси). Когда требуется хранить историю обращений к сервисному объекту.

Заместитель может сохранять историю обращения клиента к сервисному объекту.

Кеширование объектов («умная» ссылка). Когда нужно кешировать результаты запросов клиентов и управлять их жизненным циклом.

Заместитель может подсчитывать количество ссылок на сервисный объект, которые были отданы клиенту и остаются активными. Когда все ссылки освобождаются, можно будет освободить и сам сервисный объект (например, закрыть подключение к базе данных).

Кроме того, Заместитель может отслеживать, не менял ли клиент сервисный объект. Это позволит использовать объекты повторно и здóрово экономить ресурсы, особенно если речь идёт о больших прожорливых сервисах.

Шаги реализации

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

  2. Создайте класс заместителя. Он должен содержать ссылку на сервисный объект. Чаще всего, сервисный объект создаётся самим заместителем. В редких случаях заместитель получает готовый сервисный объект от клиента через конструктор.

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

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

  5. Подумайте, не реализовать ли вам ленивую инициализацию сервисного объекта при первом обращении клиента к методам заместителя.

Преимущества и недостатки

  • Позволяет контролировать сервисный объект незаметно для клиента.

  • Может работать, даже если сервисный объект ещё не создан.

  • Может контролировать жизненный цикл служебного объекта.

  • Усложняет код программы из-за введения дополнительных классов.

  • Увеличивает время отклика от сервиса.

Пример

# include <iostream>
# include <memory>
# include <map>
# include <random>

using namespace std;

class Subject
{
public:
        virtual ~Subject() = default;
        
        virtual pair<bool, double> request(size_t index) = 0;
        virtual bool changed() { return true; }
};

class RealSubject : public Subject
{
private:
        bool flag{ false };
        size_t counter{ 0 };

public:
        virtual pair<bool, double> request(size_t index) override;
        virtual bool changed() override;
};

class Proxy : public Subject
{
protected:
        shared_ptr<RealSubject> realsubject;

public:
        Proxy(shared_ptr<RealSubject> real) : realsubject(real) {}
};

class ConProxy : public Proxy
{
private:
        map<size_t, double> cache;

public:
        using Proxy::Proxy;

        virtual pair<bool, double> request(size_t index) override;
};

#pragma region Methods
bool RealSubject::changed()
{
        if (counter == 0)
        {
                flag = true;
        }
        if (++counter == 7)
        {
                counter = 0;
                flag = false;
        }
        return flag;
}


pair<bool, double> RealSubject::request(size_t index)
{
        random_device rd;
        mt19937 gen(rd());
        
        return pair<bool, double>(true, generate_canonical<double, 10>(gen));
}

pair<bool, double> ConProxy::request(size_t index)
{
        pair<bool, double> result;
        
        if (!realsubject)
        {
                cache.clear();
                
                result = pair<bool, double>(false, 0.);
        }
        else if (!realsubject->changed())
        {
                cache.clear();
                
                result = realsubject->request(index);
                
                cache.insert(map<size_t, double>::value_type(index, result.second));
        }
        else
        {
                map<size_t, double>::const_iterator it = cache.find(index);
        
                if (it != cache.end())
                {
                        result = pair<bool, double>(true, it->second);
                }
                else
                {
                        result = realsubject->request(index);
                
                        cache.insert(map<size_t, double>::value_type(index, result.second));
                }
        }
        
        return result;
}
#pragma endregion

int main()
{
        shared_ptr<RealSubject> subject = make_shared<RealSubject>();
        shared_ptr<Subject> proxy = make_shared<ConProxy>(subject);
        
        for (size_t i = 0; i < 21; ++i)
        {
                cout << "( " << i + 1 << ", " << proxy->request(i % 3).second << " )" << endl;
                
                if ((i + 1) % 3 == 0)
                        cout << endl;
        }
}

Last updated