Як визначити Singleton в TypeScript


128

Який найкращий та найзручніший спосіб реалізувати шаблон Singleton для класу в TypeScript? (І з лінивою ініціалізацією, і без неї).

Відповіді:


87

Класи Singleton в TypeScript, як правило, є антидіаграмою. Ви можете просто використовувати простори імен .

Марний однотонний візерунок

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
Мені хотілося б знати, чому Singletons у TypeScript теж вважаються антидіаграмою. А також якщо у нього немає параметрів конструктора, чому б ні export default new Singleton()?
emzero

23
Рішення простору імен більше нагадує статичний клас, а не синглтон
Mihai Răducanu

6
Він поводиться так само. У C # ви не можете передавати статичний клас навколо, як якщо б це було значення (тобто, як якщо б це був екземпляр однокласного класу), що обмежує його корисність. У TypeScript ви можете передавати простір імен навколо, як екземпляр. Ось чому вам не потрібні однокласні заняття.
Райан Кавано

13
Обмеженням використання простору імен як одиночного є те, що він не може (наскільки мені відомо) реалізувати інтерфейс. Чи погоджуєтесь ви з цим @ryan
Gabe O'Leary

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 зникають і конструктор не буде прихований.


2
конструктор може бути приватним?
Експерт хоче бути

2
@Expertwannabe Це тепер доступно в TS 2.0: github.com/Microsoft/TypeScript/wiki/…
Alex

3
Це моя найкраща відповідь! Дякую.
Мартін Маєвський

1
fyi, причиною декількох екземплярів стало дозвіл модуля вузла. Таким чином, якщо ви створюєте сингл в вузлі, переконайтеся, що це враховано. Я в кінцевому підсумку створив папку node_modules під моїм каталогом src і помістив туди синглтон.
webteckie

3
@KimchiMan Якщо проект коли-небудь використовуватиметься в нетипізованому середовищі, наприклад, імпортованому в проект JS, клас не матиме захисту від подальших інстанцій. Він працює лише в чистому середовищі TS, але не для розвитку бібліотеки JS
Дреней

39

Найкращий спосіб, який я знайшов:

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/


3
Чому б не зробити конструктор приватним?
Філ Мандер

4
Я думаю, що посада передує можливості мати приватних конструкторів в TS. github.com/Microsoft/TypeScript/isissue/2341
Тревор

Мені подобається ця відповідь. Приватні конструктори чудові під час розробки, але якщо транспольований модуль TS імпортується в середовище JS, конструктор все ще може отримати доступ. При такому підході він майже захищений від зловживань .... якщо тільки SingletonClass ['_ екземпляр'] не встановлено на нуль / невизначено
Дренай

Посилання розірвано. Я думаю, що це фактичне посилання: codebelt.github.io/blog/typescript/typescript-singleton-pattern
El Asiduo

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();


1
Це спосіб, який він повинен реалізовувати. Якщо є одна річ, я не погоджуюся з «Бандою чотирьох» - і це, мабуть, лише 1 - її шаблон Singleton. Можливо, C / ++ перешкоджає проектувати його таким чином. Але якщо ви запитаєте мене, клієнтський код не повинен знати або дбати, чи це Singletonton. Клієнти все-таки повинні реалізувати new Class(...)синтаксис.
Коді

16

Я здивований, що не бачу тут такої схеми, яка насправді виглядає дуже просто.

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

export let Shout = new ShoutSingleton();

Використання

import { Shout } from './shout';
Shout.helloWorld();

Я отримав таке повідомлення про помилку: Експортна змінна 'Shout' має або використовує приватне ім’я 'ShoutSingleton'.
Двіці

3
Вам також потрібно експортувати клас "ShoutSingleton", і помилка зникає.
Twois

Правильно, я також здивований. Навіщо хоч і не турбуватися з класом? Синглтони повинні приховувати свою внутрішню роботу. Чому б не просто експортувати функцію helloWorld?
Олег Дулін

див. цей випуск github для отримання додаткової інформації: github.com/Microsoft/TypeScript/isissue/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();

7

Додайте наступні 6 рядків до будь-якого класу, щоб зробити його "Singleton".

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

[Редагувати]: Використовуйте відповідь Алекса, якщо ви бажаєте отримати екземпляр через властивість, а не метод.


