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

Одинак

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

Суть патерна

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

Патерн Одинак

Проблема

Одинак вирішує відразу дві проблеми (порушуючи принцип єдиного обов’язку класу):

  1. Гарантує наявність єдиного екземпляра класу. Найчастіше за все це корисно для доступу до якогось спільного ресурсу, наприклад, бази даних.

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

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

Глобальний доступ до одного об'єкта

Клієнти можуть не підозрювати, що працюють з одним і тим самим об’єктом.

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

    Проте, є ще одна особливість. Було б непогано й зберігати в одному місці код, який вирішує проблему №1, і мати до нього простий та доступний інтерфейс.

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

Рішення

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

Якщо у вас є доступ до класу одинака, отже, буде й доступ до цього статичного методу. З якої точки коду ви б його не викликали, він завжди віддаватиме один і той самий об’єкт.

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

Уряд держави — вдалий приклад Одинака. У державі може бути тільки один офіційний уряд. Незалежно від того, хто конкретно засідає в уряді, він має глобальну точку доступу «Уряд країни N».

Структура

Структура класів патерна ОдинакСтруктура класів патерна Одинак
  1. Одинак визначає статичний метод getInstance, який повертає один екземпляр свого класу.

    Конструктор Одинака повинен бути прихований від клієнтів. Виклик методу getInstance повинен стати єдиним способом отримати об’єкт цього класу.

Псевдокод

У цьому прикладі роль Одинака грає клас підключення до бази даних.

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

// Клас одинака визначає статичний метод `getInstance`, котрий
// дозволяє клієнтам повторно використовувати одне і теж
// підключення до бази даних по всій програмі.
class Database is
    // Поле для зберігання об'єкта-одинака має бути оголошено
    // статичним.
    private static field instance: Database

    // Конструктор одинака завжди повинен залишатися приватним,
    // аби клієнти не могли самостійно створювати екземпляри
    // цього класу через оператор `new`.
    private constructor Database() is
        // Тут може жити код ініціалізації підключення до
        // сервера баз даних.
        // ...

    // Головний статичний метод одинака служить альтернативою
    // конструктору і є точкою доступу до екземпляра цього
    // класу.
    public static method getInstance() is
        if (Database.instance == null) then
            acquireThreadLock() and then
                // Про всяк випадок, ще раз перевіримо, чи не
                // було створено об'єкт в іншому потоці, поки
                // даний потік чекав на звільнення блокування.
                if (Database.instance == null) then
                    Database.instance = new Database()
        return Database.instance

    // І, нарешті, будь-який клас одинака повинен мати якусь
    // корисну функціональність, яку клієнти будуть запускати
    // через отриманий об'єкт одинака.
    public method query(sql) is
        // Усі запити до бази даних проходитимуть через цей
        // метод. Тому є сенс помістити сюди якусь логіку
        // кешування.
        // ...

class Application is
    method main() is
        Database foo = Database.getInstance()
        foo.query("SELECT ...")
        // ...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        // Змінна "bar" містить той самий об'єкт, що і змінна
        // "foo".

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

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

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

Коли ви хочете мати більше контролю над глобальними змінними.

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

Тим не менше, будь-коли ви можете розширити це обмеження і дозволити будь-яку кількість об’єктів-одинаків, змінивши код в одному місці (метод getInstance).

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

  1. Додайте до класу приватне статичне поле, котре міститиме одиночний об’єкт.

  2. Оголосіть статичний створюючий метод, що використовуватиметься для отримання Одинака.

  3. Додайте «ліниву ініціалізацію» (створення об’єкта під час першого виклику методу) до створюючого методу одинака.

  4. Зробіть конструктор класу приватним.

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

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

  • Гарантує наявність єдиного екземпляра класу.
  • Надає глобальну точку доступу до нього.
  • Реалізує відкладену ініціалізацію об’єкта-одинака.
  • Порушує принцип єдиного обов’язку класу.
  • Маскує поганий дизайн.
  • Проблеми багатопоточності.
  • Вимагає постійного створення Mock-об’єктів при юніт-тестуванні.

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

  • Фасад можна зробити Одинаком, оскільки зазвичай потрібен тільки один об’єкт-фасад.

  • Патерн Легковаговик може нагадувати Одинака, якщо для конкретного завдання ви змогли зменшити кількість об’єктів до одного. Але пам’ятайте, що між патернами є дві суттєві відмінності:

    1. На відміну від Одинака, ви можете мати безліч об’єктів-легковаговиків.
    2. Об’єкти-легковаговики повинні бути незмінними, тоді як об’єкт-одинак допускає зміну свого стану.
  • Абстрактна фабрика, Будівельник та Прототип можуть реалізовуватися за допомогою Одинака.

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

Одинак на C# Одинак на C++ Одинак на Go Одинак на Java Одинак на PHP Одинак на Python Одинак на Ruby Одинак на Rust Одинак на Swift Одинак на TypeScript