Как определить синглтон в TypeScript

128

Каков наилучший и наиболее удобный способ реализовать шаблон Singleton для класса в TypeScript? (Как с отложенной инициализацией, так и без нее).

Maja
источник

Ответы:

87

Одноэлементные классы в TypeScript обычно являются анти-паттерном. Вместо этого вы можете просто использовать пространства имен .

Бесполезный шаблон singleton

class Singleton {
    /* ... lots of singleton logic ... */
    public someMethod() { ... }
}

// Using
var x = Singleton.getInstance();
x.someMethod();

Эквивалент пространства имен

export namespace Singleton {
    export function someMethod() { ... }
}
// Usage
import { SingletonInstance } from "path/to/Singleton";

SingletonInstance.someMethod();
var x = SingletonInstance; // If you need to alias it for some reason
Райан Кавано
источник
55
было бы неплохо теперь, почему синглтон считается антипаттерном? рассмотрите этот подход codebelt.com/typescript/typescript-singleton-pattern
Виктор,
21
Я хотел бы знать, почему синглтоны в TypeScript тоже считаются антипаттерном. А также, если у него нет параметров конструктора, почему бы и нет export default new Singleton()?
emzero
23
Решение пространства имен больше похоже на статический класс, а не на синглтон
Михай Рэдукану
6
Он ведет себя так же. В C # вы не можете передавать статический класс, как если бы он был значением (то есть как если бы он был экземпляром одноэлементного класса), что ограничивает его полезность. В TypeScript вы можете передавать пространство имен как экземпляр. Вот почему вам не нужны одноэлементные классы.
Райан Кавано
13
Ограничение использования пространства имен как синглтона заключается в том, что оно не может (насколько мне известно) реализовать интерфейс. Вы бы согласились с этим @ryan
Гейб О'Лири
182

Начиная с TS 2.0, у нас есть возможность определять модификаторы видимости в конструкторах , так что теперь мы можем создавать синглтоны в TypeScript так же, как мы привыкли к другим языкам.

Приведен пример:

class MyClass
{
    private static _instance: MyClass;

    private constructor()
    {
        //...
    }

    public static get Instance()
    {
        // Do you need arguments? Make it a regular static method instead.
        return this._instance || (this._instance = new this());
    }
}

const myClassInstance = MyClass.Instance;

Спасибо @Drenai за то, что вы указали, что если вы пишете код, используя необработанный скомпилированный javascript, у вас не будет защиты от множественного создания экземпляров, поскольку ограничения TS исчезают, и конструктор не будет скрыт.

Alex
источник
2
конструктор может быть частным?
Эксперт хочет быть
2
@Expertwannabe Теперь это доступно в TS 2.0: github.com/Microsoft/TypeScript/wiki/…
Alex
3
Это мой предпочтительный ответ! Спасибо.
Мартин Маевски
1
кстати, причиной нескольких экземпляров было разрешение узлового модуля. Таким образом, если вы создаете синглтон в узле, убедитесь, что это учтено. В итоге я создал папку node_modules в моем каталоге src и поместил туда синглтон.
webteckie
3
@KimchiMan Если проект когда-либо использовался в среде без машинописного текста, например, импортирован в проект JS, класс не будет иметь защиты от дальнейшего создания экземпляра. Он работает только в чистой среде TS, но не для разработки библиотеки JS
Дренай
40

Лучший способ, который я нашел:

class SingletonClass {

    private static _instance:SingletonClass = new SingletonClass();

    private _score:number = 0;

    constructor() {
        if(SingletonClass._instance){
            throw new Error("Error: Instantiation failed: Use SingletonClass.getInstance() instead of new.");
        }
        SingletonClass._instance = this;
    }

    public static getInstance():SingletonClass
    {
        return SingletonClass._instance;
    }

    public setScore(value:number):void
    {
        this._score = value;
    }

    public getScore():number
    {
        return this._score;
    }

    public addPoints(value:number):void
    {
        this._score += value;
    }

    public removePoints(value:number):void
    {
        this._score -= value;
    }

}

Вот как вы его используете:

var scoreManager = SingletonClass.getInstance();
scoreManager.setScore(10);
scoreManager.addPoints(1);
scoreManager.removePoints(2);
console.log( scoreManager.getScore() );

https://codebelt.github.io/blog/typescript/typescript-singleton-pattern/

codeBelt
источник
3
Почему бы не сделать конструктор закрытым?
Фил Мандер,
4
Я думаю, что этот пост предшествовал возможности иметь частные конструкторы в TS. github.com/Microsoft/TypeScript/issues/2341
Тревор,
Мне нравится этот ответ. Частные конструкторы хороши во время разработки, но если транспилированный модуль TS импортируется в среду JS, к конструктору все равно можно получить доступ. При таком подходе он почти защищен от неправомерного использования .... если для SingletonClass ['_ instance'] не задано значение null / undefined
Дренай,
Ссылка не работает. Я думаю, что это настоящая ссылка: codebelt.github.io/blog/typescript/typescript-singleton-pattern
Эль-Асидуо,
24

Следующий подход создает класс Singleton, который можно использовать точно так же, как обычный класс:

class Singleton {
    private static instance: Singleton;
    //Assign "new Singleton()" here to avoid lazy initialisation

    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }

        this. member = 0;
        Singleton.instance = this;
    }

    member: number;
}

каждый new Singleton() операция вернет один и тот же экземпляр. Однако это может быть неожиданным для пользователя.

Следующий пример более прозрачен для пользователя, но требует другого использования:

class Singleton {
    private static instance: Singleton;
    //Assign "new Singleton()" here to avoid lazy initialisation

    constructor() {
        if (Singleton.instance) {
            throw new Error("Error - use Singleton.getInstance()");
        }
        this.member = 0;
    }

    static getInstance(): Singleton {
        Singleton.instance = Singleton.instance || new Singleton();
        return Singleton.instance;
    }

    member: number;
}

Использование: var obj = Singleton.getInstance();

Maja
источник
1
Вот как это должно быть реализовано. Если есть одна вещь, в которой я не согласен с The Gang of Four - и, вероятно, только одна, - то это The Singleton Pattern. Возможно, C / ++ мешает так спроектировать. Но если вы спросите меня, клиентский код не должен знать или заботиться о синглтоне. Клиенты по-прежнему должны реализовывать new Class(...)синтаксис.
Коди,
16

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

// shout.ts
class ShoutSingleton {
  helloWorld() { return 'hi'; }
}

export let Shout = new ShoutSingleton();

использование

import { Shout } from './shout';
Shout.helloWorld();
Ромен Брукерт
источник
Я получил следующее сообщение об ошибке: Экспортированная переменная «Shout» имеет или использует частное имя «ShoutSingleton».
Twois
3
Вы также должны экспортировать класс ShoutSingleton, и ошибка исчезнет.
Twois
Да, я тоже удивлен. Зачем вообще возиться с классом? Синглтоны должны скрывать свою внутреннюю работу. Почему бы просто не экспортировать функцию helloWorld?
Олег Дулин
см. эту проблему github для получения дополнительной информации: github.com/Microsoft/TypeScript/issues/6307
Ore4444
5
Думаю, ничто не Shout
мешает
7

Вы можете использовать для этого выражения классов (я считаю, что с версии 1.6).

var x = new (class {
    /* ... lots of singleton logic ... */
    public someMethod() { ... }
})();

или с именем, если вашему классу нужен внутренний доступ к своему типу

var x = new (class Singleton {
    /* ... lots of singleton logic ... */
    public someMethod(): Singleton { ... }
})();

Другой вариант - использовать локальный класс внутри вашего синглтона, используя некоторые статические члены.

class Singleton {

    private static _instance;
    public static get instance() {

        class InternalSingleton {
            someMethod() { }

            //more singleton logic
        }

        if(!Singleton._instance) {
            Singleton._instance = new InternalSingleton();
        }

        return <InternalSingleton>Singleton._instance;
    }
}

var x = Singleton.instance;
x.someMethod();
bingles
источник
7

Добавьте следующие 6 строк к любому классу, чтобы сделать его «Синглтоном».