Що відбувається, коли я роблю new MySingleton(), скажімо, 5 разів? чи залишає ваш код один екземпляр?
Хлавулека МАС

ніколи не слід використовувати "нове": як писав Алекс, конструктор повинен бути "приватним", заважаючи робити "новий MySingleton ()". Правильне використання - отримати екземпляр за допомогою MySingleton.getInstance (). AKAIK немає конструктора (як у моєму прикладі) = публічний порожній конструктор
Флавіен Волкен

"ви ніколи не повинні використовувати" нове "- саме мій пункт:". Але як ваша реалізація заважає мені це робити? Я ніде не бачу, де у вас в класі є приватний конструктор?
Hlawuleka МАС

@HlawulekaMAS Я цього не зробив ... Тому я відредагував відповідь, зауважте, що приватний конструктор був неможливий до TS 2.0 (тобто на той момент, коли я написав відповідь спочатку)
Flavien Volken

"тобто тоді, коли я написав відповідь першим" - Має сенс. Класно.
Хлавулека МАС

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();

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;

Кенні, хороший пункт про заморожування (), але дві примітки: (1) після заморозки (синглтон) ви все одно можете змінити singleton.data .. ви не можете додати інший атрибут (наприклад, data2), але справа в тому, що заморожування ( ) не є глибоким заморожуванням :) і (2) ваш клас Singleton дозволяє створити більше одного екземпляра (наприклад, obj1 = new Singleton (); obj2 = new Singleton ();), тож ваш Singleton не є Singleton:)
Дмитро Шевкопляс

Якщо ви імпортуєте клас Singleton в інші файли, ви завжди отримаєте один і той же екземпляр, і дані в "data" будуть відповідати між усіма іншими імпортами. Це для мене сингл. Замороження, переконавшись, що експортований екземпляр Singleton створюється лише один раз.
Kenny

Кенні, (1) якщо ти імпортуєш свій клас в інші файли, екземпляр не отримаєш. За допомогою імпорту ви просто приводите визначення класу в область, щоб ви могли створювати нові екземпляри. Тоді ви можете створити> 1 екземпляр даного класу, будь то в одному файлі або декількох файлах, що не відповідає цілі єдиної ідеї. (2) З docs: метод Object.freeze () заморожує об'єкт. Заморожений об’єкт вже не можна змінювати; заморожування об'єкта не дозволяє додавати до нього нові властивості. (кінець цитати) Що означає, що заморожування () не заважає вам створювати кілька об'єктів.
Дмитро Шевкопляс

Щоправда, але в цьому випадку це буде, тому що експортований член - це вже екземпляр. І екземпляр зберігає дані. Якщо ви також поставите експорт до класу, ви праві, і ви можете створити кілька примірників.
Kenny

@kenny, якщо ви знаєте, що збираєтеся експортувати екземпляр, навіщо турбуватися з if (!this.instance)конструктором? Це лише додаткова обережність у випадку, якщо ви створили кілька екземплярів до експорту?
Алекс

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 подумає, що він не визначений. Це можна виправити, відключивши суворий режим, але я вважаю за краще не робити це, якщо можливо. Існує також ще один недолік цього методу, який я повинен зазначити, оскільки конструктор насправді викликає кожен раз, коли він робить інший екземпляр технічно створений, але недоступний. Це теоретично може призвести до витоку пам'яті.


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!.

Приємно в тому, що робити одиночні ключі таким чином, що ти все ще використовуєш всю красу класів машинописів, це дає приємну інтелігенцію, логіка сингтона залишається відокремленою, і її легко видалити, якщо потрібно.


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цей клас, ви отримаєте існуючий об'єкт, а не новий.
Міхай Радукану

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()метод, який викликається з класу, який імпортує Singleton:

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();

Перегляд демонстраційної версії


У чому причина додавання Instanceзмінної? Ви coukd просто ставите змінну та функції безпосередньо під App.Counter.
fyaa

@fyaa Так, ви можете, але змінна та функції безпосередньо в App.Counter, але я думаю, що цей підхід краще відповідає однотонному шаблону 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

0

Не чистий сингл (ініціалізація може бути не лінивим), але подібний зразок за допомогою namespaces.

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

Доступ до об'єкта Singleton:

MyClass.instance

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();
}

Таким чином ми можемо застосувати інтерфейс, на відміну від прийнятої відповіді Раяна Кавано.


-1

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

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.