proxyПаттерн Заместитель (Proxy) предоставляет объект-заместитель, который управляет доступом к другому объекту. То есть создается объект-суррогат, который может выступать в роли другого объекта и замещать его.

Когда использовать прокси?

  • Когда надо осуществлять взаимодействие по сети, а объект-проси должен имитировать поведения объекта в другом адресном пространстве. Использование прокси позволяет снизить накладные издержки при передачи данных через сеть. Подобная ситуация еще называется удалённый заместитель (remote proxies)
  • Когда нужно управлять доступом к ресурсу, создание которого требует больших затрат. Реальный объект создается только тогда, когда он действительно может понадобится, а до этого все запросы к нему обрабатывает прокси-объект. Подобная ситуация еще называется виртуальный заместитель (virtual proxies)
  • Когда необходимо разграничить доступ к вызываемому объекту в зависимости от прав вызывающего объекта. Подобная ситуация еще называется защищающий заместитель (protection proxies)
  • Когда нужно вести подсчет ссылок на объект или обеспечить потокобезопасную работу с реальным объектом. Подобная ситуация называется “умные ссылки” (smart reference)

 

На C# паттерн формально может выглядеть следующим образом:

class Client
{
    void Main()
    {
        Subject subject = new Proxy();
        subject.Request();
    }
}
abstract class Subject
{
    public abstract void Request();
}
 
class RealSubject : Subject
{
    public override void Request()
    {}
}
class Proxy : Subject
{
    RealSubject realSubject;
    public override void Request()
    {
        if (realSubject == null)
            realSubject = new RealSubject();
        realSubject.Request();
    }
}

Участники паттерна

  • Subject: определяет общий интерфейс для Proxy и RealSubject. Поэтому Proxy может использоваться вместо RealSubject
  • RealSubject: представляет реальный объект, для которого создается прокси
  • Proxy: заместитель реального объекта. Хранит ссылку на реальный объект, контролирует к нему доступ, может управлять его созданием и удалением. При необходимости Proxy переадресует запросы объекту RealSubject
  • Client: использует объект Proxy для доступа к объекту RealSubject

Рассмотрим применение паттерна. Допустим, мы взаимодействуем с базой данных через Entity Framework. У нас есть модель и контекст данных:

class Page
{
    public int Id { get; set; }
    public int Number { get; set; }
    public string Text { get; set; }
}
class PageContext : DbContext
{
    public DbSet<Page> Pages { get; set; }
}

Класс Page представляет отдельную страницу книги, у которой есть номер и текст. Взаимодействие с базой данных может уменьшить производительность приложения. Для оптимизации приложения мы можем использовать паттерн Прокси. Для этого определим репозиторий и его прокси-двойник:

class Program
{
    static void Main(string[] args)
    {
        using(IBook book = new BookStoreProxy())
        {
            // читаем первую страницу
            Page page1 = book.GetPage(1);
            Console.WriteLine(page1.Text);
            // читаем вторую страницу
            Page page2 = book.GetPage(2);
            Console.WriteLine(page2.Text);
            // возвращаемся на первую страницу   
            page1 = book.GetPage(1);
            Console.WriteLine(page1.Text);
        }
             
        Console.Read();
    }
}
class Page
{
    public int Id { get; set; }
    public int Number { get; set; }
    public string Text { get; set; }
}
class PageContext : DbContext
{
    public DbSet<Page> Pages { get; set; }
}
 
interface IBook : IDisposable
{
    Page GetPage(int number);
}
 
class BookStore : IBook
{
    PageContext db;
    public BookStore()
    {
        db = new PageContext();
    }
    public Page GetPage(int number)
    {
        return db.Pages.FirstOrDefault(p => p.Number == number);
    }
 
    public void Dispose()
    {
        db.Dispose();
    }
}
 
class BookStoreProxy : IBook
{
    List<Page> pages;
    BookStore bookStore;
    public BookStoreProxy()
    {
        pages=new List<Page>();
    }
    public Page GetPage(int number)
    {
        Page page = pages.FirstOrDefault(p=>p.Number==number);
        if (page == null)
        {
            if (bookStore == null)
                bookStore = new BookStore();
            page= bookStore.GetPage(number);
            pages.Add(page);
        }
        return page;
    }
 
    public void Dispose()
    {
        if(bookStore!=null)
            bookStore.Dispose();
    }
}

Итак, здесь определен общий интерфейс IBook для реального объекта и для его прокси-класса. Он определяет один метод GetPage() для получения страницы по номеру.

