Структурные шаблоны: Прокси (Proxy)

Название шаблона

Прокси / Заместитель (Proxy / Surrogate).

Тип

Структурный шаблон проектирования (Structural).

Описание

Proxy patternПрокси предназначен для управления доступом к заданному объекту, перехватывая все вызовы к нему и прозрачно замещая его. Ни интерфейс ни функциональность замещенного компонента, с точки зрения клиента, не изменяются.

Данный шаблон используется если:

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

Таким образом, задача Прокси – упростить и/или оптимизировать взаимодействие с объектом, скрывая несущественные для конкретной задачи подробности реализации. При этом он прозрачен для клиента, поскольку предоставляет тот же интерфейс, что и замещаемый объект.

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

В зависимости от цели использования можно выделить следующие типы Прокси:

  1. Удаленный прокси (Remote proxy) – обеспечивает взаимодействие с объектом в другом адресном пространстве. Это может быть как другое приложение на текущем компьютере, так и компонент на интернет сервере. При этом клиент обращается к объекту как к локальному, не замечая различий.
  2. Виртуальный прокси (Virtual proxy) – выполняет оптимизацию доступа к объекту, может самостоятельно обрабатывать некоторые запросы. Например, создание ресурсоёмких объектов только при абсолютной необходимости в них. Другим характерным примером может служить кэширование, в результате которого Прокси сам может отвечать на часть запросов.
  3. Защищающий прокси (Protection proxy) – управляет объектом, разграничивая права доступа различных клиентов.
  4. Умная ссылка (Smart reference) – обеспечивает выполнение дополнительных действий при вызове методов объекта. Примерами могут служить подсчет ссылок или обеспечение потокобезопасности работы с объектом.

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

Особенности использования

Хорошей практикой порождения экземпляра Прокси является использование Фабричного метода или Абстрактной фабрики. Клиент в этом случае не знает с какой реализацией интерфейса он работает:с реальным объектом или с Прокси. Это обеспечивает больший уровень прозрачности применения шаблона.

Стоит отметить, что шаблон позволяет использовать интерфейс для взаимодействия с замещаемым объектом. В этом случае конкретная реализация скрыта от Прокси и он м��жет работать практически со всеми объектами, поддерживающими данный интерфейс.

Шаблон не накладывает ограничения на вложения одного Прокси в другой. Такой подход позволяет строить их различные конфигурации в процессе работы программы.

Интересный вариант применения шаблона – копирование по требованию. Его идея заключается в следующем: копирование тяжелых объектов всегда ресурсоемкая операция. Но иногда копия на протяжении части или даже всего своего существования не отличается от оригинала. В этом случае можно отложить реальное копирование данных до момента первой модификации копии или оригинала. Это может сэкономить ресурсы при использовании больших объектов.

Схожие шаблоны и их отличия

Прокси Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. Содержит объект или ссылку на него, может управлять существованием замещенного объекта.

Адаптер Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. Позволяет повторно использовать уже существующий код. Содержит или наследует адаптируемый объект.
Фасад Объединяет группу объектов под одним специализированным интерфейсом. Упрощает работу с группой объектов, вносит новый уровень абстракции. Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса.
Мост Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников).
Декоратор Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно.
Компоновщик Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. Включается в виде интерфейса в составные объекты и их части.
Приспособленец

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

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

Реализация шаблона в общем виде

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

Примеры реализации

1. Виртуальный прокси

В данном примере разработаем Виртуальный прокси для оптимизации работы с объектом.

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

Интерфейс IEmployeeDataSource содержит методы для чтения и записи данных о сотруднике. Его реализует класс EmployeeDataSource, для порождения экземпляров которого существует Фабричный метод DataSourceFactory.CreateEmployeeDataSource().

Для упрощения примера, в вместо базы данных будем использовать static поле _database. Медленную скорость выполнения запроса получения данных эмулируем вызовом метода Sleep(). Кроме того, в целях демонстрации добавим вывод на консоль сообщений о текущих операциях.

Исходный код:

public class EmployeeInfo
{
    public int Id { get; set; }

    public string FullName { get; set; }
 
    /* Skipped */
}

public interface IEmployeeDataSource
{
    EmployeeInfo GetEmployeeInfo(int id);

    void SetEmployeeInfo(EmployeeInfo employeeInfo);
}

public class EmployeeDataSource : IEmployeeDataSource
{
   private static ConcurrentDictionary<int, EmployeeInfo> _database
        = new ConcurrentDictionary<int, EmployeeInfo>();

    public EmployeeDataSource()
    {
        Console.WriteLine("EmployeeDataSource ctor ... ");
    }

    public EmployeeInfo GetEmployeeInfo(int id)
    {
        /*  Demo */
        Console.WriteLine("Loading {0} from DB... ", id);
        Thread.Sleep(1000);

        return EmployeeDataSource._database.GetOrAdd(id, this.CreateNewEmployee);
    }

