Comment définir Singleton dans TypeScript

128

Quel est le moyen le meilleur et le plus pratique d'implémenter un modèle Singleton pour une classe dans TypeScript? (Avec et sans initialisation paresseuse).

maja
la source

Réponses:

87

Les classes Singleton dans TypeScript sont généralement un anti-pattern. Vous pouvez simplement utiliser des espaces de noms à la place.

Motif singleton inutile

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

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

Equivalent d'espace de noms

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
Ryan Cavanaugh
la source
55
ce serait bien maintenant pourquoi le singleton est-il considéré comme un anti-motif? considérez cette approche codebelt.com/typescript/typescript-singleton-pattern
Victor
21
J'aimerais savoir pourquoi les singletons dans TypeScript sont également considérés comme un anti-pattern. Et aussi s'il n'a pas de paramètres de constructeur, pourquoi pas export default new Singleton()?
emzero
23
La solution d'espace de noms ressemble plus à une classe statique, pas à un singleton
Mihai Răducanu
6
Il se comporte de la même manière. En C #, vous ne pouvez pas passer une classe statique comme s'il s'agissait d'une valeur (c'est-à-dire comme s'il s'agissait d'une instance d'une classe singleton), ce qui limite son utilité. Dans TypeScript, vous pouvez passer un espace de noms comme une instance. C'est pourquoi vous n'avez pas besoin de classes singleton.
Ryan Cavanaugh
13
Une limitation de l'utilisation d'un espace de noms comme singleton est qu'il ne peut pas (à ma connaissance) implémenter une interface. Seriez-vous d'accord avec ça @ryan
Gabe O'Leary
182

Depuis TS 2.0, nous avons la possibilité de définir des modificateurs de visibilité sur les constructeurs , donc maintenant nous pouvons faire des singletons dans TypeScript comme nous en avons l'habitude dans d'autres langages.

Exemple donné:

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;

Merci @Drenai d'avoir souligné que si vous écrivez du code en utilisant le javascript compilé brut, vous n'aurez pas de protection contre l'instanciation multiple, car les contraintes de TS disparaissent et le constructeur ne sera pas masqué.

Alex
la source
2
le constructeur pourrait être privé?
expert veut être
2
@Expertwannabe Ceci est maintenant disponible dans TS 2.0: github.com/Microsoft/TypeScript/wiki/…
Alex
3
C'est ma réponse préférée! Je vous remercie.
Martin Majewski
1
Pour info, la raison des multiples instances était la résolution du module de nœud qui gênait. Ainsi, si vous créez un singleton dans le nœud, assurez-vous que cela est pris en compte. J'ai fini par créer un dossier node_modules sous mon répertoire src et y mettre le singleton.
webteckie
3
@KimchiMan Si le projet est déjà utilisé dans un environnement non typographique, par exemple importé dans un projet JS, la classe n'aura aucune protection contre une instanciation ultérieure. Cela fonctionne uniquement dans un environnement TS pur, mais pas pour le développement de bibliothèques JS
Drenai
39

Le meilleur moyen que j'ai trouvé est:

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

}

Voici comment vous l'utilisez:

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
la source
3
Pourquoi ne pas rendre le constructeur privé?
Phil Mander
4
Je pense que l'article est antérieur à la possibilité d'avoir des constructeurs privés dans TS. github.com/Microsoft/TypeScript/issues/2341
Trevor
J'aime cette réponse. Les constructeurs privés sont excellents pendant le développement, mais si un module TS transpilé est importé dans un environnement JS, le constructeur est toujours accessible. Avec cette approche, il est presque protégé contre les abus ... à moins que la SingletonClass ['_ instance'] soit définie sur null / undefined
Drenai
Le lien est rompu. Je pense que c'est le lien réel: codebelt.github.io/blog/typescript/typescript-singleton-pattern
El Asiduo le
24

L'approche suivante crée une classe Singleton qui peut être utilisée exactement comme une classe conventionnelle:

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

Chaque new Singleton()opération renverra la même instance. Cela peut cependant être inattendu par l'utilisateur.

L'exemple suivant est plus transparent pour l'utilisateur mais nécessite une utilisation différente:

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

Usage: var obj = Singleton.getInstance();

maja
la source
1
C'est la manière dont il doit être mis en œuvre. S'il y a une chose avec laquelle je ne suis pas d'accord avec The Gang of Four - et ce n'est probablement qu'une seule chose - c'est The Singleton Pattern. Peut-être que C / ++ empêche quelqu'un de le concevoir de cette façon. Mais si vous me demandez, le code client ne devrait pas savoir ou se soucier si c'est un Singleton. Les clients doivent toujours implémenter la new Class(...)syntaxe.
Cody
16

Je suis surpris de ne pas voir le modèle suivant ici, qui semble en fait très simple.

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

export let Shout = new ShoutSingleton();

Usage

