Название шаблона
Прокси / Заместитель (Proxy / Surrogate).
Тип
Структурный шаблон проектирования (Structural).
Описание
Прокси предназначен для управления доступом к заданному объекту, перехватывая все вызовы к нему и прозрачно замещая его. Ни интерфейс ни функциональность замещенного компонента, с точки зрения клиента, не изменяются.
Данный шаблон используется если:
- работа с объектом не должна зависеть от того, где он реально расположен (от адресного пространства приложения до удаленного сервера);
- (или) нужно выполнять определенные действия при доступе к объекту;
- (или) необходимо оптимизировать взаимодействие объекта с клиентом.
Таким образом, задача Прокси – упростить и/или оптимизировать взаимодействие с объектом, скрывая несущественные для конкретной задачи подробности реализации. При этом он прозрачен для клиента, поскольку предоставляет тот же интерфейс, что и замещаемый объект.
Реализация шаблона может как порождать экземпляр замещаемого объекта, так и ссылаться на уже существующий. Независимо от этого, Прокси контролирует все взаимодействия с объектом, а в некоторых случаях может управлять его существованием.
В зависимости от цели использования можно выделить следующие типы Прокси:
- Удаленный прокси (Remote proxy) – обеспечивает взаимодействие с объектом в другом адресном пространстве. Это может быть как другое приложение на текущем компьютере, так и компонент на интернет сервере. При этом клиент обращается к объекту как к локальному, не замечая различий.
- Виртуальный прокси (Virtual proxy) – выполняет оптимизацию доступа к объекту, может самостоятельно обрабатывать некоторые запросы. Например, создание ресурсоёмких объектов только при абсолютной необходимости в них. Другим характерным примером может служить кэширование, в результате которого Прокси сам может отвечать на часть запросов.
- Защищающий прокси (Protection proxy) – управляет объектом, разграничивая права доступа различных клиентов.
- Умная ссылка (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.