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

707c99dbeb96a1c8dd3c9d9bbac9f320.png

Паттерн «Одиночка» (Singleton) является одним из паттернов проектирования, который используется для создания класса, имеющего только один экземпляр в системе, и предоставляющего глобальную точку доступа к этому экземпляру. Это означает, что в рамках приложения может существовать только один объект данного класса, и любой запрос на создание нового экземпляра будет возвращать ссылку на существующий.

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

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

Основные принципы паттерна

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

Создадим простой Singleton класс и рассмотрим его основные элементы:

class Singleton:
    _instance = None  # Приватное поле для хранения единственного экземпляра

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

Коротко про каждую строчку (более подробно о каждой будет ниже):

  1. class Singleton: — Это определение класса Singleton.

  2. _instance = None — Это приватное поле класса, которое будет хранить единственный экземпляр класса. Изначально оно устанавливается в None, что означает, что экземпляр еще не создан.

  3. def __new__(cls): — Это метод __new__, который переопределяется в классе Singleton. Метод __new__ вызывается при создании нового объекта. Мы переопределяем его, чтобы контролировать создание экземпляров.

  4. if cls._instance is None: — Это проверка наличия существующего экземпляра класса. Если _instance равно None, это означает, что экземпляр еще не создан, и мы можем создать новый экземпляр.

  5. cls._instance = super(Singleton, cls).__new__(cls) — Здесь мы создаем новый экземпляр класса, если он еще не существует. Мы используем функцию super() для вызова базовой реализации метода __new__, которая фактически создает новый экземпляр класса.

  6. return cls._instance — Мы возвращаем существующий или только что созданный экземпляр класса. Это дает уверенность в том, что всегда будет использоваться только один экземпляр класса.

Уникальность экземпляря

Когда у вас есть только один экземпляр класса, вы можете предоставить глобальную точку доступа к этому объекту. Это означает, что любая часть вашего приложения может обратиться к этому экземпляру, не зная о его создании или управлении.

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

Реализация уникальности экземпляра:

Для гарантированного существования только одного экземпляра класса конструктор этого класса должен быть сделан приватным. Это означает, что нельзя будет создать экземпляр класса извне.

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

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

Если несколько потоков попытаются создать экземпляр одновременно, это может привести к созданию нескольких экземпляров. Для обеспечения потокобезопасности можно использовать синхронизацию или дабл чек при создании экземпляра.

Глобальная точка доступа

В первую очередь, чтобы обеспечить глобальную точку доступа к объекту, конструктор класса Singleton делается приватным (private). Это означает, что нельзя будет создать экземпляр класса извне класса.

Для получения доступа к этому единственному экземпляру класса, создается статический метод (например, getInstance()). Этот метод контролирует создание и возврат экземпляра класса:

public class Singleton {
    private static Singleton instance;
    private Singleton() {} 

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

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

Простой подход к синхронизации метода getInstance() может выглядеть так:

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Код использует ключевое слово synchronized, чтобы обеспечить атомарную и потокобезопасную инициализацию Singleton. Однако синхронизация может снижать производительность, и существуют более эффективные способы обеспечения потокобезопасности.

Когда глобальная точка доступа реализована, любая часть вашего приложения может получить доступ к экземпляру Singleton, вызывая метод getInstance():

Singleton singleton = Singleton.getInstance();

Этот объект Singleton можно использовать для хранения глобальных настроек, управления ресурсами или предоставления служб и функциональности, доступной из любой точки приложения.

Ленивая инициализация — это важный аспект реализации паттерна «Одиночка» (Singleton) с технической точки зрения. Этот принцип гарантирует, что экземпляр класса Singleton будет создан только при первом запросе на него, что может быть весьма полезным для оптимизации ресурсов и ускорения инициализации вашего приложения.

Ленивая инициализация

Для создания единственного экземпляра класса и предоставления доступа к нему, создается статический метод (например, getInstance()). Этот метод будет ответственным за создание экземпляра класса, но только при его первом вызове:

public class Singleton {
    private static Singleton instance; 
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); //создание экземпляра при первом обращении
        }
        return instance;
    }
}

