Предыдущие выпуски: Вступление, MVC, Стратегия
Если вы читаете про шаблоны (паттерны), то должны уже разбираться в ООП.
Синглтон (Singleton) – шаблон с непростой судьбой. Сначала он был равным среди равных, а потом кто-то сказал, что он, оказывается, плохой, и все дружно стали считать его плохим. Сейчас выясним, почему.
Какая проблема решается
Часто бывает нужно, чтобы какой-либо ресурс существовал в вашей программе только в одном экземпляре. Например, соединение с базой данных.
Для того, чтобы получить этот ресурс, вы пишете что-то вроде
var dbConnection = new DBConnection();
То есть в программе есть класс с названием DBConnection, который содержит в себе методы для работы с базой данных. Вы создали новый объект (экземпляр) класса DBConnection с помощью оператора new.
Но вам никто не мешает написать new DBConnection() ещё и ещё раз, получив таким образом несколько открытых соединений. Это может быть весьма нежелательно. Начиная с того, что ваша программа будет глючить из-за асинхронной обработки данных в разных соединениях, и заканчивая тем, что на сервере, где работает база данных, будет достигнут лимит соединений.
В большинстве случаев вашей программе нужно только лишь одно соединение с базой. Но что делать, если в программе много объектов, и каждый хочет открыть соединение? Он должен как-то проверять, было оно уже открыто другими объектами или нет. В общем, появляется некоторая необустроенность и неуверенность.
Шаблон Синглтон (одиночка) занимается проблемой существования только одного экземпляра класса. Он относится именно к работе с классами. Немного задержимся на этом объяснении.
Вообще говоря, вы можете обеспечить единственность ресурса исключительно силой воли. Например, можно написать ровно один раз
var dbConnection = new DBConnection();
И далее использовать везде переменную dbConnection как единственное соединение с базой. Всё, что требуется – больше нигде и никогда не писать ещё раз new DBConnection(), то есть не создавать ещё один объект этого класса.
И да, вы через это фактически реализовали паттерн Синглтон.
Но тру-Синглтон основан на том, чтобы не дать классу воплотиться в объект ещё раз, даже если попытаться сделать это намеренно. И когда стоит задача "реализовать паттерн Синглтон", речь идёт именно о том, чтобы построить некий специальный класс со специальными свойствами. Поэтому Синглтон – именно про классы.
Как превратить DBConnection в синглтон?
Классический способ сделать из класса синглтон – это сделать его конструктор приватным (повторюсь, если вы это читаете, то должны ориентироваться в ООП).
Когда у класса приватный конктруктор, вы не можете написать new DBConnection(), потому что оператор new это фактически вызов конструктора класса, но вызвать его может только сам класс, потому что он приватный, да.
Частично проблема решена – раз нельзя написать new DBConnection(), то очевидно, что нельзя создать экземпляр класса ни много раз, ни вообще ни разу. Но ведь надо как-то создать хотя бы один экземпляр?
Для этого у класса должен быть статический метод, который создаст один экземпляр. Как мы знаем, статические методы могут вызываться БЕЗ создания экземпляра класса, то есть без new. Что это будет за метод? В рамках шаблона Синглтон его обычно называют getInstance(), то есть "получить экземпляр".
Дальше будут примеры на PHP, потому что у него статические вызовы пишутся явно, так нагляднее:
$dbConnection = DBConnection::getInstance();
Теперь мы получили соединение с базой не с помощью new DBConnection(), а с помощью статического метода getInstance() класса DBConnection.
Собственно, этот метод использует тот же самый new, чтобы вернуть нам тот же самый экземпляр класса, как и прежде. Но в чём фишка? Если раньше мы могли создать несколько экземпляров класса, используя new, то теперь, спрятав new внутрь getInstance(), мы всё равно можем несколько раз вызвать getInstance(), и значит всё равно получим несколько экземпляров класса. За что боролись?
Метод getInstance() нужно дописать. В класс надо добавить опять же приватное свойство, которое не видит никто, кроме самого класса. Это свойство должно быть тоже статическим, чтобы существовать без создания экземпляра класса:
Метод getInstance() обрастает простейшей логикой. Если свойство instance пустое, то создаётся новый экземпляр класса и записывается в это свойство. Если же свойство не пустое, это значит, что экземпляр класса УЖЕ создан, и повторно создавать его не надо. Надо просто вернуть тот, который есть.
Таким образом, мы можем сколько угодно раз и из каких угодно мест обращаться к методу getInstance(), но будет создан только один экземпляр класса, и этот экземпляр будет возвращаться всем, кто его запрашивает.
Мы получили полную реализацию Синглтона.
Проблемы реализации
Не во всех языках есть приватные свойства и статические методы. Поэтому приходится выкручиваться. Например, если конструктор класса нельзя сделать приватным, то в этот конструктор вставляют некий секретный параметр, не зная который, нельзя просто вызвать new ...
Также класс можно сделать абстрактным и т.д. О способах реализации синглтонов в разных языках вы можете посмотреть в Википедии, ну или мы вернемся к особенно замороченным случаям в следующих выпусках.
А пока разберёмся,
Почему Синглтон плохой?
Сначала придумали, потом стали ругать. Странно? Дело в том, что нужно всегда знать контекст, в котором ругают шаблон. Если вы просто слушаете каких-то компьютерных гуру, которые вещают, что такое хорошо, а что такое плохо, а затем массы подхватывают и разносят это, то задайтесь вопросом: а чем занимаются эти гуру, где они работают? Одно дело – когда вы пишете программу в тысячу строк. Другое дело – проект из миллионов файлов. Третье дело – покрытие этого проекта тестами. Четвертое дело – его поддержка, сколько программистов над ним работают, сколько уволилось, сколько новых пришло, и т.д. Пятое дело – речь вообще о Гугле, Фейсбуке, Амазоне или о чём?
Короче говоря, основная проблема, которая делает Синглтон плохим – это его неправильное использование. А разве можно ожидать чего-то хорошего, неправильно используя, скажем, молоток? Молоток теперь плохой?
Ладно, а в чём именно заключается неправильное использование синглтона?
Вы создаёте в программе глобальную зависимость.
Подобно тому, как в игре Journey вдали всегда видна гора с сияющей вершиной, синглтон виден из любого места программы, и чтобы получить к нему доступ, вы везде пишете (например): DBConnection::getInstance(). Тем самым вы жёстко связываете все свои компоненты с классом DBConnection.
Вы создаёте зависимость, а зависимость в многокомпонентной среде это плохо, но не обязательно именно для вас (помните про контекст). Эти проблемы решаются с помощью других паттернов, которые обсудим в следующих выпусках.
Читайте дальше: Шаблон проектирования "Фабрика"