— Как, и он тоже 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

Есть что дополнить? Оставляйте комментарии! Самые интересные мы добавим в статью, чтобы сделать ее лучше.