Паттерн "Одиночка" (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<Singleton> 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 рассмотрят каждый подход в отдельности и объяснят, в каких ситуациях их использование может быть наиболее эффективным. Регистрация доступна по ссылке.

Комментарии (22)


  1. nronnie
    14.12.2023 06:37

    Вам никто еще не говорил, что singleton давно считается антипаттерном? В интернете, по-моему, про это уже на каждом заборе написано.


    1. panzerfaust
      14.12.2023 06:37

      про это уже на каждом заборе написано

      И ценность этого примерно та же, что и у заборной писанины. Стейтлесс-синглтоны - основа архитектуры целой кучи популярных бэкенд фремворков. Мне вот может кто-то объяснить, чем станут лучше Spring или Vert.x, если из них убрать синглтоны? Антипаттерном как правило являются попытки писать синглтон руками вместо того, чтобы использовать проверенные решения.


      1. pda0
        14.12.2023 06:37

        Тогда вместо борьбы стоит возглавлять. :) Например, выделить реализацию синглтона в отдельный обобщённый класс. Или базовый с ручным инстанцированием в языках без генериков.

        Т.е. весь рабочий код сосредоточен в обычном классе, который оказывается лишь доступен через контейнер-одиночки. Это решило бы часть претензий к шаблону.

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

        Для тестирования кода, зависящего от одиночки - подсовывать mock-обхект.


    1. SadOcean
      14.12.2023 06:37

      А вам еще никто не говорил, почему он считается антипаттерном но и паттерном одновременно?
      А автор, меж тем, прекрасно это объяснил и привел границы применимости.

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


      1. nronnie
        14.12.2023 06:37

        Само по себе наличие / использование глобального для приложения объекта антипаттерном не является (потому что, конечно, есть сценарии где это нужно). Антипаттерном является вариант реализации этого, который, наверное, со времен GoF гуляет по миру и теперь попал сюда с этой статьёй. Если все это делать через DI и DI-контейнер, то всё будет вполне нормально.


    1. Deirel
      14.12.2023 06:37

      Вспомнился мысленный эксперимент с обезьяной, лестницей и бананами.


  1. WondeRu
    14.12.2023 06:37

    Одиночка! - где вы это взяли??? Я в первый раз за 21 год разработки это слышу!


    1. pda0
      14.12.2023 06:37

      Везде. Во многих местах встречал именно такой перевод.


  1. NeoNN
    14.12.2023 06:37

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


  1. rezdm
    14.12.2023 06:37

    Я начал работать разработчиком в конце 90-ых, плюс до этого обычное увлечение, три страны, десяток работодателей, но впервые слышу "одиночка" для синглтона.


    1. nronnie
      14.12.2023 06:37

      https://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования)

      В переводе GoF тоже использовался термин "одиночка" - у меня сейчас нет книги под рукой, поэтому прув дать не могу, но можете поискать сами.


    1. nronnie
      14.12.2023 06:37

      Впрочем вот:

      Это из книжки GoF


  1. Alohahwi
    14.12.2023 06:37

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


    1. rukhi7
      14.12.2023 06:37

      а разве синглтоны перестают быть синглтонами если для них определить последовательность инициализации?


      1. Alohahwi
        14.12.2023 06:37

        Не перестают, но синглтон это автоматически плюс один if при каждом обращении к объекту. Если проинициализировать объекты самому, то все эти if-ы можно убрать.


        1. rukhi7
          14.12.2023 06:37

          Это да! А еще если их проинициализировать заведомо до запуска основного кода приложения - до перехода к функции MAIN(), ну и в правильном порядке как вы и написали,то и не придется статью каждый месяц новую писать про синглтоны и их потокобезапасность и т. д. и т. п.

          В связи с этим у меня вопрос, почему лучшие умы не рассмотрят такую возможность?

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


        1. SadOcean
          14.12.2023 06:37

          Простите, а какой if ?


          1. Alohahwi
            14.12.2023 06:37

            Который проверяет, существует ли уже объект к которому идет обращение


            1. tuxi
              14.12.2023 06:37

              можно же через холдер cделать

              public class SingletonImpl {
              
                private static class Holder {
                  private static final SingletonImpl HOLDER_INSTANCE = new SingletonImpl();
                }
              
                public static SingletonImpl getInstance() {
                  return Holder.HOLDER_INSTANCE;
                }
              
                private SingletonImpl() {}
                
                public void doSomething() {}
                
              }
              


            1. SadOcean
              14.12.2023 06:37

              Ok

              Я думал речь о клиентах синглтона а не об инициализации.


          1. Chelovechek1311
            14.12.2023 06:37

            Который проверяет, создан ли инстанс класса в приватном поле


            1. SadOcean
              14.12.2023 06:37

              Думал речь о клиенте.
              Для самого синглтона то можно и обобщить.