Заместитель - паттерн, контролирующий доступ к объектам, предоставляя более оптимальное их взаимодействие.
Условия, Задача, Назначение
Разумно управлять доступом к объекту, поскольку тогда можно отложить расходы на создание и инициализацию до момента, когда объект действительно понадобится. Одна из ключевых причин использования данного паттерна. Главная идея – не переписывая, не вмешиваясь в код некой цельной системы обеспечить более оптимальное и экономичное взаимодействие участников этой системы.
Таким образом, сначала выявляются узкие места, затем объекты, функционирование которых проходит не совсем оптимально, и вводятся эти самые объекты заместители (proxy), которые, дублируя интерфейс «проблемных» объектов, переадресовывают им вызовы клиента лишь тогда, когда это действительно необходимо, либо после некоторых оптимизационных действий. В результате, не делая никаких изменений в архитектуре системы, мы получаем немалые возможности по улучшению качества функционирования.
Мотивация
Рассмотрим редактор документов, который допускает встраивание в документ графических объектов. Затраты на создание некоторых таких объектов, например больших растровых изображений, могут быть весьма значительны. Но документ должен открываться быстро, поэтому следует избегать создания всех «тяжелых» объектов на стадии открытия (да и вообще это излишне, поскольку не все они будут видны одновременно).
В связи с такими ограничениями кажется разумным создавать «тяжелые» объекты по требованию. Это означает «когда изображение становится видимым». Но что поместить в документ вместо изображения? И как, не усложняя реализации редактора, скрыть то, что изображение создается по требованию? Например, оптимизация не должна отражаться на коде, отвечающем за рисование и форматирование (т.е. не должна никак меняться архитектура).
Решение состоит в том, чтобы использовать другой объект – заместитель изображения, который временно подставляется вместо реального изображения. Заместитель ведет себя точно так же, как само изображение, и выполняет при необходимости его инстанцирование.
Заместитель создает настоящее изображение, только если редактор документа вызовет операцию Draw у этой картинки. Все последующие запросы заместитель переадресует непосредственно изображению. Поэтому после создания изображения он должен
сохранить ссылку на него.
Предположим, что изображения хранятся в отдельных файлах. В таком случае мы можем использовать имя файла как ссылку на реальный объект. Заместитель хранит также размер изображения, то есть длину и ширину. «Зная» ее, заместитель может отвечать на запросы форматера о своем размере, не инстанцируя изображение.
На следующей диаграмме классов этот пример показан более подробно.
Редактор документов получает доступ к встроенным изображениям только через интерфейс, определенный в абстрактном классе
Graphic.
ImageProxy – это класс для представления изображений, создаваемых по требованию. В
ImageProxy хранится имя файла, играющее роль ссылки на изображение, которое находится на диске. Имя файла передается конструктору класса
ImageProxy.
В объекте ImageProxy находятся также ограничивающий прямоугольник изображения и ссылка на экземпляр реального объекта Image. Ссылка остается недействительной, пока заместитель не инстанцирует реальное изображение. Операцией Draw гарантируется, что изображение будет создано до того, как заместитель переадресует ему запрос. Операция GetExtent переадресует запрос изображению, только если оно уже инстанцировано, в противном случае ImageProxy возвращает размеры пустого прямоугольника, которые хранит сам.
Признаки применения, использования паттерна Заместитель (Proxy)
Паттерн заместитель применим во всех случаях, когда возникает необходимость сослаться на объект более изощренно, чем это возможно, если использовать простую ссылку. Вот несколько типичных ситуаций, где заместитель оказывается полезным:
- Когда требуется удаленный функционал.
Удаленный заместитель предоставляет локального представителя локального представителя вместо целевого объекта, находящегося в другом адресном пространстве. В реализации известного интерфейса удаленного взаимодействия RMI языка Java используется именно этот подход.
- Когда нужен виртуальный заместитель.
Виртуальный заместитель создает «тяжелые» объекты по требованию. Примером может служить класс ImageProxy, описанный ранее.
- Когда нужно контролировать доступ к исходному объекту.
Защищающий заместитель контролирует доступ к исходному объекту. Такие заместители полезны, когда для разных объектов определены различные права доступа. Например, в операционной системе Choices объекты Kernel Proxy ограничивают права доступа к объектам операционной системы.
- Когда нужно выполнять дополнительные действия при доступе к объекту.
«Умная ссылка» - это замена обычного указателя. Она позволяет выполнить дополнительные действия при доступе к объекту. К типичным применениям такой ссылки можно отнести:
- подсчет числа ссылок на реальный объект, с тем, чтобы занимаемую им память можно было освободить автоматически, когда не останется ни одной ссылки (такие ссылки называют еще «умными» указателями);
- загрузку объекта в память при первом обращении к нему;
- проверку и установку блокировки на реальный объект при обращении к нему, чтобы никакой другой объект не смог в это время изменить его.
- и т.д.
Решение
Вот как может выглядеть диаграмма объектов для структуры с заместителем во время выполнения:
Участники паттерна Заместитель (Proxy)
- Proxy (imageProxy) – заместитель,
Хранит ссылку, которая позволяет заместителю обратиться к реальному субъекту, используя тот же интерфейс класса Subject.
Предоставляет интерфейс, идентичный интерфейсу Subject, так что заместитель всегда может быть подставлен вместо реального субъекта.
Контролирует доступ к реальному субъекту и может отвечать за его создание и удаление.
Выполняет прочие обязанности - зависит от вида заместителя.
Удаленный заместитель отвечает за кодирование запроса и его аргументов и отправление закодированного запроса реальному субъекту в другом адресном пространстве (Java RMI).
Виртуальный заместитель может кэшировать дополнительную информацию о реальном субъекте, чтобы отложить его создание. Например, класс ImageProxy кэширует размеры реального изображения.
Защищающий заместитель проверяет, имеет ли вызывающий объект необходимые для выполнения запроса права.
- Subject (Graphic) – субъект.
Определяет общий для RealSubject и Proxy интерфейс, так что класс Proxy можно использовать везде, где ожидается RealSubject;
- RealSubject (Image) - реальный субъект.
Определяет реальный объект, представленный заместителем.
Схема использования паттерна Заместитель (Proxy)
Proxy при необходимости переадресует запросы объекту RealSubject. Детали зависят от вида заместителя. Либо запросы переадресовываются после выполнения некоторых действий по оптимизации. Детали опять же зависят от вида заместителя.
Вопросы, касающиеся реализации паттерна Заместитель (Proxy)
При реализации
паттерна заместитель можно использовать следующие возможности языка:
- Перегрузка оператора доступа к членам в C++.
Язык C++ поддерживает перегрузку оператора доступа к членам класса “->”. Это позволяет производить дополнительные действия при любом разыменовании указателя на объект. Для реализации некоторых видов заместителей это оказывается полезно, поскольку заместитель ведет себя аналогично указателю.
Но перегрузка оператора доступа - лучшее решение далеко не для всех видов заместителей. Некоторым из них должно быть точно известно, какая операция вызывается, а в таких случаях перегрузка оператора доступа не работает. Рассмотрим пример виртуального заместителя такого как ImageProxy. Изображение нужно загружать в точно определенное время - при вызове операции Draw, а не при каждом обращении к нему. Перегрузка оператора доступа не позволяет различить подобные случаи. В такой ситуации придется вручную реализовать каждую операцию заместителя, переадресующую запрос субъекту.
Обычно все эти операции очень похожи друг на друга. Они проверяют, что запрос корректен, что объект-адресат существует и т.д., а потом уже перенаправляют ему запрос. Писать этот код снова и снова надоедает. Поэтому нередко для его автоматической генерации используют препроцессор.
- Метод doesNotUnderstand в Smalltalk.
В языке Smalltalk есть возможность, позволяющая автоматически поддержать переадресацию запросов. При отправлении клиентом сообщения, для которого у получателя нет соответствующего метода, Smalltalk вызывает метод doesNotUnderstand: aMessage.
Заместитель может переопределить doesNotUnderstand так, что сообщение будет переадресовано субъекту.
Дабы гарантировать, что запрос будет перенаправлен субъекту, а не просто тихо поглощен заместителем, класс Proxy можно определить так, что он не станет понимать никаких сообщений. Smalltalk позволяет это сделать, надо лишь, чтобы у Proxy не было суперкласса.
Главный недостаток метода doesNotUnderstand: в том, что в большинстве Smalltalk-систем имеется несколько специальных сообщений, обрабатываемых непосредственно виртуальной машиной, а в этом случае стандартный механизм поиска методов обходится. Правда, единственной такой операцией, написанной в классе Ob j ect (следовательно, могущей затронуть заместителей), является тождество ==.
Если вы собираетесь применять doesNotUnderstand: для реализация заместителя, то должны как-то решить вышеописанную проблему. Нельзя же ожидать, что совпадение заместителей - это то же самое, что и совпадение реальных субъектов. К сожалению, doesNotUnderstand: изначально создавался для обработки ошибок, а не для построения заместителей, поэтому его быстродействие оставляет желать лучшего.
- Заместителю не всегда должен быть известен тип реального объекта.
Если класс Proxy может работать с субъектом только через его абстрактный интерфейс, то не нужно создавать Proxy для каждого класса реального субъекта RealSubject; заместитель может обращаться к любому из них единообразно. Но если заместитель должен инстанцировать реальных субъектов (как обстоит дело в случае виртуальных заместителей), то знание конкретного класса обязательно, либо опять же – использование абстрактной фабрики.
К проблемам реализации можно отнести и решение вопроса о том, как обращаться к еще не инстанцированному субъекту. Некоторые заместители должны обращаться к своим субъектам вне зависимости от того, где они находятся - диске или в памяти. Это означает, что нужно использовать какую-то форму не зависящих от адресного пространства идентификаторов объектов. В примере с
ImageProxy для этой цели использовалось имя файла.
Результаты
С помощью
паттерна заместитель при доступе к объекту вводится дополнительный уровень косвенности. У этого подхода есть много вариантов в зависимости от вида заместителя:
- Удаленный заместитель инкапсулирует тот факт, что объект находится в другом адресном пространстве;
- Виртуальный заместитель может выполнять оптимизацию, например создание объекта по требованию;
- Защищающий заместитель и «умная» ссылка позволяют решать дополнительные задачи (оптимизации) при доступе к объекту.
Есть еще одна оптимизация, которую
паттерн заместитель иногда скрывает от клиента. Она называется копированием при записи (copy-on-write) и имеет много общего с созданием объекта по требованию. Копирование большого и сложного объекта - очень дорогая операция. Если копия не модифицировалась, то нет смысла эту цену платить.
Если отложить процесс копирования, применив заместитель, то можно быть уверенным, что эта операция произойдет только тогда, когда он действительно был изменен.
Чтобы во время записи можно было копировать, необходимо подсчитывать ссылки на субъект. Копирование заместителя просто увеличивает счетчик ссылок. И только тогда, когда клиент запрашивает операцию, изменяющую субъект, заместитель действительно выполняет копирование. Одновременно заместитель должен уменьшить счетчик ссылок. Когда счетчик ссылок становится равным нулю, субъект уничтожается.
Копирование при записи может существенно уменьшить плату за копирование «тяжелых» субъектов.
Пример
Система обрабатывает поставки продуктов. На одном из этапов обработки система должна, к примеру, просто отобразить содержимое текущей поставки.
Имеется базовый интерфейс для наших продуктов:
ProductI.
Таким образом, мы должны используя этот интерфейс вывести содержимое каждого товара на печать (его название и цену). Конкретный базовый класс для продуктов имеет вид:
Product.
Нет ничего сложного в том, чтобы построить цикл по всем продуктам из поставки, выводя их название и стоимость. Допустим, что мы это уже сделали. Но что, вдруг, если, выводя эту суммарную информацию по товару, нам нужно будет учитывать еще и факт поставки, а именно: добавлять в название специальное словосочетание «[NOT DELIVERED]», например, и также корректировать цену на величину DELIVERY_COST определенную в
ProductI в виде константы? И это притом, что у нас уже имеется пара десятков разных классов типа
ProductI выводящих каждый раз по иному свою информацию об имени и цене: прямолинейное решение – добавлять в каждый такой класс эти модификации, заново затем все перекомпилировать – не совсем разумно и совсем неразумно с точки зрения, что это нам надо будет делать для каждого нового добавляемого типа продукта, а также все заново править и перекомпилировать в случае если снова понадобятся какие-либо модификации.
Просто инстанцируя в соответствующем слое, отвечающем за создание продуктов – вместо объектов
Product – объекты
ProductProxy, мы одним движением добиваемся добавления всех этих модификаций во все используемые классы продуктов:
Client.
Известные применения паттерна Заместитель (Proxy)
Пример виртуального заместителя из раздела
условия заимствован из классов строительного блока текста, определенных в каркасе
ЕТ++.
В системе
ссылку на удаленный объект, которая на самом деле является ссылкой на объект-заглушку. Параметры любого вызова клиента, сериализуются этой заглушкой и по специальному протоколу посылаются удаленному объекту. Возвращаемый в этой же форме ответ от сервера - декодируется и передается клиенту в виде возвращаемого значения метода.
В работе [google=McCullough McC87]McCullough" target="_blank" class="link">RMI Java[google] заместители используются как клиентские заглушки через которые происходит взаимодействие с реальным объектом на удаленном сервере в другом адресном пространстве. Клиентский код имеет т.н.
ссылку на удаленный объект, которая на самом деле является ссылкой на объект-заглушку. Параметры любого вызова клиента, сериализуются этой заглушкой и по специальному протоколу посылаются удаленному объекту. Возвращаемый в этой же форме ответ от сервера - декодируется и передается клиенту в виде возвращаемого значения метода.
В работе [google=McCullough McC87]McCullough обсуждается применение заместителей в Smalltalk для доступа к удаленным объектам. Джефри Пэско (Geoffrey Pascoe) описывает, как обеспечить побочные эффекты при вызове методов и реализовать контроль доступа с помощью «инкапсуляторов».
Родственные паттерны
Паттерн адаптер предоставляет другой интерфейс к адаптируемому объекту. Напротив,
заместитель в точности повторяет интерфейс своего субъекта. Однако, если заместитель используется для ограничения доступа, он может отказаться выполнять операцию, которую субъект выполнил бы, поэтому на самом деле интерфейс заместителя может быть и подмножеством интерфейса субъекта.
Несколько замечаний относительно
декоратора. Хотя его реализация и похожа на реализацию заместителя, но назначение совершенно иное.
Декоратор добавляет объекту новые обязанности, а заместитель контролирует доступ к объекту. Степень схожести реализации
заместителей и
декораторов может быть различной.
Защищающий заместитель мог бы быть реализован в точности как
декоратор. С другой стороны,
удаленный заместитель не содержит прямых ссылок на реальный субъект, а лишь косвенную ссылку, что-то вроде «идентификатор хоста и локальный адрес на этом хосте». Вначале
виртуальный заместитель имеет только косвенную ссылку (скажем, имя файла), но в конечном итоге получает и использует прямую ссылку.