Реализация Одиночки в JS
Как не облажаться на интервью
Тема, вроде бы, изъезженная. Кто-то реально на практике применяет понимая что это и зачем. Кто-то применяет, но не знает что это известный шаблон проектирования и он так называется. У кого-то спрашивают про это на собеседованиях.
С учетом особенностей JS различные варианты из ООП в нем могут быть очень даже нестандартными. Я сам иногда на собеседовании прошу написать Singleton у кандидата, только в случае если он совершил следующие действия:
- сказал что знает ООП и шаблоны проектирования
- сказал, что из всех паттернов знает Singleton
Не хочу поднимать сейчас вопрос о необходимости такого паттерна в JavaScript. Но как это не удивительно, Singleton’ом в JavaScript мы оперируем повседневно.
Обычно я спрашиваю вопрос таким образом:
Какие есть способы получать один и тот же экземпляр объекта?
И тут я жду рассуждений. Каждый из нас пользовался и не раз Singleton объектами в JS даже не думая ни о каких шаблонах объектно ориентированного программирования. Как ни странно, но задача для многих оказывается нетривиальной. Даже если человек до этого писал на PHP или C#, к примеру. Даже если человек давно знаком с JS, почему-то мало кто отвечает, что самый простой способ создать Singleton объект в JS — это создать глобальную переменную с присвоением объекта, ведь в JS для создания экземпляра объекта не обязательно создавать класс. В JS все есть объект…
В результате решил агрегировать все свои знания и оформить в виде сборника решений одной задачи на все случаи жизни. Причем покажу варианты на ES5, ES6+ и TypeScript. TypeScript в данном случае выступает в роли правильного ООП языка. И так, поехали…
Singleton — одиночка
Немного занудства, можно пропустить, если все это знаете.
Одиночка (англ. Singleton) — порождающий шаблон проектирования, гарантирующий что в однопоточном приложении будет единственный экземпляр класса с глобальной точкой доступа.
Порождающие шаблоны (англ. Creational patterns) — шаблоны проектирования, которые абстрагируют процесс инстанцирования. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов.
Шаблон, порождающий классы, использует наследование, чтобы изменять инстанцируемый класс, а шаблон, порождающий объекты, делегирует инстанцирование другому объекту.
Цель
Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Существенно то, что можно пользоваться именно экземпляром класса, так как при этом во многих случаях становится доступной более широкая функциональность. Например, к описанным компонентам класса можно обращаться через интерфейс, если такая возможность поддерживается языком.
Плюсы
- Контролируемый доступ к единственному экземпляру.
Минусы
- Глобальные объекты могут быть вредны для объектного программирования, в некоторых случаях приводя к созданию немасштабируемого проекта;
- Усложняет написание модульных тестов и следование 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).
TypeScript 2.0
Релиз TS 2.0 еще не вышел, но он в бета и его уже можно даже попробовать. Среди новшеств добавление модификаторов для конструктора (private, protected), а это значит что можно сделать Singleton еще проще, используя классическую парадигму:
class Singleton {
protected static instance = new Singleton;
public foo: number = 123;
private constructor() {}
public static getInstance() :Singleton {
return Singleton.instance;
}
}Анонимный класс
Так можНо, если нужно ;)
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.