Реализация Одиночки в JS

Как не облажаться на интервью

Тема, вроде бы, изъезженная. Кто-то реально на практике применяет понимая что это и зачем. Кто-то применяет, но не знает что это известный шаблон проектирования и он так называется. У кого-то спрашивают про это на собеседованиях.

С учетом особенностей JS различные варианты из ООП в нем могут быть очень даже нестандартными. Я сам иногда на собеседовании прошу написать Singleton у кандидата, только в случае если он совершил следующие действия:

  1. сказал что знает ООП и шаблоны проектирования
  2. сказал, что из всех паттернов знает Singleton

Не хочу поднимать сейчас вопрос о необходимости такого паттерна в JavaScript. Но как это не удивительно, Singleton’ом в JavaScript мы оперируем повседневно.

Обычно я спрашиваю вопрос таким образом:

Какие есть способы получать один и тот же экземпляр объекта?

И тут я жду рассуждений. Каждый из нас пользовался и не раз Singleton объектами в JS даже не думая ни о каких шаблонах объектно ориентированного программирования. Как ни странно, но задача для многих оказывается нетривиальной. Даже если человек до этого писал на PHP или C#, к примеру. Даже если человек давно знаком с JS, почему-то мало кто отвечает, что самый простой способ создать Singleton объект в JS — это создать глобальную переменную с присвоением объекта, ведь в JS для создания экземпляра объекта не обязательно создавать класс. В JS все есть объект…

В результате решил агрегировать все свои знания и оформить в виде сборника решений одной задачи на все случаи жизни. Причем покажу варианты на ES5, ES6+ и TypeScript. TypeScript в данном случае выступает в роли правильного ООП языка. И так, поехали…

Singleton — одиночка

Немного занудства, можно пропустить, если все это знаете.

Одиночка (англ. Singleton) — порождающий шаблон проектирования, гарантирующий что в однопоточном приложении будет единственный экземпляр класса с глобальной точкой доступа.

Порождающие шаблоны (англ. Creational patterns) — шаблоны проектирования, которые абстрагируют процесс инстанцирования. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов.

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

Цель

Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Существенно то, что можно пользоваться именно экземпляром класса, так как при этом во многих случаях становится доступной более широкая функциональность. Например, к описанным компонентам класса можно обращаться через интерфейс, если такая возможность поддерживается языком.

Плюсы

  1. Контролируемый доступ к единственному экземпляру.

Минусы

  1. Глобальные объекты могут быть вредны для объектного программирования, в некоторых случаях приводя к созданию немасштабируемого проекта;
  2. Усложняет написание модульных тестов и следование TDD.

Примеры использования

  • Объект-дебаггер для отладки web-приложения
  • Объект сбора ошибок
  • Класс доступа к браузерным хранилищам и cookies
  • Реализация шаблона Реестр (Registry). Например реестр объектов, для контроля за используемыми объектами на странице
  • Реализация паттерна Медиатор или Application controller

Условия выполнения

Мы воспользуемся подходом TDD и прежде чем писать реализации запишем минимальные тесты, которые должны быть выполнены. Так как это JS и этот язык сильно отличается от более “правильных” языков типа Java, C++ и прочих, в которых создание объекта реализуется через класс, а конструктор не может ничего возвращать, то мы будем так же рассматривать варианты реализации без конструктора и классического определения класса. Ведь в JS это можно делать.

И так, минимальные условия:

var errorMessage = 'Instantiation failed: use Singleton.getInstance() instead of new.';// Test constructor
try { var obj0 = new Singleton } catch(c) { console.log(c == errorMessage) }
// Create and get object
let obj1 = Singleton.getInstance();
let obj2 = Singleton.getInstance();
obj1.foo = 456;
console.log( obj1 === obj2 );
try { var obj3 = new Singleton } catch(c) { console.log(c == errorMessage) }console.log(obj0 === void 0);
console.log(obj3 === void 0);

Но эти тесты будут модифицированы в процессе разбора решений, так как у нас будут реализации, возвращающие ссылку на объект из конструктора.

Реализации Singleton в TypeScript

Классическая реализация на TypeScript

Все бы хорошо, но в TypeScript 1.8 нет возможности задать приватный конструктор, что приводит к добавлению логики в него. Минусы такой реализации — при первом вызове new Singleton конструктор вернет объект. Да им можно пользоваться, но это нарушает классическую реализацию. Мы можем модифицировать код и получить вот такую версию:

В такой реализации и кода меньше, и нельзя получить экземпляр объекта через new, так как первая иницализация происходит “автоматически” при объявлении класса.

Снова повторюсь: так как у нас необычный язык, то реализовать задачу можно совершенно необычными способами. Например мы можем реализовать одиночку через пространство имен (namespace):

Продолжая развивать тему, мы можем реализовать паттерн через модуль. Я покажу вариант модуля:

Вы так же можете реализовать модуль в отдельном файле:

В такой реализации у нас не то чтобы нет доступа к объекту через вызов new Singleton. У нас вообще нет возможности достучаться до конструктора (ну мы сейчас не рассматриваем цепочку прототипов и прочие возможности JS).

Анонимный класс

Так можНо, если нужно ;)

const Singleton = new (class {
public foo :number = 123;
getInstance() :this { return this }
})();

Реализации Singleton в JavaScript

А теперь вернемся к нашему любимому JavaScript со всеми его возможностями. И так, помните я говорил, что мы каждый день пользуемся объектами одиночками? Так как у нас JS , то нам вовсе не нужно создавать класс для получения объекта. Мы можем создать объект, который будет проходить наши тесты:

const Singleton = {
foo: 123,
getInstance() { return this }
};
let obj1 = Singleton.getInstance();
let obj2 = Singleton.getInstance();
obj1.foo = 456;
console.log( obj1 === obj2 );

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

А теперь рассмотрим более сложные примеры на ES5+, позволяющие создавать именно классы, которые будут порождать Singleton.

И да, мы можем возвращать из конструктора любой объект, что дает простор воображению. Поехали!

Используем arguments

function Singleton() {
if (arguments.callee.instance) return arguments.callee.instance;
this.foo = 123;
return arguments.callee.instance = this;
}
let obj1 = new Singleton;
let obj2 = new Singleton;
obj1 === obj2 // true

Метод лаконичен и просто в реализации, но у него есть недостаток. В режиме “use strict” этот код не будет работать, а JSLint/JSHint в (Php|Web)Storm будет показывать ошибку.

Тогда этот же пример можно переписать так:

function Singleton() {
if (Singleton.instance) return Singleton.instance;
this.foo = 123;
return Singleton.instance = this;
}

Скрываем доступ к instance

Пример на ES5 c приватными (локальными) переменными:

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

Краткая запись

var Singleton = new function(){
var instance = this;
// Код конструктора
return function(){ return instance }
}
console.log( new Singleton === new Singleton ); // true

ECMAScript 2015

Эпилог

Как видите способов и разнообразия для реализации логики порождающей единственный экземпляр объекта хватает в нашем любимом JavaScript.

Александр Майоров / Alexander Mayorov — ProWEBIT

Written by

Активист программного комитета FrontendConf. CTO and co-founder at New.HR & 𝔾𝕖𝕖𝕜🄹🄾🄱.ru

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade