Також відомий як: Proxy

Заступник

Суть патерну

Заступник — це структурний патерн проектування, який дає змогу підставляти замість реальних об'єктів спеціальні об'єкти-замінники. Ці об'єкти перехоплюють виклики до оригінального об'єкта, дозволяючи зробити щось до чи після передачі виклику оригіналові.

Патерн Заступник

Проблема

Для чого взагалі контролювати доступ до об'єктів? Розглянемо такий приклад: у вас є зовнішній ресурсоємний об'єкт, який потрібен не весь час, а лише зрідка.

Проблема, яку вирішує Заступник

Запити до бази даних можуть бути дуже повільними.

Ми могли б створювати цей об'єкт не на самому початку програми, а тільки тоді, коли він реально кому-небудь знадобиться. Кожен клієнт об'єкта отримав би деякий код відкладеної ініціалізації. Це, ймовірно, призвело б до дублювання великої кількості коду.

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

Рішення

Патерн Заступник пропонує створити новий клас-дублер, який має той самий інтерфейс, що й оригінальний службовий об'єкт. При отриманні запиту від клієнта об'єкт-заступник сам би створював примірник службового об'єкта та переадресовував би йому всю реальну роботу.

Рішення з допомогою Заступника

Заступник «прикидається» базою даних, прискорюючи роботу внаслідок ледачої ініціалізації і кешування запитів, що повторюються.

Але в чому ж його користь? Ви могли б помістити до класу заступника якусь проміжну логіку, що виконувалася б до або після викликів цих самих методів чинного об'єкта. А завдяки однаковому інтерфейсу об'єкт-заступник можна передати до будь-якого коду, що очікує на сервісний об'єкт.

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

Приклад з чеком та готівкою

Банківським чеком можна розраховуватися так само, як і готівкою.

Банківський чек — це заступник пачки готівки. І чек, і готівка мають спільний інтерфейс — ними обома можна оплачувати товари. Вигода покупця в тому, що не потрібно носити з собою «тонни» готівки. З іншого боку власник магазину, звернувшись до банку, зможе перетворити чек на «зелені папірці».

Структура

Структура класів патерну ЗаступникСтруктура класів патерну Заступник
  1. Інтерфейс сервісу визначає загальний інтерфейс для сервісу й заступника. Завдяки цьому об'єкт заступника можна використовувати там, де очікується об'єкт сервісу.

  2. Сервіс містить корисну бізнес-логіку.

  3. Заступник зберігає посилання на об'єкт сервісу. Після того, як заступник закінчує свою роботу (наприклад, ініціалізацію, логування, захист або інше), він передає виклики вкладеному сервісу.

    Заступник може сам відповідати за створення й видалення об'єкта сервісу.

  4. Клієнт працює з об'єктами через інтерфейс сервісу. Завдяки цьому його можна «обдурити», підмінивши об'єкт сервісу об'єктом заступника.

Псевдокод

У цьому прикладі Заступник допомагає додати до програми механізм ледачої ініціалізації та кешування результатів роботи бібліотеки інтеграції з Youtube.

Структура класів прикладу патерну Заступник

Приклад кешування результатів роботи реального сервісу за допомогою заступника.

Оригінальний об'єкт починав завантаження з мережі, навіть якщо користувач повторно запитував одне й те саме відео. Заступник завантажує відео тільки один раз, використовуючи для цього службовий об'єкт, але в інших випадках повертає закешований файл.

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

// Конкретна реалізація сервісу. Методи цього класу запитують у
// Youtube різну інформацію. Швидкість запиту залежить не лише
// від якості інтернет-каналу користувача, але й від стану
// самого Youtube. Тому, чим більше буде викликів до сервісу,
// тим менш відзивною стане програма.
class ThirdPartyYoutubeClass is
    method listVideos() is
        // Отримати список відеороликів за допомогою API
        // Youtube.

    method getVideoInfo(id) is
        // Отримати детальну інформацію про якийсь відеоролик.

    method downloadVideo(id) is
        // Завантажити відео з Youtube.

// З іншого боку, можна кешувати запити до Youtube і не
// повторювати їх деякий час, доки кеш не застаріє. Але внести
// цей код безпосередньо в сервісний клас неможливо, бо він
// знаходиться у сторонній бібліотеці. Тому ми помістимо логіку
// кешування в окремий клас-обгортку. Він буде делегувати запити
// сервісному об'єкту, тільки якщо потрібно безпосередньо
// відіслати запит.
class CachedYoutubeClass implements ThirdPartyYoutubeLib is
    private field service: ThirdPartyYoutubeClass
    private field listCache, videoCache
    field needReset

    constructor CachedYoutubeClass(service: ThirdPartyYoutubeLib) is
        this.service = service

    method listVideos() is
        if (listCache == null || needReset)
            listCache = service.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id)
        return videoCache

    method downloadVideo(id) is
        if (!downloadExists(id) || needReset)
            service.downloadVideo(id)