Реальный объект BookStore использует контекст данных для извлечения информации о странице из базы данных. Действие же прокси-класса отличается. Прокси определяет дополнительный объект – список pages. При получении страницы прокси сначала смотрит в этот список, и если там страницы не окажется, то идет обращение к реальному объекту BookStore и его методу. То есть фактически будет реализована функциональность кэша страниц.

Клиент, в роли которого в данном случае выступает класс Program, вообще не будет знать, использует ли он функционал класса BookStore или его прокси.

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

 

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

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

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

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) изменять не требуется. Он может работать как с самим объектом, так и с Прокси. Однако в последнем случае будет заметно увеличение скорости повторного чтения записей.

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

2. Удаленный прокси

Процесс разработки Удаленного прокси в .NET достаточно прост. Это обеспечивается классами из пространства имен System.Runtime.Remoting. По сути, .NET предлагает свою реализацию данного шаблона. Но при этом на объекты приложения накладывается ряд требований, в частности:

  • Классы, передаваемые удаленно как параметры или возвращаемые значения, должны поддерживать сериализацию. Таким классом является EmployeeInfo и поэтому ему необходимо добавить атрибут [Serializable].
  • Класс, с которым будет осуществляться удаленное взаимодействие, должен быть унаследован от MarshalByRefObject. Поскольку доступ будет осуществляться к Виртуальному прокси, то добавим указанный класс в качестве родительского EmployeeDataSourceProxy.

Стоит отметить еще две особенности. При обращении к серверу .NET Remoting создает необходимые экземпляры класса и управляет временем их существования. При этом:

  1. Для создания объекта используется конструктор по умолчанию, а созданный объект не доступен локально. Для контроля над созданием или при необходимости вызывать конструктор с параметрами можно самостоятельно создать объект. Это и будет сделано в разрабатываемом коде.
  2. По умолчанию, при отсутствии обращений созданный объект будет уничтожен через определенное время и создан заново при очередном запросе. MarshalByRefObject предоставляет возможность установить свою политику путем переопределения метода InitializeLifetimeService(). В примере будем просто возвращать значение null, что соответствует неограниченному сроку жизни объекта.

Перейдем к модификации предыдущего примера. Классы EmployeeInfo, EmployeeDataSource и EmployeeDataSourceProxy будут задействованы как в проекте сервера, так и клиента. Поэтому вынесем их код в библиотеку классов и внесем указанные выше модификации.

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

[Serializable]
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 { /* Skipped */ }
 
public class EmployeeDataSourceProxy : MarshalByRefObject, 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);
    }
 
    public override object InitializeLifetimeService() { return null; }
}

Отдельно остановимся на классе DataSourceFactory. Теперь это Абстрактная фабрика, порождающая объекты как для клиента, так и для сервера. Кроме того, она скрывает не только тип создаваемых объектов, но и подробности взаимодействия с .NET Remote.

public static class DataSourceFactory
{
    private static bool _isClientNotRegistered = true;
    private static bool _isServerNotRegistered = true;
    private static readonly string _dataSourceUri = "B7167E88-14FC-4023-AF96-CB1E50E7CE5A";
    private static readonly object _threadSafetyObject = new object();
 
    public static IEmployeeDataSource CreateServerDataSource()
    {
        EmployeeDataSourceProxy dataSource = EmployeeDataSourceProxy.Instance;
 
        lock (DataSourceFactory._threadSafetyObject) {
            if (DataSourceFactory._isServerNotRegistered) {
                int tcpPort = 80;
 
                TcpServerChannel channel = new TcpServerChannel(tcpPort);
                ChannelServices.RegisterChannel(channel, true);
 
                RemotingConfiguration.RegisterWellKnownServiceType(
                    typeof(EmployeeDataSourceProxy),
                    DataSourceFactory._dataSourceUri, WellKnownObjectMode.Singleton);
 
                RemotingServices.Marshal(dataSource,
                    DataSourceFactory._dataSourceUri, typeof(EmployeeDataSourceProxy));
 
                DataSourceFactory._isServerNotRegistered = false;
            }
        }
 
        return dataSource;
    }
 
    public static IEmployeeDataSource CreateEmployeeDataSource(bool isRemote)
    {
        if (isRemote) {
            return DataSourceFactory.CreateRemoteDataSource();
        }
 
        return DataSourceFactory.CreateLocalDataSource();
    }
 
    private static IEmployeeDataSource CreateLocalDataSource()
    {
        return EmployeeDataSourceProxy.Instance;
    }
 