class MySingleton
{
    private constructor(){ /* ... */}
    private static _instance: MySingleton;
    public static getInstance(): MySingleton
    {
        return this._instance || (this._instance = new this());
    };
}

Пример теста:

var test = MySingleton.getInstance(); // will create the first instance
var test2 = MySingleton.getInstance(); // will return the first instance
alert(test === test2); // true

[Edit]: используйте ответ Alex, если вы предпочитаете получать экземпляр через свойство, а не через метод.

Флавьен Волкен
источник
Что произойдет, если я new MySingleton()скажу 5 раз? ваш код резервирует один экземпляр?
Hlawuleka MAS 08
вы никогда не должны использовать «new»: как писал Алекс, конструктор должен быть «частным», чтобы не выполнять «new MySingleton ()». Правильное использование - получить экземпляр с помощью MySingleton.getInstance (). AKAIK без конструктора (как в моем примере) = открытый пустой конструктор
Флавьен Волкен
«Вы никогда не должны использовать« новый »- в точности моя точка зрения:». Но как ваша реализация мешает мне это сделать? Я нигде не вижу, чтобы у вас в классе был приватный конструктор?
Hlawuleka MAS
@HlawulekaMAS Я не делал ... Поэтому я отредактировал ответ, обратите внимание, что частный конструктор был невозможен до TS 2.0 (то есть в то время, когда я написал ответ первым)
Flavien Volken
«т.е. в то время, когда я первым написал ответ» - имеет смысл. Прохладно.
Hlawuleka MAS
3

я думаю, может быть, использовать дженерики быть жидким

class Singleton<T>{
    public static Instance<T>(c: {new(): T; }) : T{
        if (this._instance == null){
            this._instance = new c();
        }
        return this._instance;
    }

    private static _instance = null;
}

как пользоваться

шаг 1

class MapManager extends Singleton<MapManager>{
     //do something
     public init():void{ //do }
}

шаг 2

    MapManager.Instance(MapManager).init();
Sanye
источник
3

Вы также можете использовать функцию Object.Freeze () . Это просто и легко:

class Singleton {

  instance: any = null;
  data: any = {} // store data in here

  constructor() {
    if (!this.instance) {
      this.instance = this;
    }
    return this.instance
  }
}

const singleton: Singleton = new Singleton();
Object.freeze(singleton);

export default singleton;
Kenny
источник
Кенни, хорошая точка зрения на freeze (), но два примечания: (1) после того, как вы заморозите (singleton), вы все равно можете изменить singleton.data .. вы не можете добавить другой атрибут (например, data2), но дело в том, что freeze ( ) не является глубоким замораживанием :) и (2) ваш класс Singleton позволяет создавать более одного экземпляра (например, obj1 = new Singleton (); obj2 = new Singleton ();), поэтому ваш синглтон не является синглтоном :)
Дмитрий Шевкопляс
Если вы импортируете Singleton Class в другие файлы, вы всегда будете получать один и тот же экземпляр, а данные в «data» будут согласованы между всеми другими импортами. Это для меня синглтон. Заморозка в том, чтобы убедиться, что экспортированный экземпляр Singleton создается только один раз.
kenny
Кенни, (1) если вы импортируете свой класс в другие файлы, вы не получите экземпляр. При импорте вы просто переносите определение класса в область видимости, чтобы вы могли создавать новые экземпляры. Затем вы можете создать> 1 экземпляра данного класса, будь то в одном файле или в нескольких файлах, что противоречит всей цели идеи синглтона. (2) Из документации: метод Object.freeze () замораживает объект. Замороженный объект больше нельзя изменить; замораживание объекта предотвращает добавление к нему новых свойств. (конец цитаты) Это означает, что freeze () не мешает вам создавать несколько объектов.
Дмитрий Шевкопляс
Верно, но в этом случае будет, потому что экспортированный член уже является экземпляром. И экземпляр хранит данные. Если вы также поместите экспорт в класс, вы правы и можете создать несколько экземпляров.
kenny
@kenny, если вы знаете, что собираетесь экспортировать экземпляр, зачем возиться с if (!this.instance)внутри конструктора? Это дополнительная мера предосторожности на случай, если вы создали несколько экземпляров перед экспортом?
Alex
2

