Структурные шаблоны: Прокси (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.