    private static IEmployeeDataSource CreateRemoteDataSource()
    {
        lock (DataSourceFactory._threadSafetyObject) {
            if (DataSourceFactory._isClientNotRegistered) {
                TcpClientChannel channel = new TcpClientChannel();
                ChannelServices.RegisterChannel(channel, true);
 
                string server = "localhost";
                int tcpPort = 80;
 
                string serverUrl = string.Format("tcp://{0}:{1}/{2}",
                    server, tcpPort, DataSourceFactory._dataSourceUri);
 
                RemotingConfiguration.RegisterWellKnownClientType(
                    typeof(EmployeeDataSourceProxy), serverUrl);
 
                DataSourceFactory._isClientNotRegistered = false;
            }
        }
 
        return EmployeeDataSourceProxy.Instance;
    }
}

Метод CreateServerDataSource() используется для создания объекта на сервере. Рассмотрим подробнее:

  1. Получаем экземпляр Виртуального прокси EmployeeDataSourceProxy. Как уже упоминалось, будем использовать готовый объект вместо его автоматического создания при запросах.
  2. Регистрируем канал (TcpServerChannel) и порт (80), через которые будет подключаться клиент. Для упрощения примера, в качестве номера порта используем готовое значение.
  3. Вызов метода RegisterWellKnownServiceType() регистрирует тип, который будет использоваться для работы с клиентами.
    • Передаваемое в качестве параметра значение _dataSourceUri является идентификатором для доступа к объекту. Можно использовать любую уникальную строку. В данном случае это обычный GUID.
    • Значение WellKnownObjectMode.Singleton указывает на то, что все запросы клиента будут обслуживаться один экземпляром объекта. Таким образом Remoting позволяет использовать любой класс как Одиночку, даже если сам он не является таковым. Другой режим, WellKnownObjectMode.SingleCall, подразумевает отдельные объекты для каждого запроса.
  4. Чтобы созданный на первом шаге экземпляр Виртуального прокси использовался для обслуживания клиентских запросов его необходимо зарегистрировать. С этой целью вызывается метод Marshal() c указанием объекта, его типа и идентификатора.

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

  1. Регистрируем канал для обращений к серверу.
  2. Регистрируем тип, экземпляры которого будут расположены на сервере. Обратите внимание, что протокол, адрес сервера, порт и идентификатор указаны в передаваемой строке serverUrl.
  3. После вызова RegisterWellKnownClientType() использование ключевого слова new с указанным типом будет приводить к созданию Удаленного прокси для объекта сервере.
  4. В завершении метода произойдет создание EmployeeDataSourceProxy. И как уже сказано .NET подменит экземпляр создаваемого класса на Удаленный прокси.

Стоит отметить, что полученная цепочка из Удаленного и Виртуального прокси по прежнему прозрачна для клиента. Детали кэширования и взаимодействия по сети скрыты в классах прокси, а подробности порождения – в Абстрактной фабрике DataSourceFactory.

Осталось рассмотреть создание клиента и сервера. Здесь все очень просто. Стоит только упомянуть один момент: для большей наглядности, добавим на сервере запись с id = 42 и запросим ее на клиенте. Кроме того, повторный запуск клиента покажет уже две измененные записи.

Код сервера выглядит следующим образом:

class Program
{
    static void Main(string[] args)
    {
        IEmployeeDataSource dataSource = DataSourceFactory.CreateServerDataSource();
 
        EmployeeInfo employeeInfo = new EmployeeInfo() {
            Id = 42,
            FullName = "John Doe"
        };
        dataSource.SetEmployeeInfo(employeeInfo);
         
        Console.WriteLine("Press [Enter] to terminate server...");
        Console.ReadLine();          
    }
}

Код клиента, по сути, не изменился. Единственное существенное отличие – появление параметра у метода CreateEmployeeDataSource(). Но это сделано исключительно в демонстрационных целях: изменяя значение переменной useRemoteDB можно запускать клиента как для работы с локальным источником данных, так и с удаленным.

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)
    {
        bool useRemoteDB = true;
        IEmployeeDataSource dataSource = DataSourceFactory.CreateEmployeeDataSource(useRemoteDB);
 
        ShowEmployeeInfo(11, dataSource);
        ShowEmployeeInfo(12, dataSource);
 
        SetEmployeeName(11, "Employee name 1", dataSource);
        SetEmployeeName(12, "Employee name 2", dataSource);
 
        ShowEmployeeInfo(42, dataSource);
 
        ShowEmployeeInfo(11, dataSource);
        ShowEmployeeInfo(12, dataSource);
 
        Console.WriteLine("nDone ...");
        Console.ReadKey(true);
    }
}