singleton для чего нужен
Singleton (Одиночка) или статический класс?
Статья будет полезна в первую очередь разработчикам, которые теряются на собеседованиях когда слышат вопрос «Назовите основные отличия синглтона от статического класса, и когда следует использовать один, а когда другой?». И безусловно будет полезна для тех разработчиков, которые при слове «паттерн» впадают в уныние или просят прекратить выражаться 🙂
Что такое статический класс?
Что такое Singleton (Одиночка)?
Один из порождающих паттернов, впервые описанный «бандой четырех» (GoF). Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Мы не будем подробно рассматривать здесь этот паттерн, его предназначение и решаемые им задачи — в сети существует масса подробной информации о нем (например здесь и здесь). Отмечу лишь что синглтоны бывают потокобезопасные и нет, с простой и отложенной инициализацией.
А если нет разницы — зачем плодить больше?
Так в чем же все-таки разница между этими двумя сущностями и когда следует их использовать? Думаю что лучше всего это проиллюстрировать в следующей таблице:
Singleton | Static class | |
---|---|---|
Количество точек доступа | Одна (и только одна) точка доступа — статическое поле Instance | N (зависит от количества публичных членов класса и методов) |
Наследование классов | Возможно, но не всегда (об этом — ниже) | Невозможно — статические классы не могут быть экземплярными, поскольку нельзя создавать экземпляры объекты статических классов |
Наследование интерфейсов | Возможно, безо всяких ограничений | Невозможно по той же причине, по которой невозможно наследование классов |
Возможность передачи в качестве параметров | Возможно, поскольку Singleton предоставляет реальный объект | Отсутствует |
Контроль времени жизни объекта | Возможно — например, отложенная инициализация (или создание по требованию) | Невозможно по той же причине, по которой невозможно наследование классов |
Использование абстрактной фабрики для создания экземпляра класса | Возможно | Невозможно по причине осутствия самой возможности создания экземпляра |
Сериализация | Возможно | Неприменима по причине отсутствия экземпляра |
Рассмотрим подробнее перечисленные выше критерии.
Количество точек доступа
Конечно же имеются ввиду внешние точки доступа, другими словами — публичный контракт взаимодействия класса и его клиентов. Это удобнее проиллюстрировать с помощью кода:
Singleton в «канонической» реализации:
Наследование классов
С наследованием статических классов все просто — оно просто не поддерживается на уровне языка. С Singleton все несколько сложнее. Для удобства использования многие разработчики чаще всего используют следующую реализацию паттерна:
А поскольку множественное наследование в C# и в любом CLI-совместимом языке запрещено — это означает что мы не сможем унаследовать класс Session от любого другого полезного класса. Выходом является делагирование синглтону управления доступом к экземпляру объекта:
Наследование интерфейсов
Использование интерфейсов позволяет достичь большей гибкости, увеличить количество повторно используемого кода, повысить тестируемость, и, самое главное — избежать сильной связности объектов. Статические классы не поддерживают наследования в принципе. Синглтон, напротив, наследование интерфейсов поддерживает в полной мере, поскольку это обычный класс. Но вот использовать эту возможность стоит только в том случае, если экземпляр синглтона планируется передавать в качестве входных параметров в смешанных сценариях или транслировать за границу домена. Пример смешанного сценария:
Возможность передачи в качестве параметров
Для статических классов это не поддерживается — можно передать разве что тип, но в большинстве ситуаций это бесполезно, за исключением случаев применения механизмов отражения (reflection). Синглтон же по сути является обычным экземпляром объекта:
Контроль времени жизни объекта
Время жизни статического класса ограничено временем жизни домена — если мы создали этот домен вручную, то мы косвенно управляем временем жизни всех его статических типов. Временем жизни синглтона мы можем управлять по нашему желанию. Яркий пример — отложенная инициализация:
Можно также добавить операцию удаления экземпляра синглтона:
Данная операция является крайне небезопасной, поскольку синглтон может хранить некоторое состояние и поэтому его пересоздание может иметь нежелательные последствия для его клиентов. Если все же необходимость в таком методе возникла (что скорее всего указывает на ошибки проектирования) то нужно постараться свести к минимуму возможное зло от его использования — например сделать его закрытым и вызывать внутри свойства Instance при определенных условиях:
Использование абстрактной фабрики для создания экземпляра класса
Статический класс не поддерживает данной возможности ввиду того, что нельзя создать экземпляр статического класса. В случае с синглтоном все выглядит просто:
Правда в варианте с аггрегацией синглтона придеться применить не совсем красивое и, немного громоздкое решение:
Сериализация
Сериализация применима только к экземплярам классов. Статический класс не может иметь экзмпляров поэтому сериализовать в данном случае нечего.
Так что же использовать Синглтон или Статический класс?
В любом случае выбор решения зависит от разработчика и от специфики решаемой им задачи. Но, в любом случае, можно сделать следующие выводы:
Для чего нужен singleton?
Оценить 1 комментарий
В общем и целом: синглтон как таковой не нужен вообще. Это бред придуманный программистами в пьяном угаре.
Тем же раскладом вы можете создать файл app.class.php где написать
Class App <>
return new App;
и когда подключаете файл в файле сборки сделать:
$app = require_once app.class.php;
Как сказал бы любой яваскриптер пхп-шнику «ребят, мне бы ваши проблемы»
Dauren: банальный пример: у программы есть файл настроек.
Вы создали синглтон, в конструкторе которого читается этот файл, обращаетесь к этому синглтону, когда нужны значения настроек, и через него же их изменяете.
В результате вы уверены, что настройки программы актуальны и консистентны, у них есть удобный интерфейс, и ту часть логики, которая за ним стоит, можно выкинуть из головы и больше о ней не думать.
Почему не статический класс? Потому что этот класс может, например, быть наследником базовой логики и может передаваться как параметр.
Или он может быть членом какого-то класса и будет инициализирован конструктором по умолчанию вместе с этим классом. в общем, есть варианты.
Приложение состоящее ровно из одной UI формы будет иметь глобальное состояние в виде этой формы вне зависимости от того, будет ли эта форма оформлена как синглтон или нет. Просто, если нет, то этой формой приходится управлять иными путями, не всегда более удобными чем синглтон.
В качестве альтернативы синглтонам пропагандируются два других подхода: Registry / Service locator и Dependency injection.
К слову, все альтернативы столь же активно и все так же незаслуженно обзываются антипаттернами. 🙂
Звук, ввод, многопоточный конвеер задач. вспоминать места явной необходимости синглтонов можно еще долго.
Использование паттерна синглтон
Введение
Реализация
Первое, что хотелось бы отметить: синглтон — это реализация, а не интерфейс. Что это значит? Это значит, что класс по возможности должен использовать некий интерфейс, а то, будет там синглтон или нет, это он не знает и знать не должен, т.к. всякое явное использование синглтона и будет приводить к указанным проблемам. На словах выглядит хорошо, давайте посмотрим, как это должно выглядеть в жизни.
Для реализации данной идеи мы воспользуемся мощным подходом, который называется Dependency Injection. Суть его состоит в том, что мы неким образом заливаем реализацию в класс, при этом класс, использующий интерфейс, не заботится о том, кто и когда это будет делать. Его эти вопросы вообще не интересуют. Все, что он должен знать, это как правильно использовать предоставленный функционал. Интерфейс функционала при этом может быть как абстрактный интерфейс, так и конкретный класс. В нашем конкретном случае это неважно.
Идея есть, давайте реализуем на языке C++. Тут нам помогут шаблоны и возможность их специализации. Для начала определим класс, который будет содержать указатель на необходимый экземпляр:
Описанный класс решает несколько задач. Во-первых, он хранит указатель на необходимый экземпляр класса. Во-вторых, при отсутствии экземпляра вызывается функция anFill, которая заполняет нужным экземпляром в случае отсутствия такового (метод reinit). При обращении к классу происходит автоматическая инициализация экземпляром и его вызов. Посмотрим на реализацию функции anFill:
Таким образом по умолчанию данная функция кидает исключение с целью предотвращения использования незадекларированной функции.
Примеры использования
Теперь предположим, что у нас есть класс:
Мы хотим сделать его синглтоном для использования в различных контекстах. Для этого специализируем функцию anFill для нашего класса X:
В данном случае мы использовали простейший синглтон и для наших рассуждений конкретная реализация не имеет значения. Стоит отметить, что данная реализация не является потокобезопасной (вопросы многопоточности будут рассмотрены в другой статье). Теперь мы можем использовать класс X следующим образом:
Что выведет на экран:
При повторном вызове action мы увидим:
Что говорит о том, что у нас сохраняется состояние и экземпляр класса X ровно один. Теперь усложним немного пример. Для этого создадим новый класс Y, который будет содержать использование класса X:
Теперь если мы хотим использовать экземпляр по умолчанию, то нам просто можно сделать следующее:
Что после предыдущих вызовов выведет на экран:
Теперь предположим, что мы захотели использовать другой экземпляр класса. Это сделать очень легко:
Т.е. мы заполняем класс Y нашим (известным) экземпляром и вызываем соответствующую функцию. На экране мы получим:
Разберем теперь случай с абстракными интерфейсами. Создадим абстрактный базовый класс:
Определим 2 различные реализации этого интерфейса:
По умолчанию будем заполнять, используя первую реализацию Impl1:
Таким образом, следующий код:
Создадим класс, использующий наш интерфейс:
Теперь мы хотим поменять реализацию. Тогда делаем следующее:
Что дает в результате:
Развитие идеи
В целом на этом можно было бы закончить. Однако стоит добавить немножко полезных макросов для облегчения жизни:
Многие могут сказать, что макросы — это зло. Ответственно заявляю, что с данным фактом я знаком. Тем не менее, это часть языка и ее можно использовать, к тому же я не подвержен догмам и предрассудкам.
Макрос DECLARE_IMPL декларирует заполнение, отличное от заполнения по умолчанию. Фактически эта строчка говорит о том, что для этого класса будет происходить автоматическое заполнение неким значением в случае отсутствия явной инициализации. Макрос BIND_TO_IMPL_SINGLE будет использоваться в CPP файле для реализации. Он использует функцию single, которая возвращает экземпляр синглтона:
Использование макроса BIND_TO_SELF_SINGLE говорит о том, что для класса будет использоваться экземпляр его самого. Очевидно, что в случае абстракного класса этот макрос неприменим и необходимо использовать BIND_TO_IMPL_SINGLE с заданием реализации класса. Данная реализация может быть скрыта и объявлена только в CPP файле.
Теперь рассмотрим использование уже на конкретном примере, например конфигурации:
Далее можно использовать в других классах:
Выводы
Таким образом описанный подход избавляет от проблем, указанных в начале этой статьи. В последующих статьях я хотел бы затронуть важные вопросы, связанные с временем жизни и многопоточностью.
Паттерны проектирования: Singleton
Что такое синглтон?
Дает гарантию, что у класса будет всего один экземпляр класса.
Предоставляет глобальную точку доступа к экземпляру данного класса.
Приватный конструктор. Ограничивает возможность создания объектов класса за пределами самого класса.
Варианты реализации
Ленивая инициализация: когда класс загружается во время работы приложения именно тогда, когда он нужен.
Простота и прозрачность кода: метрика, конечно, субъективная, но важная.
Потокобезопасность: корректная работа в многопоточной среде.
Высокая производительность в многопоточной среде: потоки блокируют друг друга минимально, либо вообще не блокируют при совместном доступе к ресурсу.
Не ленивая инициализация: когда класс загружается при старте приложения, независимо от того, нужен он или нет (парадокс, в мире IT лучше быть лентяем)
Сложность и плохая читаемость кода. Метрика также субъективная. Будем считать, что если кровь пошла из глаз, реализация так себе.
Отсутствие потокобезопасности. Иными словами, “потокоопасность”. Некорректная работа в многопоточной среде.
Низкая производительность в многопоточной среде: потоки блокируют друг друга все время либо часто, при совместном доступе к ресурсу.
Самая простая реализация. Плюсы:
Простота и прозрачность кода
Высокая производительность в многопоточной среде
Реализация интересна. Мы можем инициализироваться лениво, но утратили потокобезопасность. Не беда: в реализации номер три мы все синхронизируем.
Низкая производительность в многопоточной среде
Отлично! В реализации номер три мы вернули потокобезопасность! Правда, медленную… Теперь метод getInstance синхронизирован, и входить в него можно только по одному. На самом деле нам нужно синхронизировать не весь метод, а лишь ту его часть, в которой мы инициализируем новый объект класса. Но мы не можем просто обернуть в synchronized блок часть, отвечающую за создание нового объекта: это не обеспечит потокобезопасность. Все немного сложнее. Правильный способ синхронизации представлен ниже:
Double Checked Locking
Высокая производительность в многопоточной среде
Не поддерживается на версиях Java ниже 1.5 (в версии 1.5 исправили работу ключевого слова volatile)
Class Holder Singleton
Высокая производительность в многопоточной среде.
Реализация практически идеальная. И ленивая, и потокобезопасная, и быстрая. Но есть нюанс, описанный в минусе. Сравнительная таблица различных реализаций паттерна Singleton:
Реализация | Ленивая инициализация | Потокобезопасность | Скорость работы при многопоточности | Когда использовать? |
---|---|---|---|---|
Simple Solution | — | + | Быстро | Никогда. Либо когда не важна ленивая инициализация. Но лучше никогда. |
Lazy Initialization | + | — | Неприменимо | Всегда, когда не нужна многопоточность |
Synchronized Accessor | + | + | Медленно | Никогда. Либо когда скорость работы при многопоточности не имеет значения. Но лучше никогда |
Double Checked Locking | + | + | Быстро | В редких случаях, когда нужно обрабатывать исключения при создании синглтона. (когда неприменим Class Holder Singleton) |
Class Holder Singleton | + | + | Быстро | Всегда, когда нужна многопоточность и есть гарантия, что объект синглтон класса будет создан без проблем. |
Плюсы и минусы паттерна Singleton
Дает гарантию, что у класса будет всего один экземпляр класса.
Предоставляет глобальную точку доступа к экземпляру данного класса.
Синглтон нарушает SRP (Single Responsibility Principle) — класс синглтона, помимо непосредственных обязанностей, занимается еще и контролированием количества своих экземпляров.
Зависимость обычного класса или метода от синглтона не видна в публичном контракте класса.
Глобальные переменные это плохо. Синглтон превращается в итоге в одну здоровенную глобальную переменную.
Наличие синглтона снижает тестируемость приложения в целом и классов, которые используют синглтон, в частности.
Ну вот и все. Мы рассмотрели с тобой паттерн проектирования синглтон. Теперь в разговоре за жизнь с друзьями программистами ты сможешь сказать не только чем он хорош, но и пару слов о том, чем он плох. Удачи в освоении новых знаний.
[Археология Live] Стыдный разговор о синглтонах
Аудитория: Java Junior, любители холиворов, профессиональные написатели синглтонов
Любые замечания и предложения — очень приветствуются. Это мое первое видео, и не совсем понятно, нужен ли тут вообще такой контент. Считайте это закрытым альфа-тестом, только для посетителей хаба Java 🙂
Ниже дана полная текстовая расшифровка, кому не хочется тратить время на просмотр.
Вступление
Привет, Хабр! Наступил вечер, и нам пора серьезно поговорить. Хочу с тобой обсудить стыдное. Cинглтоны.
Дело в том, что я постоянно слышу от знакомых на попойках, как они на код ревью уничтожают синглтоны и выкашивают спринг, потому что Спринг – шлак. И вот сил моих больше нет терпеть, давайте заясню за всю фигню.
Сначала о том, почему синглтоны – это стыдно.
Старейшая книга в которой говорится о синглтоне (ну по крайней мере, самая старая, какую видел своими глазами) написана в 1994 году. Это книга «Паттерны проектирования» Банды Четырех: Гамма, Хелм, Джонсон, Влиссидес.
Просто задумайтесь, какая это древность. Чем вы занимались в 1994 году? Кое-кто из наших коллег в этом году еще не родился.
Или вот, вторая любовь моей жизни – книга «Test Driven Development» Кента Бека, написанная в 2002 году.
И вот что написано про синглтоны там:
Я рассказываю это не затем, чтобы тряхнуть стариной, а чтобы показать, какой чудовищный баян — вся эта тема с хейтом синглтонов. Синглтоны хейтили, когда ты еще не родился, или учился в начальных классах школы.
Поэтому в принципе, мне кажется, что обсуждать на публике синглтоны – это стыдно. Об этом можно поговорить только с самыми близкими друзьями, со своей любимой женщиной/мужчиной, ну и с тобой, дорогой Хабр.
Светлая сторона
Понятно, что тут все эксперты, и задницей чувствуют опасность синглтонов. Но задница – не лучший индикатор, давайте как-то вербализуем нашу тревогу. Чем конкретно плохи синглтоны?
Глобальное состояние
Синглтон — это в первую очередь глобальное состояние. Вы получаете один неделимый, плохо управляемый глобальный скоуп. Этот недостаток управляемости – сама по себе проблема.
Чтобы как-то это проиллюстрировать, вот представьте что у вас внутри синглтона спряталась какая-то машина состояний или протокол общения. И если вы захотите автоматически тестировать это, то окажется что а) нужно одновременно тестировать всё, что прикасается к синглтону и б) самое ужасное — эти тесты обязаны выполняться в определенном порядке.
Если тесты обязаны выполняться один за другим, из них нельзя выбросить ненужное, то очень быстро мы приходим к ситуации, когда один тестовый прогон делается не менее, чем за 12 часов, и это очень плохо.
Явное и неявное
Существует известный принцип, согласно которому явное лучше неявного. Этот принцип, кстати, заложен в PEP 20, более известный как «дзен языка Python».
Когда ты передаешь объекты не точно по адресу, а переносишь их в глобальное состояние, этим самым ты прячешь связи внутри кода. Это супер неявно. В результате, код быстро превращается в магию. Он как-то работает, но сам по себе, и он уже не в твоей власти.
The Single Responsibility Principle
Принцип единственной ответственности. То есть это буква S в аббревиатуре SOLID. Эта аббревиатура – важнейшая в жизни любого джависта. Я так ее уважаю, что хочу сделать наколку с ней.
Этот принцип когда-то ввел Роберт Мартин (более известный как Дядя Боб).
Он утверждает, что каждый объект должен иметь одну ответственность и эта ответственность должна быть полностью инкапсулирована в класс.
Если мы делаем синглтон, то кроме своих непосредственных обязанностей, он возлагает на себя еще одну задачу: управление своим жизненным циклом. Больше не вызывающий код решает, как и когда появляется объект, не обычное окружение организует доступ к нему, а все это упихано внутрь класса-синглтона. В каком-то смысле он действительно выполняет эту задачу, но криво и убого. Собственно, как и любой код, который старается угнаться за двумя зайцами.
Сильная связанность
Все компоненты, использующие синглтон, или не дай бог – общающиеся через него, мгновенно оказываются жестко связанными. Связанное зачастую приходится развязывать — например, чтобы написать тесты. И, тестировать код, густо обмазанный синглтонами, очень неприятно. Даже один-единственный синглтон может серьезно напрячь вообще всех: и тестировщиков, и программистов с TDD, и подгадить на проведении демонстраций изолированной функциональности.
Специфика Java
В Java нет специального способа записывать синглтон. Поэтому существует множество способов записать его, и все они некрасивые.
Во-первых, все можно впихать в статическое поле или енум.
Но это будет не ленивый вариант, он нам не нужен.
Хотя енум таки можно сделать ленивым, если хочется.
Можно засунуть всё под synchronized:
Но это будут тормоза, кои собственно и символизирует synchronized, в самом типичном случае использования.
Или приходится использовать double-checked locking вариант:
Выглядит он еще более мерзко. Плюс отношение к этой форме записи выразили сами разработчики языка Java:
«There exist a number of common but dubious coding idioms, such as the double-checked locking idiom, that are proposed to allow threads to communicate without synchronization. Almost all such idioms are invalid under the existing semantics, and are expected to remain invalid under the proposed semantics.»
Темная сторона
И как бы, все эти соображения на виду. Но есть и другая, темная сторона вопроса. Существуют другие люди, более практически настроенные. У них есть собственный стандартный ответ: разработчики, упарывающиеся по Солиду, по принципу Лисков итп – это просто теоретики. В реальной жизни все не так, и на самом деле синглтоны нужны. И для этого у них есть специальная философия. Контр-философия.
Глобальное состояние есть везде
Сложно придумать систему, не являющуюся хэлловорлдом или теоретической иллюстрацией, где в самой предметной области не встречалось бы глобальное состояние.
Как минимум, глобальная переменная – это наш реальный мир, в котором мы все живем. Если у нас какая-то компьютерная игра, MMORPG, мы ее пишем, то единый виртуальный мир игры – это тоже глобальное состояние. У нас не будет другой жизни и другого мира.
Более локально, если у вас в системе есть единственная база данных, и это часть предметной области, то зачастую имеет смысл привязаться к её единственности. Иначе на решение прикладной задачи не останется времени, и все начнут писать систему управления бесконечным количеством баз данных.
Аргумент про порядок тестов тут тоже не сработает: очевидно, что при исполнении, первой должна подниматься наша единственная база (и выполняться тесты относительно базы), и потом уже прикладной код. Если поднять код раньше базы, то все упадет. Большинство проектов, которые я видел, явным образом запускали базу до кода, и не видели в этом вообще никаких практических проблем.
«It just works» лучше явного
Современные программные проекты необычайно сложны. Может быть, это самые структурно сложные вещи, из всех, которые делало человечество за всю свою историю.
И рано или поздно, в вашем проекте оказываются тысячи классов, сотни и тысячи зависимостей на внешние библиотеки. Начиная с какого-то момента, ты не можешь управлять ими вручную, появляются умные системы сборки типа Maven. Maven появился, насколько помню, от ужаса, с которым встретились разработчики при сборке проектов на веб-фреймворке Turbine.
Рано или поздно ты понимаешь, что система становится умонепостигаемой. Ее сложность слишком велика, чтобы поместиться в мозг одному человеку, пусть это даже самый главный важный архитектор. И начинаешь искать такую автоматику, которая будет сама думать о внутренних связях, об индикации и сопровождении ошибок, об автоматическом dataflow, и так далее.
Очевидно, что такая система в значительной степени состоит из магии. И в данном случае, магия – это не то, от чего стоит бежать. Наоборот, к ней стоит стремиться. Но это должна быть хорошая, качественная, надежная магия.
Жесткое лучше мягкого
Если проект большой, и в нем сложно разобраться, то прямые жесткие связи упрощают понимание кода. В случае ошибки, в стектрейсе будет именно то место, где она возникла. Открываются возможности по построению очень простых графиков, отражающих структуру кода. Сравните это с действительно слабой связанностью кода на основе очередей, когда вылетевшую в одном месте системы ошибку на самом деле нужно искать на другой стороне очереди, или нескольких очередей – такие поиски могут занимать часы.
Что касается тестирования. Если посмотреть, как это в реальности, то во многих проектах тестирование либо вообще не делают, либо делают в очень урезанном виде. В проекте может быть тысячи классов, но всего несколько десятков тестов. И ради этого крошечного количества вообще не имеет смысла беспокоиться, и переписывать систему особым образом. Это касается не только синглтонов, а вообще всего – настоящее полномасштабное тестирование тянет за собой очень много дополнительных условий. На практике ставить во главу угла тестируемость – не всегда хорошая идея.
Специфика Java
Да, написание синглтона выглядит некрасиво. Но в IDE всегда можно создать шаблон «некрасивого» класса, и навсегда забыть о том, чтобы писать этот неприятный код вручную. Как говорится, темнота – друг молодежи.
Решение
И вот, мы оказались на пороге реального конфликта: одни люди хотят синглтоны (на самом деле, просто глобальное состояние, просто они его называют синглтоном), а другие – наоборот сильно против этого.
Отличным решением является переход от настоящих синглтонов к сиглтонам курильщика… ой ой. Singleton Beans из Spring. В чем суть: с помощью аннотации Component и Scope(SCOPE_SINGLETON) вы помечаете некоторые классы как синглтоны.
Потом в любом месте вы можете сделать поле, пометить его как @Autowired, и при старте приложения в этом поле окажется нужный экземпляр.
Заметьте, что этот вариант решает все перечисленные выше проблемы.
Резюме
Вопрос синглтонов давным-давно решен. Удвительно, что находятся люди, которые об этом еще не знают. Решение — это Spring и другие системы инверсии контроля и депенденси инжекшена. Синглтоны не нужно выкашивать, не нужно хетить людей, которые их пишут — их нужно переводить на Spring и обращать в нашу Spring-религию.