import { Shout } from './shout';
Shout.helloWorld();
Romain Bruckert
la source
J'ai reçu le message d'erreur suivant: La variable exportée «Shout» a ou utilise le nom privé «ShoutSingleton».
Twois
3
Vous devez également exporter la classe «ShoutSingleton» et l'erreur disparaîtra.
Twois
Oui, je suis aussi surpris. Pourquoi même s'embêter avec la classe? Les singletons sont censés cacher leur fonctionnement interne. Pourquoi ne pas simplement exporter la fonction helloWorld?
Oleg Dulin
voir ce numéro de github pour plus d'informations: github.com/Microsoft/TypeScript/issues/6307
Ore4444
5
Je suppose que rien Shout
n'empêche les
7

Vous pouvez utiliser des expressions de classe pour cela (à partir de 1.6 je crois).

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

ou avec le nom si votre classe a besoin d'accéder à son type en interne

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

Une autre option consiste à utiliser une classe locale à l'intérieur de votre singleton en utilisant des membres statiques

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
la source
7

Ajoutez les 6 lignes suivantes à n'importe quelle classe pour en faire un "Singleton".

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

Exemple de test:

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

[Edit]: Utilisez Alex answer si vous préférez obtenir l'instance via une propriété plutôt qu'une méthode.

Flavien Volken
la source
Que se passe-t-il quand je fais new MySingleton(), disons 5 fois? votre code réserve-t-il une seule instance?
Hlawuleka MAS
vous ne devriez jamais utiliser "new": comme Alex l'a écrit, le constructeur devrait être "private", empêchant de faire "new MySingleton ()". La bonne utilisation est d'obtenir une instance en utilisant MySingleton.getInstance (). AKAIK no constructor (comme dans mon exemple) = un constructeur vide public
Flavien Volken
"vous ne devriez jamais utiliser" nouveau "- exactement ce que je veux dire:". Mais comment votre implémentation m'empêche-t-elle de le faire? Je ne vois nulle part où vous avez un constructeur privé dans votre classe?
Hlawuleka MAS
@HlawulekaMAS Je n'ai pas ... J'ai donc édité la réponse, notez qu'un constructeur privé n'était pas possible avant TS 2.0 (c'est à dire au moment où j'ai écrit la réponse en premier)
Flavien Volken
"c'est-à-dire au moment où j'ai écrit la réponse en premier" - Cela a du sens. Cool.
Hlawuleka MAS
3

je pense que peut-être utiliser des génériques

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

comment utiliser

étape 1

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

étape 2

    MapManager.Instance(MapManager).init();
sanye
la source
3

Vous pouvez également utiliser la fonction Object.Freeze () . C'est simple et facile:

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
la source
Kenny, bon point sur freeze (), mais deux notes: (1) après avoir freeze (singleton), vous pouvez toujours modifier singleton.data .. vous ne pouvez pas ajouter d'autre attribut (comme data2), mais le point est que freeze ( ) n'est pas deep freeze :) et (2) votre classe Singleton permet de créer plus d'une instance (exemple obj1 = new Singleton (); obj2 = new Singleton ();), donc votre Singleton n'est pas Singleton
:)
Si vous importez la classe Singleton dans d'autres fichiers, vous obtiendrez toujours la même instance et les données dans 'data' seront cohérentes entre toutes les autres importations. C'est pour moi un singleton. Le gel pour s'assurer que l'instance Singleton exportée n'est créée qu'une seule fois.
kenny le
Kenny, (1) si vous importez votre classe dans d'autres fichiers, vous n'obtiendrez pas d'instance. En important, vous introduisez simplement la définition de classe dans la portée afin de pouvoir créer de nouvelles instances. Ensuite, vous pouvez créer> 1 instances de la classe donnée, que ce soit dans un fichier ou plusieurs fichiers, ce qui défie tout le but de l'idée de singleton. (2) À partir de la documentation: La méthode Object.freeze () gèle un objet. Un objet gelé ne peut plus être modifié; le gel d'un objet empêche l'ajout de nouvelles propriétés. (fin de citation) Ce qui signifie que freeze () ne vous empêche pas de créer plusieurs objets.
Dmitry Shevkoplyas
C'est vrai, mais dans ce cas, ce sera le cas, car le membre exporté est déjà une instance. Et l'instance conserve les données. Si vous mettez également une exportation sur la classe, vous avez raison et vous pouvez créer plusieurs instances.
kenny le
@kenny si vous savez que vous allez exporter une instance, pourquoi s'embêter avec le if (!this.instance)dans le constructeur? Est-ce juste une précaution supplémentaire au cas où vous auriez créé plusieurs instances avant l'exportation?
Alex
2

J'ai trouvé une nouvelle version de ceci que le compilateur Typescript est tout à fait d'accord, et je pense qu'elle est meilleure car elle ne nécessite pas d'appeler une getInstance()méthode en permanence.

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

