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

Заместитель

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

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

Проблема

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

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

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

Решение

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

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

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

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

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

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

Структура

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

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

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

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

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

Псевдокод

// Интерфейс удалённого сервиса.
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