Я нашел новую версию этого, с которой компилятор Typescript полностью согласен, и я думаю, что она лучше, потому что она не требует getInstance()постоянного вызова метода.

import express, { Application } from 'express';

export class Singleton {
  // Define your props here
  private _express: Application = express();
  private static _instance: Singleton;

  constructor() {
    if (Singleton._instance) {
      return Singleton._instance;
    }

    // You don't have an instance, so continue

    // Remember, to set the _instance property
    Singleton._instance = this;
  }
}

Это имеет другой недостаток. Если у вас Singletonесть какие-либо свойства, то компилятор Typescript подберет подходящий, если вы не инициализируете их значением. Вот почему я включил _expressсвойство в свой примерный класс, потому что, если вы не инициализируете его значением, даже если вы назначите его позже в конструкторе, Typescript будет думать, что оно не было определено. Это можно исправить, отключив строгий режим, но по возможности я предпочитаю этого не делать. Есть еще один недостаток этого метода, на который я должен указать, потому что конструктор фактически вызывается, каждый раз, когда он выполняет другой экземпляр, технически создается, но недоступен. Теоретически это могло вызвать утечку памяти.

Eagerestwolf
источник
1

Это, вероятно, самый долгий процесс создания синглтона в машинописном тексте, но в более крупных приложениях именно тот, который работал у меня лучше.

Сначала вам понадобится класс Singleton, скажем, "./utils/Singleton.ts" :

module utils {
    export class Singleton {
        private _initialized: boolean;

        private _setSingleton(): void {
            if (this._initialized) throw Error('Singleton is already initialized.');
            this._initialized = true;
        }

        get setSingleton() { return this._setSingleton; }
    }
}

Теперь представьте, что вам нужен синглтон маршрутизатора "./navigation/Router.ts" :

/// <reference path="../utils/Singleton.ts" />

module navigation {
    class RouterClass extends utils.Singleton {
        // NOTICE RouterClass extends from utils.Singleton
        // and that it isn't exportable.

        private _init(): void {
            // This method will be your "construtor" now,
            // to avoid double initialization, don't forget
            // the parent class setSingleton method!.
            this.setSingleton();

            // Initialization stuff.
        }

        // Expose _init method.
        get init { return this.init; }
    }

    // THIS IS IT!! Export a new RouterClass, that no
    // one can instantiate ever again!.
    export var Router: RouterClass = new RouterClass();
}

Отлично !, теперь инициализируйте или импортируйте туда, где вам нужно:

/// <reference path="./navigation/Router.ts" />

import router = navigation.Router;

router.init();
router.init(); // Throws error!.

При таком подходе синглтоны хорошо то, что вы по-прежнему используете всю красоту классов машинописного текста, это дает вам хороший intellisense, логика синглтонов каким-то образом отделена и ее легко удалить при необходимости.

a.guerrero.g87
источник
1

Мое решение для этого:

export default class Modal {
    private static _instance : Modal = new Modal();

    constructor () {
        if (Modal._instance) 
            throw new Error("Use Modal.instance");
        Modal._instance = this;
    }

    static get instance () {
        return Modal._instance;
    }
}
Даниэль де Андраде Варела
источник
1
В конструкторе вместо исключения можно return Modal._instance. Таким образом, если вы используете newэтот класс, вы получите существующий объект, а не новый.
Mihai Răducanu
1

В Typescript не обязательно следовать new instance()методологии Singleton. Импортированный статический класс без конструктора также может работать.

Рассматривать:

export class YourSingleton {

   public static foo:bar;

   public static initialise(_initVars:any):void {
     YourSingleton.foo = _initvars.foo;
   }

   public static doThing():bar {
     return YourSingleton.foo
   }
}

Вы можете импортировать класс и ссылаться на него YourSingleton.doThing()в любом другом классе. Но помните, поскольку это статический класс, у него нет конструктора, поэтому я обычно использую intialise()метод, который вызывается из класса, который импортирует синглтон:

import {YourSingleton} from 'singleton.ts';

YourSingleton.initialise(params);
let _result:bar = YourSingleton.doThing();

Не забывайте, что в статическом классе каждый метод и переменная также должны быть статическими, поэтому вместо thisвас следует использовать полное имя класса YourSingleton.

Доминик Ли
источник
0

Вот еще один способ сделать это с помощью более традиционного подхода javascript с использованием IFFE :

module App.Counter {
    export var Instance = (() => {
        var i = 0;
        return {
            increment: (): void => {
                i++;
            },
            getCount: (): number => {
                return i;
            }
        }
    })();
}

module App {
    export function countStuff() {
        App.Counter.Instance.increment();
        App.Counter.Instance.increment();
        alert(App.Counter.Instance.getCount());
    }
}

App.countStuff();

Посмотреть демо

JesperA
источник
В чем причина добавления Instanceпеременной? Вы просто помещаете переменную и функции прямо под App.Counter.
fyaa 01
@fyaa Да, вы могли бы, но переменную и функции непосредственно в App.Counter, но я думаю, что этот подход лучше соответствует шаблону singleton en.wikipedia.org/wiki/Singleton_pattern .
JesperA
0

Другой вариант - использовать символы в вашем модуле. Таким образом вы можете защитить свой класс, даже если конечный пользователь вашего API использует обычный Javascript:

let _instance = Symbol();
export default class Singleton {

    constructor(singletonToken) {
        if (singletonToken !== _instance) {
            throw new Error("Cannot instantiate directly.");
        }
        //Init your class
    }

    static get instance() {
        return this[_instance] || (this[_instance] = new Singleton(_singleton))
    }

    public myMethod():string {
        return "foo";
    }
}

Использование:

var str:string = Singleton.instance.myFoo();

Если пользователь использует ваш скомпилированный js-файл API, он также получит сообщение об ошибке, если попытается вручную создать экземпляр вашего класса:

// PLAIN JAVASCRIPT: 
var instance = new Singleton(); //Error the argument singletonToken !== _instance symbol
Ciberman
источник
0

Не чистый синглтон (инициализация может быть не ленивая), а похожий паттерн с помощью namespaces.

namespace MyClass
{
    class _MyClass
    {
    ...
    }
    export const instance: _MyClass = new _MyClass();
}

Доступ к объекту Синглтона:

MyClass.instance
sergzach
источник
0

Это самый простой способ

class YourSingletoneClass {
  private static instance: YourSingletoneClass;

  private constructor(public ifYouHaveAnyParams: string) {

  }
  static getInstance() {
    if(!YourSingletoneClass.instance) {
      YourSingletoneClass.instance = new YourSingletoneClass('If you have any params');
    }
    return YourSingletoneClass.instance;
  }
}
Сорин Вештемян
источник
-1
namespace MySingleton {
  interface IMySingleton {
      doSomething(): void;
  }
  class MySingleton implements IMySingleton {
      private usePrivate() { }
      doSomething() {
          this.usePrivate();
      }
  }
  export var Instance: IMySingleton = new MySingleton();
}

Таким образом, мы можем применить интерфейс, в отличие от принятого ответа Райана Кавано.

user487779
источник
-1

После просмотра этого потока и экспериментов со всеми описанными выше параметрами я остановился на синглтоне, который можно создать с помощью подходящих конструкторов:

export default class Singleton {
  private static _instance: Singleton

  public static get instance(): Singleton {
    return Singleton._instance
  }

  constructor(...args: string[]) {
    // Initial setup

    Singleton._instance = this
  }

  work() { /* example */ }

}

Это потребует первоначальной настройки (в main.tsили index.ts), которую можно легко реализовать с помощью
new Singleton(/* PARAMS */)

Затем в любом месте кода просто позвоните Singleton.instnace; в этом случае, чтобы workзакончить, я бы позвонилSingleton.instance.work()

TheGeekZn
источник
Зачем кому-то голосовать против ответа, фактически не комментируя улучшения? Мы сообщество
TheGeekZn