Cela présente un inconvénient différent. Si vous Singletonavez des propriétés, le compilateur Typescript lèvera un ajustement à moins que vous ne les initialisiez avec une valeur. C'est pourquoi j'ai inclus une _expresspropriété dans mon exemple de classe car à moins que vous ne l'initialisiez avec une valeur, même si vous l'assignez plus tard dans le constructeur, Typescript pensera qu'elle n'a pas été définie. Cela pourrait être résolu en désactivant le mode strict, mais je préfère ne pas le faire si possible. Il y a aussi un autre inconvénient à cette méthode que je dois souligner, car le constructeur est en fait appelé, chaque fois qu'il le fait, une autre instance est techniquement créée, mais pas accessible. Cela pourrait, en théorie, provoquer des fuites de mémoire.

Eagerestwolf
la source
1

C'est probablement le processus le plus long pour créer un singleton en tapuscrit, mais dans les applications plus importantes, c'est celui qui a le mieux fonctionné pour moi.

Vous avez d'abord besoin d'une classe Singleton dans, disons, "./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; }
    }
}

Imaginez maintenant que vous ayez besoin d'un singleton de routeur "./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();
}

Nice !, maintenant initialisez ou importez où vous en avez besoin:

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

import router = navigation.Router;

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

L'avantage de faire des singletons de cette façon est que vous utilisez toujours toute la beauté des classes dactylographiées, cela vous donne une belle intelligence, la logique des singleton reste en quelque sorte séparée et elle est facile à supprimer si nécessaire.

a.guerrero.g87
la source
1

Ma solution pour cela:

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;
    }
}
Daniel de Andrade Varela
la source
1
Dans le constructeur, au lieu de l'exception, vous pouvez return Modal._instance. De cette façon, si vous newutilisez cette classe, vous obtenez l'objet existant, pas un nouveau.
Mihai Răducanu
1

Dans Typescript, il n'est pas nécessaire de suivre les new instance() méthodologie Singleton. Une classe statique importée sans constructeur peut également fonctionner.

Considérer:

export class YourSingleton {

   public static foo:bar;

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

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

Vous pouvez importer la classe et y faire référence YourSingleton.doThing()dans n'importe quelle autre classe. Mais rappelez-vous, comme il s'agit d'une classe statique, elle n'a pas de constructeur, donc j'utilise généralement une intialise()méthode appelée à partir d'une classe qui importe le Singleton:

import {YourSingleton} from 'singleton.ts';

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

N'oubliez pas que dans une classe statique, chaque méthode et variable doit également être statique, donc au lieu d' thisutiliser le nom complet de la classe YourSingleton.

Dominic Lee
la source
0

Voici encore une autre façon de le faire avec une approche javascript plus conventionnelle utilisant un 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();

Voir une démo

JesperA
la source
Quelle est la raison d'ajouter la Instancevariable? Vous pouvez simplement mettre la variable et les fonctions directement sous App.Counter.
fyaa
@fyaa Oui, vous pourriez mais la variable et les fonctions directement sous App.Counter mais je pense que cette approche est mieux conforme au modèle singleton en.wikipedia.org/wiki/Singleton_pattern .
JesperA
0

Une autre option consiste à utiliser des symboles dans votre module. De cette façon, vous pouvez protéger votre classe, même si l'utilisateur final de votre API utilise du Javascript normal:

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

Usage:

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

Si l'utilisateur utilise votre fichier API js compilé, il obtiendra également une erreur s'il tente d'instancier manuellement votre classe:

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

Pas un singleton pur (l'initialisation peut ne pas être paresseuse), mais un modèle similaire à l'aide de namespaces.

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

Accès à l'objet de Singleton:

MyClass.instance
Sergzach
la source
0

C'est le moyen le plus simple

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;
  }
}
Sorin Veștemean
la source
-1
namespace MySingleton {
  interface IMySingleton {
      doSomething(): void;
  }
  class MySingleton implements IMySingleton {
      private usePrivate() { }
      doSomething() {
          this.usePrivate();
      }
  }
  export var Instance: IMySingleton = new MySingleton();
}

De cette façon, nous pouvons appliquer une interface, contrairement à la réponse acceptée de Ryan Cavanaugh.

user487779
la source
-1

Après avoir parcouru ce fil et joué avec toutes les options ci-dessus, je me suis installé avec un Singleton qui peut être créé avec les constructeurs appropriés:

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

}

Cela nécessiterait une configuration initiale (dans main.tsou index.ts), qui peut facilement être implémentée par
new Singleton(/* PARAMS */)

Ensuite, n'importe où dans votre code, appelez simplement Singleton.instnace; dans ce cas, pour workfaire, j'appelleraisSingleton.instance.work()

TheGeekZn
la source
Pourquoi quelqu'un voterait-il contre une réponse sans vraiment commenter les améliorations? Nous sommes une communauté
TheGeekZn