    public void SetEmployeeInfo(EmployeeInfo employeeInfo)
    {
        /* Demo */
        Console.WriteLine("Saving ({0}, {1}) to DB... ",
            employeeInfo.Id, employeeInfo.FullName);

        EmployeeDataSource._database.AddOrUpdate(employeeInfo.Id,
            employeeInfo, (key, value) => employeeInfo);
    }

    private EmployeeInfo CreateNewEmployee(int id)
    {
        return new EmployeeInfo() {
            Id = id,
            FullName = "[NoName]"
        };
    }
}

public static class DataSourceFactory
{
    public static IEmployeeDataSource CreateEmployeeDataSource()
    {
        return new EmployeeDataSource();
    }
}

Для демонстрации просто считаем и запишем данные пары сотрудников. Пусть их id равны 11 и 12:

class Program
{
    public static void ShowEmployeeInfo(int id, IEmployeeDataSource dataSource)
    {
        EmployeeInfo employeeInfo = dataSource.GetEmployeeInfo(id);
        Console.WriteLine("Employee id = {0}", employeeInfo.Id);
        Console.WriteLine("Employee name = {0}\n", employeeInfo.FullName);
    }

    public static void SetEmployeeName(int id, string fullName, IEmployeeDataSource dataSource)
    {
        EmployeeInfo employeeInfo = dataSource.GetEmployeeInfo(id);
        employeeInfo.FullName = fullName;
        dataSource.SetEmployeeInfo(employeeInfo);
    }

    public static void Main(string[] args)
    {
        IEmployeeDataSource dataSource = DataSourceFactory.CreateEmployeeDataSource();

        ShowEmployeeInfo(11, dataSource);
        ShowEmployeeInfo(12, dataSource);

        SetEmployeeName(11, "Employee 1 name", dataSource);
        SetEmployeeName(12, "Employee 2 name", dataSource);

        ShowEmployeeInfo(11, dataSource);
        ShowEmployeeInfo(12, dataSource);

        Console.WriteLine("\nDone ...");
        Console.ReadKey(true);
    }
}

Оптимизируем чтение данных с использованием Виртуального прокси. Применим кэширование данных о сотрудниках, используя потокобезопасный класс ConcurrentDictionary из .NET4. Кроме того, шаблон Одиночка поможет сделать кэш единым для всех компонентов приложения.

Прокси реализует интерфейса замещаемого объекта IEmployeeDataSource, а так же сам создает его экземпляр для работы.

public class EmployeeDataSourceProxy : IEmployeeDataSource
{
    #region Singleton implementation

    private static readonly Lazy<EmployeeDataSourceProxy> _instance =
        new Lazy<EmployeeDataSourceProxy>(() => new EmployeeDataSourceProxy());

    public static EmployeeDataSourceProxy Instance
    {
        get { return EmployeeDataSourceProxy._instance.Value; }
    }

    private EmployeeDataSourceProxy()
    {
        Console.WriteLine("EmployeeDataSourceProxy ctor...");
    }

    #endregion

    private readonly IEmployeeDataSource _dataSource = new EmployeeDataSource();

    private static ConcurrentDictionary<int, EmployeeInfo> _cache
        = new ConcurrentDictionary<int, EmployeeInfo>();

    public EmployeeInfo GetEmployeeInfo(int id)
    {
        return EmployeeDataSourceProxy._cache.GetOrAdd(id, this._dataSource.GetEmployeeInfo);
    }

    public void SetEmployeeInfo(EmployeeInfo employieInfo)
    {
        this._dataSource.SetEmployeeInfo(employieInfo);

        EmployeeDataSourceProxy._cache.AddOrUpdate(employieInfo.Id,
            employieInfo, (key, value) => employieInfo);
    }
}

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

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

Теперь необходимо поправить DataSourceFactory так, чтобы он порождал созданный Прокси.

public static class DataSourceFactory
{
    public static IEmployeeDataSource CreateEmployeeDataSource()
    {
        return EmployeeDataSourceProxy.Instance;
    }
}

Обратите внимание, что клиентский код (класс Program) изменять не требуется. Он может работать как с самим объектом, так и с Прокси. Однако в последнем случае будет заметно увеличение скорости повторного чтения записей.

В следующей части рассмотрим разработку Удаленного прокси c использованием возможностей .NET.

Комментарии (8) -

Таксономия проксей совсем неправильная. В том смысле, что начинать стоит от разделения на статические и динамические прокси. Ну я говоря о динамических проксях не грех упомянуть про АОП и штучки типа Unity.Interceptor или Castle.DynamicProxy.

@ Дмитрий Нестерук: Я все же считаю, что в основе должна быть цель, для которой объект создается. Статический или динамический - это все же больше к деталям реализации.

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

Андрей, добро пожаловать в подписки Smile Именно так нужно рассказывать про паттерны. Спасибо!

@ Илья: И вам спасибо за отзыв.

Если это структурный шаблон, почему тип указан: порождающий? Очепятка?

Действительно опечатка. Спасибо что сообщили.

Александр 19.10.2011 21:26:06

Ссылки на фабричный метод и на абстрактую фабрику не работают

@ Александр: Спасибо за сообщение. Перемудрил с Rewrite. Поправил.

Добавить комментарий