Jak zdefiniować Singleton w TypeScript


128

Jaki jest najlepszy i najwygodniejszy sposób implementacji wzorca Singleton dla klasy w języku TypeScript? (Zarówno z leniwą inicjalizacją, jak i bez niej).

Odpowiedzi:


87

Klasy singleton w TypeScript są generalnie anty-wzorcem. Zamiast tego możesz po prostu użyć przestrzeni nazw .

Bezużyteczny wzór singletona

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

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

Odpowiednik przestrzeni nazw

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
byłoby miło teraz, dlaczego singleton jest uważany za anty-wzór? rozważ to podejście codebelt.com/typescript/typescript-singleton-pattern
Victor

21
Chciałbym wiedzieć, dlaczego Singletony w TypeScript są również uważane za anty-wzorzec. A jeśli nie ma żadnych parametrów konstruktora, dlaczego nie export default new Singleton()?
emzero

23
Rozwiązanie przestrzeni nazw wygląda bardziej jak klasa statyczna, a nie singleton
Mihai Răducanu

6
Zachowuje się tak samo. W języku C # nie można przekazać klasy statycznej tak, jakby była wartością (tj. Jakby była instancją klasy pojedynczej), co ogranicza jej użyteczność. W języku TypeScript można przekazywać przestrzeń nazw jak instancję. Dlatego nie potrzebujesz klas pojedynczych.
Ryan Cavanaugh

13
Ograniczeniem używania przestrzeni nazw jako singletona jest to, że nie może (o ile wiem) zaimplementować interfejs. Zgodziłbyś się z tym @ryan
Gabe O'Leary

182

Od TS 2.0 mamy możliwość definiowania modyfikatorów widoczności w konstruktorach , więc teraz możemy tworzyć pojedyncze elementy w TypeScript, tak jak jesteśmy przyzwyczajeni z innych języków.

Podany przykład:

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;

Dziękuję @Drenai za wskazanie, że jeśli piszesz kod przy użyciu skompilowanego surowego javascript, nie będziesz mieć ochrony przed wielokrotnymi instancjami, ponieważ ograniczenia TS znikną, a konstruktor nie zostanie ukryty.


2
konstruktor może być prywatny?
Ekspert chce być

2
@Expertwannabe To jest teraz dostępne w TS 2.0: github.com/Microsoft/TypeScript/wiki/ ...
Alex

3
To moja ulubiona odpowiedź! Dziękuję Ci.
Martin Majewski,

1
fyi, przyczyną wielu wystąpień była przeszkadzająca rozdzielczość modułu węzła. Tak więc, jeśli tworzysz singleton w węźle, upewnij się, że jest to brane pod uwagę. Skończyło się na utworzeniu folderu node_modules w moim katalogu src i umieszczeniu tam singletona.
webteckie

3
@KimchiMan Jeśli projekt jest kiedykolwiek używany w środowisku innym niż maszynopis, np. Zaimportowany do projektu JS, klasa nie będzie chroniona przed dalszymi instancjami. Działa tylko w czystym środowisku TS, ale nie w przypadku programowania bibliotek JS
Drenai

39

Najlepszy sposób, jaki znalazłem, to:

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

}

Oto jak go używasz:

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
Dlaczego nie ustawić konstruktora jako prywatnego?
Phil Mander

4
Myślę, że ten post poprzedza możliwość posiadania prywatnych konstruktorów w TS. github.com/Microsoft/TypeScript/issues/2341
Trevor

Podoba mi się ta odpowiedź. Prywatne konstruktory są świetne podczas programowania, ale jeśli transpiled moduł TS jest importowany do środowiska JS, nadal można uzyskać dostęp do konstruktora. Dzięki takiemu podejściu jest prawie chroniony przed niewłaściwym użyciem ... chyba że SingletonClass ['_ instance'] ma wartość null / undefined
Drenai

Link jest uszkodzony. Myślę, że to jest rzeczywisty link: codebelt.github.io/blog/typescript/typescript-singleton-pattern
El Asiduo

24

Poniższe podejście tworzy klasę Singleton, której można używać dokładnie tak, jak klasycznej klasy:

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

Każdy new Singleton() operacja zwróci to samo wystąpienie. Może to być jednak nieoczekiwane przez użytkownika.

Poniższy przykład jest bardziej przejrzysty dla użytkownika, ale wymaga innego zastosowania:

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

Stosowanie: var obj = Singleton.getInstance();


1
W ten sposób powinno być realizowane. Jeśli jest jedna rzecz, której nie zgadzam się z The Gang of Four - i jest to prawdopodobnie tylko jedna - to The Singleton Pattern. Być może C / ++ utrudnia zaprojektowanie go w ten sposób. Ale jeśli o mnie chodzi, kod klienta nie powinien wiedzieć lub przejmować się tym, czy jest to singleton. Klienci powinni nadal implementować new Class(...)składnię.
Cody

16

Dziwię się, że nie widzę tutaj następującego wzoru, który w rzeczywistości wygląda bardzo prosto.

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

