Autumn SALE

Замісник

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

Суть патерна

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

Патерн Замісник

Проблема

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

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

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

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

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

Рішення

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

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

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

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

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

Платіжна картка та готівка

Платіжною карткою можна розраховуватися так само, як і готівкою.

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

Структура

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

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

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

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

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

Псевдокод

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

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

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

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

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

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

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

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

// З іншого боку, можна кешувати запити до YouTube і не
// повторювати їх деякий час, доки кеш не застаріє. Але внести
// цей код безпосередньо в сервісний клас неможливо, бо він
// знаходиться у сторонній бібліотеці. Тому ми помістимо логіку
// кешування в окремий клас-обгортку. Він буде делегувати запити
// сервісному об'єкту, тільки якщо потрібно безпосередньо
// відіслати запит.
class CachedYouTubeClass implements ThirdPartyYouTubeLib is
    private field service: ThirdPartyYouTubeLib
    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(id) is
        info = service.getVideoInfo(id)
        // Відобразити сторінку відеоролика.

    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. Подумайте, чи не реалізувати вам ліниву ініціалізацію сервісного об’єкта при першому зверненні клієнта до методів замісника.

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

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

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

  • З Адаптером ви отримуєте доступ до існуючого об’єкта через інший інтерфейс. Використовуючи Замісник, інтерфейс залишається незмінним. Використовуючи Декоратор, ви отримуєте доступ до об’єкта через розширений інтерфейс.

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

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

Приклади реалізації патерна

Замісник на C# Замісник на C++ Замісник на Go Замісник на Java Замісник на PHP Замісник на Python Замісник на Ruby Замісник на Rust Замісник на Swift Замісник на TypeScript