// Клас GUI, який використовує сервісний об'єкт. Замість
// реального сервісу, ми підсунемо йому об'єкт-заступник. Клієнт
// нічого не помітить, так як заступник має той самий інтерфейс,
// що й сервіс.
class YoutubeManager is
    protected field service: ThirdPartyYoutubeLib

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

    method renderVideoPage() is
        info = service.getVideoInfo()
        // Відобразити сторінку відеоролика.

    method renderListPanel() is
        list = service.listVideos()
        // Відобразити список превью відеороликів.

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()

// Конфігураційна частина програми створює та передає клієнтам
// об'єкт заступника.
class Application is
    method init() is
        youtubeService = new ThirdPartyYoutubeClass()
        youtubeProxy = new CachedYoutubeClass(youtubeService)
        manager = new YoutubeManager(youtubeProxy)
        manager.reactOnUserInput()

Застосування

Лінива ініціалізація (віртуальний проксі). Коли у вас є важкий об'єкт, який завантажує дані з файлової системи або бази даних.

Замість того, щоб завантажувати дані відразу після старту програми, можна заощадити ресурси й створити об'єкт тоді, коли він дійсно знадобиться.

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

Проксі може перевіряти доступ під час кожного виклику та передавати виконання службовому об'єкту, якщо доступ дозволено.

Локальний запуск сервісу (віддалений проксі). Коли справжній сервісний об'єкт знаходиться на віддаленому сервері.

У цьому випадку заступник транслює запити клієнта у виклики через мережу по протоколу, який є зрозумілим віддаленому сервісу.

Логування запитів (логуючий проксі). Коли потрібно зберігати історію звернень до сервісного об'єкта.

Заступник може зберігати історію звернення клієнта до сервісного об'єкта.

Кешування об'єктів («розумне» посилання). Коли потрібно кешувати результати запитів клієнтів і керувати їхнім життєвим циклом.

Заступник може підраховувати кількість посилань на сервісний об'єкт, які були віддані клієнту та залишаються активними. Коли всі посилання звільняться, можна буде звільнити і сам сервісний об'єкт (наприклад, закрити підключення до бази даних).

Крім того, Заступник може відстежувати, чи клієнт не змінював сервісний об'єкт. Це дозволить повторно використовувати об'єкти й суттєво заощаджувати ресурси, особливо якщо мова йде про великі «ненажерливі» сервіси.

Кроки реалізації

  1. Визначте інтерфейс, який би зробив заступника та оригінальний об'єкт взаємозамінними.

  2. Створіть клас заступника. Він повинен містити посилання на сервісний об'єкт. Частіше за все сервісний об'єкт створюється самим заступником. У рідкісних випадках заступник отримує готовий сервісний об'єкт від клієнта через конструктор.

  3. Реалізуйте методи заступника в залежності від його призначення. У більшості випадків, виконавши якусь корисну роботу, методи заступника повинні передати запит сервісному об'єкту.

  4. Подумайте про введення фабрики, яка б вирішувала, який з об'єктів створювати: заступника або реальний сервісний об'єкт. Проте, з іншого боку, ця логіка може бути вкладена до створюючого методу самого заступника.

  5. Подумайте, чи не реалізувати вам ліниву ініціалізацію сервісного об'єкта при першому зверненні клієнта до методів заступника.

Переваги та недоліки

  • Дозволяє контролювати сервісний об'єкт непомітно для клієнта.
  • Може працювати, навіть якщо сервісний об'єкт ще не створено.
  • Може контролювати життєвий цикл службового об'єкта.
  • Ускладнює код програми внаслідок введення додаткових класів.
  • Збільшує час отримання відклику від сервісу.

Відносини з іншими патернами

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

  • Фасад схожий на Заступник тим, що замінює складну підсистему та може сам її ініціалізувати. Але, на відміну від Фасаду, Заступник має такий самий інтерфейс, що і його службовий об'єкт, завдяки чому їх можна взаємозаміняти.

  • Декоратор та Заступник мають схожі структури, але різні призначення. Вони схожі тим, що обидва побудовані на композиції та делегуванні роботи іншому об'єкту. Патерни відрізняються тим, що Заступник сам керує життям сервісного об'єкта, а обгортання Декораторів контролюється клієнтом.

Реалізація на різних мовах програмування

Заступник Java Заступник C# Заступник PHP