Также известен как Proxy

Заместитель

Суть паттерна

Оборачивает полезный объект или сервис специальным объектом-заменителем, который «притворяется» оригиналом и перехватывает все вызовы к нему, а затем после некоторой обработки, направляет их обёрнутому объекту.

Проблема

Для чего вообще контролировать доступ к объектам? Рассмотрим такой пример: у вас есть внешний ресурсоёмкий объект, который нужен не все время, а изредка.

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

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

Решение

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

Но в чём же здесь польза? Вы могли бы поместить в класс заместителя какую-то промежуточную логику, которая выполнялась бы до (или после) вызовов этих же методов в настоящем объекте. А благодаря одинаковому интерфейсу, объект заместитель можно передатьдать в любой код, ожидающий сервисный объект.

Аналогия из жизни

Банковский чек

Банковский чек — это заместитель пачки наличности. И чек, и наличность имеют общий интерфейс — ними можно оплачивать товары.

Для покупателя польза в том, что не надо таскать с собой тонны наличности. А владелец магазина может превратить чек в зелёные бумажки, обратившись в банк.

Структура

Схема структуры классов паттерна Заместитель
  1. Интерфейс сервиса определяет общий интерфейс для Сервиса и Заместителя. Благодаря этому, объект Заместителя можно использовать там, где ожидается объект Сервиса.

  2. Сервис содержит полезную бизнес-логику.

  3. Заместитель хранит ссылку на объект Сервиса. После того как Заместитель заканчивает свою работу (например, инициализацию, логирование, защиту или другое), он передаёт вызовы вложенному Сервису.

    Заместитель может сам отвечать за создание и удаление объекта Сервиса.

  4. Клиент работает с объектами через Интерфейс сервиса. Благодаря этому, его можно «одурачить» и подменить объект Сервиса объектом Заместителя.

Псевдокод

В этом примере Заместитель помогает добавить в программу механизм ленивой инициализации и кеширования тяжёлой служебной библиотеки интеграции с Youtube.

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

// Интерфейс удалённого сервиса.
interface ThirdPartyYoutubeLib is
    method listVideos()
    method getVideoInfo(id)
    method downloadVideo(id)

// Конкретная реализация сервиса. Методы этого класса запрашивают у ютуба
// различную информацию. Скорость запроса зависит от интернет-канала
// пользователя и состояния самого ютуба. Чем больше будет вызовов к сервису,
// тем менее отзывчивой будет программа.
class ThirdPartyYoutubeClass is
    method listVideos() is
        Send API request to Youtube.

    method getVideoInfo(id) is
        Get a meta information about some video.

    method downloadVideo(id) is
        Download video file from Youtube.

// С другой стороны, можно кешировать запросы к ютубу и не повторять их какое-то
// время, пока кеш не устареет. Но внести этот код напрямую в сервисный класс
// нельзя, так как он находится в сторонней библиотеке. Поэтому мы поместим
// логику кеширования в отдельный класс-обёртку. Он будет делегировать запросы к
// сервисному объекту, только если нужно непосредственно выслать запрос.
class CachedYoutubeClass implements ThirdPartyYoutubeLib is
    field youtubeService: ThirdPartyYoutubeClass
    field listCache, videoCache
    field needReset

    constructor YoutubeCache() is
        youtubeService = new ThirdPartyYoutubeClass()

    method listVideos() is
        if (listCache == null && !needReset)
            listCache = youtubeService.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null && !needReset)
            videoCache = youtubeService.getVideoInfo(id)
        return videoCache

    method downloadVideo(id is
        if (!downloadExists(id) && !needReset)
            youtubeService.listVideos()

// Класс GUI, который использует сервисный объект. Вместо реального сервиса, мы
// подсунем ему объект-заместитель. Клиент ничего не заметит, так как
// заместитель имеет тот же интерфейс, что и сервис.
class YoutubeManager is
    field service: ThirdPartyYoutubeLib

    constructor YoutubeManager(service: ThirdPartyYoutubeClass) is
        this.service = service

    method renderVideoPage() is
        info = service.getVideoInfo()
        Render the video page.

    method renderListPanel() is
        list = service.listVideos()
        Render the list of video thumbnails.

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()

// Конфигурационная часть приложения создаёт и передаёт клиентам
// объект заместителя.
class Application is
    method init() is
        youtubeLib = new CachedYoutubeClass();
        manager = new YoutubeManager(youtubeLib)
        manager.reactOnUserInput()

Применимость

Ленивая инициализация (виртуальный прокси). Когда у вас есть тяжёлый объект, грузящий данные из файловой системы или базы данных.

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

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

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

Локальный запуск сервиса (удалённые прокси). Когда настоящий сервисный объект находится на удалённом сервере.

В этом случае заместитель транслирует запросы клиента в вызовы по сети, в протоколе понятном удалённому сервису.

Кеширование объектов («умная» ссылка). Когда нужно кешировать результаты запросов клиентов и управлять их жизненным циклом.

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

Кроме того, Заместитель может отслеживать, не менял ли клиент сервисный объект. Это позволит использовать объекты повторно и здорово экономить ресурсы, особенно если речь идёт о больших ресурсоёмких сервисах.

Логирование запросов (логирующий прокси). Когда требуется хранить историю обращений к сервисному объекту.

Заместитель может сохранять историю обращения клиента к сервисному объекту.

Шаги реализации

  1. Определите интерфейс, который бы сделал заместитель и оригинальный объект взаимозаменямыми.

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

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

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

  5. Подумайте, не реализовать ли вам ленивую инициализацию сервисного объекта при первом обращении клиента к методам заместителя.

Преимущества и недостатки

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

Отношения с другими паттернами

  • Адаптер предоставляет классу альтернативный интерфейс. Заместитель предоставляет тот же интерфейс. Декоратор предоставляет расширенный интерфейс.

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

  • Декоратор и Заместитель имеют похожие структуры, но разные назначения. Они похожи тем, что оба построены на композиции и делегировании работы другому объекту. Паттерны отличаются тем, что Заместитель сам управляет жизнью сервисного объекта, а обёртывание Декораторов контролируется клиентом.

Реализация в различных языках программирования

Java