В методе getInstance() добавляется проверка: если экземпляр еще не создан (instance == null), то он создается. Важно заметить, что при последующих вызовах getInstance(), уже существующий экземпляр будет возвращен, и новый экземпляр не будет создаваться.

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

Приложение будет инициализироватьс быстрее, так как экземпляр Singleton создается только при реальной необходимости, а не при запуске программы.

Обработка многопоточности

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

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

Самый простой способ обеспечить потокобезопасность — использовать ключевое слово synchronized, мы его использовали чуть выше. Это означает, что только один поток может одновременно выполнять код, защищенный синхронизацией.

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Однако синхронизация может снижать производительность.

Double-Checked Locking»(DCL): этот подход позволяет избежать синхронизации при каждом вызове getInstance(). Он предполагает двойную проверку: сначала проверка без синхронизации, а затем с синхронизацией, только если экземпляр не был создан. Этот метод более эффективен:

public class Singleton {
    private static volatile Singleton instance; // Волатильность для корректной работы DCL
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Некоторые япы предоставляют встроенные механизмы для обеспечения безопасности в многопоточной среде. В джаве, например, можно использовать класс java.util.concurrent.atomic.AtomicReference для ленивой инициализации синглтона:

public class Singleton {
    private static final AtomicReference instance = new AtomicReference<>();
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance.get() == null) {
            instance.compareAndSet(null, new Singleton());
        }
        return instance.get();
    }
}

Сценарии, когда стоит использовать альтернативные подходы

Думаю, что всем понятно, что паттерны имееют свои минусы. И возможно, какие то минусы в паттерне нашего дня вы уже заметили.

Паттерн «одиночка» полезен во многих случаях, но существуют сценарии, когда стоит рассмотреть алтернативу:

  1. Изменение глобального состояния:

    Если ваше приложение изменяет глобальное состояние слишком часто или слишком интенсивно, это может сделать код сложным и трудноподдерживаемым.

    Вместо использования Singleton для хранения глобального состояния, можно рассмотреть использование инъекции зависимостей (Dependency Injection) и передачу состояния через параметры функций и методов. Сделает код более предсказуемым.

  2. Множественные экземпляры для тестирования:

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

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

  3. Недостаток потокобезопасности:

    Если ваша реализация Singleton не обеспечивает потокобезопасность и вы сталкиваетесь с проблемами с многопоточностью.

    Можно использовать более сложные механизмы для обеспечения потокобезопасности, такие как блокировки (locks) или использование классов из библиотеки threading. Кстати, можно еще использовать пул объектов (object pooling) для ресурсоемких операций.

  4. Сложная иерархия зависимостей:

    Если ваш класс Singleton имеет сложную иерархию зависимостей с другими классами, что делает его создание и управление сложным.

    Здесь более уместно применение паттернов внедрения зависимостей и инверсии управления.

    Еще Dagger в Python, может значительно упростить управление зависимостями и сделать код более гибким.

  5. Ленивая инициализация не требуется:

    Если создание экземпляра класса не требует значительных ресурсов и не влияет на производительность приложения.

    В этом случае можно просто создавать экземпляры класса по мере необходимости, без использования Singleton. Это уменьшит сложность кода и избежит излишней оптимизации.

Заключение

Существование только одного экземпляра класса одиночки и обеспечивает глобальную точку доступа к этому экземпляру. Правильное применение паттерна «Одиночка» зависит от специфики вашего проекта и его требований. Умело использованный Singleton может значительно улучшить структуру вашего app.

Разработка ПО является сложным и многогранным процессом, и каждый подход имеет свои сильные и слабые стороны. По устоявшейся традиции хочу порекомендовать вам бесплатный вебинар, на котором вы познакомитесь с несколькими основными методологиями разработки ПО, такими как водопадная модель, итеративная разработка, спиральная модель, гибкая разработка и другие. Эксперты OTUS рассмотрят каждый подход в отдельности и объяснят, в каких ситуациях их использование может быть наиболее эффективным. Регистрация доступна по ссылке.

© Habrahabr.ru