Весняний РОЗПРОДАЖ

Посередник

Також відомий як: Intermediary, Controller, Mediator

Суть патерна

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

Патерн Посередник

Проблема

Припустімо, що у вас є діалог створення профілю користувача. Він складається з різноманітних елементів керування: текстових полів, чекбоксів, кнопок.

Безладні зв'язки між елементами інтерфейсу користувача

Безладні зв’язки між елементами інтерфейсу користувача.

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

Код елементів роздутий умовами, які часто змінюються

Код елементів потрібно правити під час зміни кожного діалогу.

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

Рішення

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

У нашому прикладі посередником міг би стати діалог. Імовірно, клас діалогу вже знає, з яких елементів він складається. Тому жодних нових зв’язків додавати до нього не доведеться.

Елементи спілкуються через посередника

Елементи інтерфейсу спілкуються через посередника.

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

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

Таким чином, посередник приховує у собі всі складні зв’язки й залежності між класами окремих компонентів програми. А чим менше зв’язків мають класи, тим простіше їх змінювати, розширювати й повторно використовувати.

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

Приклад з диспетчерською вежею.

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

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

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

Структура

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

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

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

  4. Компоненти не повинні спілкуватися один з одним безпосередньо. Якщо в компоненті відбувається важлива подія, він повинен повідомити свого посередника, а той сам вирішить, чи стосується подія інших компонентів, і чи треба їх сповістити. При цьому компонент-відправник не знає, хто обробить його запит, а компонент-одержувач не знає, хто його надіслав.

Псевдокод

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

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

Приклад структурування класів UI діалогів.

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

Посередник у вигляді діалогу авторизації знає, як конкретні елементи повинні взаємодіяти. Тому при отриманні повідомлень він може перенаправити виклик тому чи іншому елементу.

// Загальний інтерфейс посередників.
interface Mediator is
    method notify(sender: Component, event: string)


// Конкретний посередник. Усі зв'язки між конкретними
// компонентами переїхали до коду посередника. Він отримує
// повідомлення від своїх компонентів та знає, як на них
// реагувати.
class AuthenticationDialog implements Mediator is
    private field title: string
    private field loginOrRegisterChkBx: Checkbox
    private field loginUsername, loginPassword: Textbox
    private field registrationUsername, registrationPassword,
                  registrationEmail: Textbox
    private field okBtn, cancelBtn: Button

    constructor AuthenticationDialog() is
        // Тут потрібно буде створити об'єкти усіх компонентів,
        // подавши поточний об'єкт-посередник до їхніх
        // конструкторів.

    // Коли щось трапляється з компонентом, він надсилає
    // посереднику повідомлення. Після отримання повідомлення
    // посередник може або зробити щось самостійно, або
    // перенаправити запит іншому компонентові.
    method notify(sender, event) is
        if (sender == loginOrRegisterChkBx and event == "check")
            if (loginOrRegisterChkBx.checked)
                title = "Log in"
                // 1. Показати компоненти форми входу.
                // 2. Приховати компоненти форми реєстрації.
            else
                title = "Register"
                // 1. Показати компоненти форми реєстрації.
                // 2. Приховати компоненти форми входу.

        if (sender == okBtn && event == "click")
            if (loginOrRegister.checked)
                // Намагатись знайти користувача з даними із
                // форми логіна.
                if (!found)
                    // Показати помилку над формою логіна.
            else
                // 1. Створити аккаунт користувача з даними
                // форми реєстрації.
                // 2. Авторизувати цього користувача.
                // ...


// Класи компонентів спілкуються з посередниками через їх
// загальний інтерфейс. Завдяки цьому, одні й ті ж компоненти
// можна використовувати в різних посередниках.
class Component is
    field dialog: Mediator

    constructor Component(dialog) is
        this.dialog = dialog

    method click() is
        dialog.notify(this, "click")

    method keypress() is
        dialog.notify(this, "keypress")

// Конкретні компоненти жодним чином не пов'язані між собою. У
// них є тільки один канал спілкування — через надсилання
// повідомлень посереднику.
class Button extends Component is
    // ...

class Textbox extends Component is
    // ...

class Checkbox extends Component is
    method check() is
        dialog.notify(this, "check")
    // ...

Придатність

  • Коли вам складно змінювати деякі класи через те, що вони мають величезну кількість хаотичних зв’язків з іншими класами.

  • Посередник дозволяє розмістити усі ці зв’язки в одному класі. Після цього вам буде легше їх відрефакторити, зробити більш зрозумілими й гнучкими.

  • Коли ви не можете повторно використовувати клас, оскільки він залежить від безлічі інших класів.

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

  • Коли вам доводиться створювати багато підкласів компонентів, щоб використовувати одні й ті самі компоненти в різних контекстах.

  • Якщо раніше зміна відносин в одному компоненті могла призвести до лавини змін в усіх інших компонентах, то тепер вам достатньо створити підклас посередника та змінити в ньому зв’язки між компонентами.

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

  1. Знайдіть групу тісно сплетених класів, де можна отримати деяку користь, відв’язавши деякі один від одного. Наприклад, щоб повторно використовувати їхній код в іншій програмі.

  2. Створіть загальний інтерфейс посередників та опишіть в ньому методи для взаємодії з компонентами. У найпростішому випадку достатньо одного методу для отримання повідомлень від компонентів.

    Цей інтерфейс необхідний, якщо ви хочете повторно використовувати класи компонентів для інших завдань. У цьому випадку все, що потрібно зробити, — це створити новий клас конкретного посередника.

  3. Реалізуйте цей інтерфейс у класі конкретного посередника. Помістіть до нього поля, які міститимуть посилання на всі об’єкти компонентів.

  4. Ви можете піти далі і перемістити код створення компонентів до класу конкретного посередника, перетворивши його на фабрику.

  5. Компоненти теж повинні мати посилання на об’єкт посередника. Зв’язок між ними зручніше всього встановити шляхом подання посередника до параметрів конструктора компонентів.

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

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

  • Усуває залежності між компонентами, дозволяючи використовувати їх повторно.
  • Спрощує взаємодію між компонентами.
  • Централізує керування в одному місці.

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

  • Ланцюжок обов’язків, Команда Посередник та Спостерігач показують різні способи роботи тих, хто надсилає запити, та тих, хто їх отримує:

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

    • Фасад створює спрощений інтерфейс підсистеми, не вносячи в неї жодної додаткової функціональності. Сама підсистема не знає про існування Фасаду. Класи підсистеми спілкуються один з одним безпосередньо.
    • Посередник централізує спілкування між компонентами системи. Компоненти системи знають тільки про існування Посередника, у них немає прямого доступу до інших компонентів.
  • Різниця між Посередником та Спостерігачем не завжди очевидна. Найчастіше вони виступають як конкуренти, але іноді можуть працювати разом.

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

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

    Але Посередник має й інші реалізації, коли окремі компоненти жорстко прив’язані до об’єкта посередника. Такий код навряд чи буде нагадувати Спостерігача, але залишиться Посередником.

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

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

Посередник на C# Посередник на C++ Посередник на Go Посередник на Java Посередник на PHP Посередник на Python Посередник на Ruby Посередник на Rust Посередник на Swift Посередник на TypeScript