— Как, и он тоже singleton? А я думала, что он нормальный!
— Сколько раз тебе повторять, что слова singleton, mediator, decorator и даже prototype никакого отношения к ориентации кода не имеют?
(разговор вызова курильщика и вызова нормального программиста)
Всем привет, я Максим Кравец, CEO команды веб-инженеров Holyweb, адептов JavaScript. И сегодня хочу поговорить о паттернах проектирования.
Давайте взглянем на маленький кусочек кода:
@Injectable()
export class SomeClass implements SomeInterface {
private currentData: Data
setData() {
this.currentData = new Data()
}
Попробуйте найти разработчика, который прочитает этот фрагмент следующим образом: «Мы воспользуемся структурным паттерном decorator для обеспечения возможности внедрения методов нашего SomeClass в другие классы посредством Dependency Injection, в методе setData нашего класса применим порождающий паттерн Builder посредством new и в приватное поле currentData положим...»
Паттерны живут в нашем коде. Паттерны бывают порождающие, структурные, поведенческие. Мы ими пользуемся, даже не акцентируя на этом внимание, а строгие формулировки вспоминаем порой лишь при подготовке к собеседованию. Паттернов у программиста — что жен в гареме султана. И как те самые жены — они не очень любят, когда про них забывают и ими пренебрегают. И тогда наш код начинает делать нам нервы.
Так что сами — вспомним. А тем, кто еще не знает — расскажем. И начнем с паттерна, которому впору присвоить звание «нелюбимой жены султана». Можно даже встретить мнение, что его бездумное использование — безвкусица, дурной тон и вообще антипаттерн. Наш сегодняшний рассказ посвящен синглтону (паттерн Singleton).
Часть первая. Детективная. Ограбление, которого не было
Идея паттерна Singleton очень проста — на все приложение создается один-единственный инстанс класса, и при любом обращении возвращается именно этот инстанс. Техническая реализация паттерна также укладывается в одно предложение описания и одну строку кода:
описание:
Реализация должна скрыть конструктор класса и предоставить публичный статический метод, контролирующий жизненный цикл инстанса
код метода, контролирующего жизненный цикл:
public static getInstance(): SingletonClassName {
!instance ? instance = new SingletonClassName : instance
}
Все это прекрасно, но зачем он нужен вообще? Ведь инстансы придумали не зря! Пусть у каждого компонента нашего приложения будет свой инстанс, которым он волен распоряжаться по своему усмотрению! Окей, давайте представим…
У вас есть семья (приложение), у семьи есть единый банковский счет (база данных) и несколько карт, привязанных к этому счету (инстансов класса, предоставляющего подключение к базе данных). На счету — 100 рублей (данные).
Вы и супруга (или супруг и вы) одновременно помещаете свои карточки в банкоматы и просите снять 100 рублей. Счет одномоментно получает два требования на списание, параллельно проводит две проверки на наличие средств, получает два подтверждения что такая сумма есть и параллельно списывает 100 рублей. Вы забираете из банкоматов каждый по сто рублей, забираете карточки…
Хорошо бы, но нет)) Даже весьма условный пример выше объясняет, почему в один момент времени к базе просто необходимо только одно подключение. Окей, но нас же (компонентов) несколько? Ждать очереди? Давайте еще немного напряжем воображение.
Теперь мы общаемся с банком (базой данных) по телефону. У нас только одна линия, и если кто-то еще попробует позвонить — услышит короткие гудки. Но нас же двое? Поступаем самым очевидным образом — включаем на своей стороне громкую связь! Как итог, линия связи одна (подключение). Телефон на нашей стороне один (инстанс), но пользоваться им могут все (различные компоненты нашего приложения...), находящиеся в комнате с телефоном (...имеющие доступ к инстансу).
Мы только что создали синглтон в отдельно взятой ячейке общества, заодно определившись с тем, когда он нужен и как с ним работать. Ну что ж, давайте познакомимся с ним поближе.
Часть вторая. Романтическая. Продемонстрируем синглтону заинтересованность в нем
Singleton — это порождающий шаблон (паттерн), гарантирующий, что в однопоточном приложении будет только один экземпляр (инстанс) некоего класса, предоставляющий точку доступа к этому экземпляру.
// создадим класс
class DataBase {
// объявим статическое поле для хранения объекта Singleton-а
private static instance
// сделаем приватным конструктор, чтобы никто не имел возможности
// самостоятельно создавать инстансы нашего класса через new
private constructor() {
// здесь мы инициализируем подключение к базе данных
….
// укажем, что инстанса изначально нет
this.instance = null
}
// создадим доступную извне альтернативу конструктору
// для того, чтобы обеспечить точку доступа к инстансу Singleton-а
public static getInstance() {
if ( this.instance === null ) {
// если инстанса нет, создаем его
this.instance = new DataBase()
}
// отдадим инстанс тому, кто запрашивал
return this.instance
}
// для примера создадим метод query для формирования
// запросов к базе данных
public getQuery(payload) {
// здесь реализуем логику запроса
...
}
}
// создадим обращающийся класс
class AppModule {
// метод запроса к базе
Data() {
// объявим два подключения
let foo = DataBase.getInstance()
let bar = DataBase.getInstance()
// foo содержит тот же объект,
// что и bar
foo.getQuery("SELECT ...")
bar.getQuery("SELECT ...")
}
}
Фактически, Singleton предоставляет глобальную точку доступа, но в отличие от простых глобальных переменных, которые также могут использоваться для решения этой задачи, Singleton скрывает от внешнего пользователя методы конструктора и тем самым гарантирует, что никакой сторонний код не сможет подменить данные.
Лучший друг Singletonа — это TypeScript, потому что он дает возможность обращаться через интерфейсы, существенно расширяя базовую функциональность.
Кроме того, несомненным плюсом Singleton является то, что он может быть создан «по запросу» — не при инициализации приложения, а при первом обращении. Однако тут следует быть осторожным и не забывать, что если объект нужен уже при инициализации, он может быть затребован раньше, чем будет создан.
Выбирая Singleton, стоит также помнить, что небрежное использование глобальных объектов может приводить к проблемам масштабируемости, контроля за многопоточностью, написания модульных тестов и в целом следования принципам TTD.
Так что же делать? Да то же, что и со всеми остальными паттернами! Использовать, помня о его несомненных плюсах, но не забывая о недостатках. Тем более, что проблемы с тестированием решаются с помощью методов внедрения зависимостей (DI), а при необходимости увеличить количество инстансов (отойти от паттерна Singleton) потребуется переписать всего один метод, отвечающий за доступ к инстансу.
Часть третья. Жизненная. Если б я был султан, я б имел трех жен?
Подведем итоги. Паттерн Singleton — мощный, а порой просто незаменимый инструмент в руках разработчика. Если использовать его по назначению. Микроскопом, как известно, тоже можно гвозди заколачивать, но вряд ли кто оценит. Так и с Singletonом. Ну а чтобы не запутались, вот вам сводная табличка:
Преимущества | Недостатки |
Гарантирует наличие единственного инстанса класса. | Маскирует плохую архитектуру. |
Реализует отложенную инициализацию. | Нарушает принцип единственной ответственности класса. |
Предоставляет глобальную точку доступа. | Создает проблемы контроля многопоточности. |
Отдельно стоит отметить, что Singleton может быть «включен» в состав других паттернов. Например, фасад нередко выступает в приложении в одиночку и может быть реализован как Singleton. Так же большая часть абстрактных классов при необходимости легко приводятся к виду Singletonа.
В общем, постарайтесь подружиться с этим полезным одиночкой, а мы тем временем подготовим рассказ о следующем паттерне. Впрочем, если вы уже считаете себя магистром Йодой в разработке или на пути к этому званию, тоже будет здорово познакомиться: пишите в Telegram @maximkravec.
Есть что дополнить? Оставляйте комментарии! Самые интересные мы добавим в статью, чтобы сделать ее лучше.
DmitryKazakov8
Может глупый вопрос, но чем отличается от
export const dataBase = new DataBase()
? При импорте из разных мест придет один и тот же инстанс по правилам ES Imports. И DI соответственноclass { private dataBase = dataBase; }
без контейнера, передающего значение через декоратор.Maxim_from_HW Автор
Вопрос не глупый, а очень даже интересный. В теории, предложенный вариант будет работать, пока забывчивый разработчик не воткнет private dataBase = new dataBase
Чтобы этого не случилось — и закрывают конструктор.
DmitryKazakov8
Сборка упадет и он поправит, это же обычная опечатка. Получается этот паттерн через getInstance неактуален? Ни разу не видел в современных проектах, везде просто готовый инстанс экспортируется
Maxim_from_HW Автор
Пример паттерна — схематичный, для описания принципа создания. Увидеть его реализацию можно, если залезть «под капот», например, Apollo или TypeORM. А «снаружи» да, просто подключение через DI.
В том и суть — мы часто пользуемся паттернами, зная о них, но воспринимая поведение как должное. Машина ездит и хорошо, красная такая. А сколько колец на клапане обжимных — современный водитель не в курсе)
Alexandroppolus
Это уже не DI :)
За DI на самом деле стоит SRP, то есть класс не занимается резолвом зависимостей самостоятельно — это не его задача. А синглтон может и перестать быть синглтоном.
DmitryKazakov8
Спасибо за пояснение, я в DI не хорош — видел реализации в проектах, но смысла для фронтенда так и не понял. Предпочитаю делать слои экшенов, апи и сайд-эффектов отдельно от хранилищ, с доступом к глобальному стору.