export let Shout = new ShoutSingleton();

Stosowanie

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

Otrzymałem następujący komunikat o błędzie: Wyeksportowana zmienna „Shout” ma lub używa nazwy prywatnej „ShoutSingleton”.
Twois

3
Musisz także wyeksportować klasę „ShoutSingleton”, a błąd zniknie.
Twois

Racja, ja też jestem zaskoczony. Dlaczego w ogóle zawracać sobie głowę klasą? Singletony mają ukrywać swoje wewnętrzne działanie. Dlaczego nie wyeksportować funkcji helloWorld?
Oleg Dulin

zobacz ten problem na githubie,
Ore4444

5
Chyba nic nie powstrzymuje jednak użytkowników przed utworzeniem nowej Shoutklasy
dalore

7

Możesz użyć do tego wyrażeń klasowych (od wersji 1.6, jak sądzę).

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

lub z nazwą, jeśli Twoja klasa potrzebuje wewnętrznego dostępu do jej typu

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

Inną opcją jest użycie lokalnej klasy wewnątrz singletona przy użyciu statycznych elementów członkowskich

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

Dodaj następujące 6 wierszy do dowolnej klasy, aby nadać jej nazwę „Singleton”.

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

Przykład testu:

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

[Edytuj]: Użyj odpowiedzi Alexa, jeśli wolisz uzyskać instancję za pomocą właściwości, a nie metody.


Co się stanie, gdy to zrobię new MySingleton(), powiedzmy 5 razy? czy twój kod rezerwuje pojedyncze wystąpienie?
Hlawuleka MAS

nigdy nie powinieneś używać "new": jak napisał Alex, konstruktor powinien być "private", uniemożliwiając wykonanie "new MySingleton ()". Właściwe użycie to pobranie instancji za pomocą MySingleton.getInstance (). AKAIK bez konstruktora (jak w moim przykładzie) = publiczny pusty konstruktor
Flavien Volken

„nigdy nie należy używać słowa„ nowy ”- dokładnie o co mi chodzi:„. Ale w jaki sposób Twoja implementacja uniemożliwia mi to? Nie widzę nigdzie, gdzie masz prywatnego konstruktora w swojej klasie?
Hlawuleka MAS

@HlawulekaMAS Nie zrobiłem… Dlatego zredagowałem odpowiedź, zauważ, że prywatny konstruktor nie był możliwy przed TS 2.0 (tj. W momencie, gdy pisałem pierwszą odpowiedź)
Flavien Volken

„tj. wtedy, gdy pisałem odpowiedź jako pierwszy” - ma sens. Chłodny.
Hlawuleka MAS

3

Myślę, że może użyć leków generycznych

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

jak używać

krok 1

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

krok 2

    MapManager.Instance(MapManager).init();

3

Możesz także skorzystać z funkcji Object.Freeze () . To proste i łatwe:

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, dobra uwaga na freeze (), ale dwie uwagi: (1) po zamrożeniu (singleton) nadal możesz modyfikować singleton.data .. nie możesz dodać innego atrybutu (jak data2), ale chodzi o to, że zamrożenie ( ) nie jest głębokim zamrożeniem :) i (2) twoja klasa Singleton pozwala na utworzenie więcej niż jednej instancji (przykład obj1 = new Singleton (); obj2 = new Singleton ();), więc twój Singleton nie jest Singleton
:)

Jeśli zaimportujesz klasę Singleton w innych plikach, zawsze otrzymasz tę samą instancję, a dane w „danych” będą spójne między wszystkimi innymi importami. To dla mnie singleton. Zamrożenie w upewnianiu się, że wyeksportowana instancja Singleton jest tworzona tylko raz.
kenny

Kenny, (1) jeśli zaimportujesz klasę do innych plików, nie otrzymasz instancji. Importując, po prostu wprowadzasz definicję klasy w zakres, dzięki czemu możesz tworzyć nowe instancje. Następnie możesz utworzyć> 1 instancję danej klasy, czy to w jednym pliku, czy w wielu plikach, co jest sprzeczne z celem pojedynczej idei. (2) Z dokumentacji: Metoda Object.freeze () zamraża obiekt. Zamrożonego obiektu nie można już zmienić; zamrożenie obiektu zapobiega dodawaniu do niego nowych właściwości. (koniec cudzysłowu) Co oznacza, że ​​freeze () nie powstrzymuje Cię przed tworzeniem wielu obiektów.
Dmitry Shevkoplyas

To prawda, ale w tym przypadku będzie, ponieważ wyeksportowany element członkowski jest już instancją. A instancja przechowuje dane. Jeśli wyeksportujesz również klasę, masz rację i możesz utworzyć wiele instancji.
kenny

@kenny, jeśli wiesz, że zamierzasz wyeksportować instancję, po co zawracać sobie głowę if (!this.instance)konstruktorem? Czy to tylko dodatkowe zabezpieczenie na wypadek, gdyby utworzono wiele instancji przed eksportem?
Alex

2

Znalazłem nową wersję tego, z którą kompilator Typescript jest całkowicie w porządku i myślę, że jest lepsza, ponieważ nie wymaga getInstance()ciągłego wywoływania metody.

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

Ma to inną wadę. Jeśli Singletonmasz jakieś właściwości, kompilator Typescript wykona dopasowanie, chyba że zainicjujesz je wartością. Dlatego dołączyłem _expresswłaściwość do mojej przykładowej klasy, ponieważ jeśli nie zainicjujesz jej wartością, nawet jeśli przypiszesz ją później w konstruktorze, Typescript pomyśli, że nie została zdefiniowana. Można to naprawić, wyłączając tryb ścisły, ale wolę tego nie robić, jeśli to możliwe. Jest jeszcze jedna wada tej metody, na którą powinienem zwrócić uwagę, ponieważ konstruktor jest faktycznie wywoływany, za każdym razem, gdy wykonuje inną instancję, jest technicznie tworzona, ale niedostępna. Może to teoretycznie powodować wycieki pamięci.


1

Jest to prawdopodobnie najdłuższy proces tworzenia singletona w maszynopisie, ale w większych aplikacjach jest to ten, który działał lepiej dla mnie.

Najpierw potrzebujesz klasy Singleton w, powiedzmy, „./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; }
    }
}

Teraz wyobraź sobie, że potrzebujesz singletona routera „./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();
}

Świetnie !, teraz zainicjuj lub zaimportuj, gdziekolwiek potrzebujesz:

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

import router = navigation.Router;

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

Zaletą robienia singletonów w ten sposób jest to, że nadal używasz całego piękna klas maszynopisu, daje to niezły intelizm, logika singletonów jest w jakiś sposób oddzielona i można ją łatwo usunąć w razie potrzeby.


1

Moje rozwiązanie:

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
W konstruktorze zamiast wyjątku możesz return Modal._instance. W ten sposób, jeśli masz newtę klasę, otrzymasz istniejący obiekt, a nie nowy.
Mihai Răducanu

1

W przypadku maszynopisu niekoniecznie trzeba przestrzegać new instance() metodologii Singleton. Zaimportowana klasa statyczna bez konstruktora może również działać równie dobrze.

Rozważać:

export class YourSingleton {

   public static foo:bar;

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

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

Możesz zaimportować klasę i odnosić się do niej YourSingleton.doThing()w dowolnej innej klasie. Ale pamiętaj, ponieważ jest to klasa statyczna, nie ma konstruktora, więc zazwyczaj używam intialise()metody, która jest wywoływana z klasy importującej Singleton:

import {YourSingleton} from 'singleton.ts';

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

Nie zapominaj, że w klasie statycznej każda metoda i zmienna również musi być statyczna, więc zamiast thisciebie należy użyć pełnej nazwy klasy YourSingleton.


0

Oto kolejny sposób na zrobienie tego z bardziej konwencjonalnym podejściem javascript przy użyciu 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();

Zobacz demo


Jaki jest powód dodania Instancezmiennej? Możesz po prostu umieścić zmienną i funkcje bezpośrednio pod App.Counter.
fyaa

@fyaa Tak, możesz ale zmienną i funkcje bezpośrednio w App.Counter, ale myślę, że to podejście lepiej pasuje do wzorca singleton en.wikipedia.org/wiki/Singleton_pattern .
JesperA

0

Inną opcją jest użycie symboli w module. W ten sposób możesz chronić swoją klasę, również jeśli końcowy użytkownik twojego API używa normalnego 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";
    }
}

Stosowanie:

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

Jeśli użytkownik używa Twojego skompilowanego pliku js API, również otrzyma błąd, jeśli spróbuje ręcznie utworzyć instancję Twojej klasy:

// PLAIN JAVASCRIPT: 
var instance = new Singleton(); //Error the argument singletonToken !== _instance symbol

0

Nie jest to czysty singleton (inicjalizacja może nie być leniwa), ale podobny wzór za pomocą namespaces.

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

Dostęp do obiektu Singleton:

MyClass.instance

0

To jest najprostszy sposób

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

W ten sposób możemy zastosować interfejs, w przeciwieństwie do zaakceptowanej odpowiedzi Ryana Cavanaugha.


-1

Po przejrzeniu tego wątku i zabawie wszystkimi powyższymi opcjami - zdecydowałem się na Singletona, który można stworzyć za pomocą odpowiednich konstruktorów:

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 */ }

}

Wymagałoby to wstępnej konfiguracji (w main.tslub index.ts), którą można łatwo zaimplementować za pomocą
new Singleton(/* PARAMS */)

Następnie, w dowolnym miejscu kodu, po prostu zadzwoń Singleton.instnace; w takim przypadku, żeby workskończyć, zadzwonięSingleton.instance.work()


Dlaczego ktoś miałby głosować przeciw odpowiedzi bez komentowania ulepszeń? Jesteśmy społecznością
TheGeekZn
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.