Cómo definir Singleton en TypeScript

128

¿Cuál es la forma mejor y más conveniente de implementar un patrón Singleton para una clase en TypeScript? (Tanto con y sin inicialización perezosa).

maja
fuente

Respuestas:

87

Las clases Singleton en TypeScript son generalmente un antipatrón. Simplemente puede usar espacios de nombres en su lugar.

Patrón singleton inútil

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

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

Equivalente de espacio de nombres

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
fuente
55
Sería bueno ahora ¿por qué el singleton se considera un anti patrón? considere este enfoque codebelt.com/typescript/typescript-singleton-pattern
Victor
21
Me gustaría saber por qué los Singletons en TypeScript también se consideran un antipatrón. Y también si no tiene ningún parámetro de constructor, ¿por qué no export default new Singleton()?
emzero
23
La solución de espacio de nombres se parece más a una clase estática, no a un singleton
Mihai Răducanu
66
Se comporta igual. En C #, no puede pasar una clase estática como si fuera un valor (es decir, como si fuera una instancia de una clase singleton), lo que limita su utilidad. En TypeScript, puede pasar un espacio de nombres como una instancia. Es por eso que no necesitas clases de singleton.
Ryan Cavanaugh
13
Una limitación del uso de un espacio de nombres como singleton es que no puede (que yo sepa) implementar una interfaz. ¿Estaría de acuerdo con esto @ryan
Gabe O'Leary
182

Desde TS 2.0, tenemos la capacidad de definir modificadores de visibilidad en los constructores , por lo que ahora podemos hacer singletons en TypeScript como estamos acostumbrados desde otros lenguajes.

Ejemplo dado:

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;

Gracias @Drenai por señalar que si escribes código usando el javascript compilado sin procesar no tendrás protección contra la creación de instancias múltiples, ya que las restricciones de TS desaparecen y el constructor no estará oculto.

Alex
fuente
2
El constructor podría ser privado?
Experto quiere ser
2
@Expertwannabe Ahora está disponible en TS 2.0: github.com/Microsoft/TypeScript/wiki/…
Alex
3
Esta es mi respuesta preferida! Gracias.
Martin Majewski
1
Para su información, la razón de las múltiples instancias fue que la resolución del módulo de nodo se interpuso en el camino. Por lo tanto, si está creando un singleton en el nodo, asegúrese de que eso se tenga en cuenta. Terminé creando una carpeta node_modules en mi directorio src y colocando el singleton allí.
webteckie
3
@KimchiMan Si el proyecto se usa alguna vez en un entorno que no sea de tipo mecanografiado, por ejemplo, importado a un proyecto JS, la clase no tendrá protección contra nuevas instancias. Funciona solo en un entorno TS puro, pero no para el desarrollo de la biblioteca JS
Drenai
39

La mejor manera que he encontrado es:

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

}

Así es como lo usas:

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
fuente
3
¿Por qué no hacer que el constructor sea privado?
Phil Mander
44
Creo que la publicación es anterior a la capacidad de tener constructores privados en TS. github.com/Microsoft/TypeScript/issues/2341
Trevor
Me gusta esta respuesta Los constructores privados son excelentes durante el desarrollo, pero si un módulo TS transpilado se importa a un entorno JS, aún se puede acceder al constructor. Con este enfoque, está casi protegido contra el mal uso ... a menos que SingletonClass ['_ instancia'] esté configurado como nulo / indefinido
Drenai
El enlace está roto. Creo que este es el enlace real: codebelt.github.io/blog/typescript/typescript-singleton-pattern
El Asiduo
24

El siguiente enfoque crea una clase Singleton que puede usarse exactamente como una clase convencional:

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

Cada new Singleton()operación devolverá la misma instancia. Sin embargo, esto puede ser inesperado por el usuario.

El siguiente ejemplo es más transparente para el usuario pero requiere un uso diferente:

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

Uso: var obj = Singleton.getInstance();

maja
fuente
1
Esta es la forma en que debe implementarse. Si hay 1 cosa en la que no estoy de acuerdo con The Gang of Four, y probablemente sea solo 1, es The Singleton Pattern. Quizás, C / ++ impide a uno diseñarlo de esta manera. Pero si me preguntas, el código del cliente no debería saber o importar si es Singleton. Los clientes aún deben implementar la new Class(...)sintaxis.
Cody
16

Me sorprende no ver el siguiente patrón aquí, que en realidad parece muy simple.

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

export let Shout = new ShoutSingleton();

Uso

import { Shout } from './shout';
Shout.helloWorld();
Romain Bruckert
fuente
Recibí el siguiente mensaje de error: La variable exportada 'Shout' tiene o está usando el nombre privado 'ShoutSingleton'.
Twois
3
También debe exportar la clase 'ShoutSingleton' y el error desaparecerá.
Twois
Bien, yo también estoy sorprendido. ¿Por qué incluso molestarse con la clase? Se supone que los Singleton ocultan su funcionamiento interno. ¿Por qué no simplemente exportar la función helloWorld?
Oleg Dulin
vea este número de github para obtener más información: github.com/Microsoft/TypeScript/issues/6307
Ore4444
55
Sin Shoutembargo
supongo que
7

Puede usar expresiones de clase para esto (a partir de 1.6, creo).

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

o con el nombre si su clase necesita acceder internamente a su tipo

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

Otra opción es usar una clase local dentro de su singleton usando algunos miembros estáticos

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
fuente
7

Agregue las siguientes 6 líneas a cualquier clase para que sea "Singleton".

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

Ejemplo de prueba:

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

[Editar]: use la respuesta de Alex si prefiere obtener la instancia a través de una propiedad en lugar de un método.

Flavien Volken
fuente
¿Qué pasa cuando lo hago new MySingleton(), digo 5 veces? ¿Su código reserva una sola instancia?
Hlawuleka MAS
nunca debe usar "nuevo": como Alex escribió, el constructor debe ser "privado", evitando hacer "nuevo MySingleton ()". El uso correcto es obtener una instancia usando MySingleton.getInstance (). AKAIK sin constructor (como en mi ejemplo) = un constructor público vacío
Flavien Volken
"nunca deberías usar" nuevo "- exactamente mi punto:". ¿Pero cómo su implementación me impide hacerlo? ¿No veo ningún lugar donde tenga un constructor privado en su clase?
Hlawuleka MAS
@HlawulekaMAS No lo hice ... Por lo tanto, edité la respuesta, tenga en cuenta que un constructor privado no era posible antes de TS 2.0 (es decir, en el momento en que escribí la respuesta primero)
Flavien Volken
"es decir, en el momento en que escribí la respuesta primero" - Tiene sentido. Frio.
Hlawuleka MAS
3

creo que tal vez usar genéricos sea la masa

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

cómo utilizar

paso 1

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

paso 2

    MapManager.Instance(MapManager).init();
sanye
fuente
3

También puede hacer uso de la función Object.Freeze () . Es simple y fácil:

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
fuente
Kenny, buen punto en congelar (), pero dos notas: (1) después de congelar (singleton), aún puedes modificar singleton.data ... no puedes agregar otro atributo (como data2), pero el punto es ese congelar ( ) no es congelación profunda :) y (2) su clase Singleton permite crear más de una instancia (ejemplo obj1 = new Singleton (); obj2 = new Singleton ();), entonces su Singleton no es Singleton
:)
Si importa la clase Singleton en otros archivos, siempre obtendrá la misma instancia y los datos en 'datos' serán consistentes entre todas las demás importaciones. Eso es para mí un singleton. La congelación se asegura de que la instancia de Singleton exportada solo se cree una vez.
kenny
Kenny, (1) si importas tu clase en otros archivos no obtendrás instancia. Al importar, simplemente está incorporando la definición de clase en el alcance para que pueda crear nuevas instancias. Luego puede crear> 1 instancias de la clase dada, ya sea en un archivo o en varios archivos, lo que desafía todo el propósito de la idea singleton. (2) Desde documentos: El método Object.freeze () congela un objeto. Un objeto congelado ya no se puede cambiar; congelar un objeto evita que se le agreguen nuevas propiedades. (fin de la cita) Lo que significa que congelar () no le impide crear múltiples objetos.
Dmitry Shevkoplyas
Es cierto, pero en este caso lo hará, porque el miembro exportado ya es una instancia. Y la instancia mantiene los datos. Si coloca una exportación en la clase también, tiene razón y podría crear varias instancias.
kenny
@kenny si sabe que va a exportar una instancia, ¿por qué molestarse con if (!this.instance)el constructor? ¿Es solo una precaución adicional en caso de que haya creado varias instancias antes de la exportación?
Alex
2

He encontrado una nueva versión de esto con la que el compilador deScript está totalmente de acuerdo, y creo que es mejor porque no requiere llamar a un getInstance()método constantemente.

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

Esto viene con un inconveniente diferente. Si Singletontiene propiedades, el compilador de Script mecanografiará un ajuste a menos que lo inicialice con un valor. Es por eso que incluí una _expresspropiedad en mi clase de ejemplo porque a menos que la inicialice con un valor, incluso si la asigna más adelante en el constructor, TypeScript pensará que no se ha definido. Esto podría solucionarse desactivando el modo estricto, pero prefiero no hacerlo si es posible. También hay otro inconveniente de este método que debo señalar, porque en realidad se llama al constructor, cada vez que lo hace se crea técnicamente otra instancia, pero no es accesible. Esto podría, en teoría, causar pérdidas de memoria.

Eagerestwolf
fuente
1

Este es probablemente el proceso más largo para hacer un singleton en mecanografiado, pero en aplicaciones más grandes es el que mejor me ha funcionado.

Primero necesita una clase Singleton en, digamos, "./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; }
    }
}

Ahora imagine que necesita un enrutador singleton "./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();
}

¡Agradable !, ahora inicialice o importe donde lo necesite:

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

import router = navigation.Router;

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

Lo bueno de hacer singletons de esta manera es que todavía usas toda la belleza de las clases de mecanografía, te da un buen sentido, la lógica singleton se mantiene separada y es fácil de eliminar si es necesario.

a.guerrero.g87
fuente
1

Mi solución para ello:

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
fuente
1
En el constructor, en lugar de la excepción, puedes hacerlo return Modal._instance. De esta manera, si newesa clase, obtienes el objeto existente, no uno nuevo.
Mihai Răducanu
1

En Typecript, uno no necesariamente tiene que seguir la new instance()metodología Singleton. Una clase estática importada, sin constructor, puede funcionar igualmente.

Considerar:

export class YourSingleton {

   public static foo:bar;

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

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

Puede importar la clase y consultarla YourSingleton.doThing()en cualquier otra clase. Pero recuerde, debido a que esta es una clase estática, no tiene constructor, por lo que generalmente uso un intialise()método que se llama desde una clase que importa Singleton:

import {YourSingleton} from 'singleton.ts';

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

No olvide que en una clase estática, todos los métodos y variables también deben ser estáticos, por lo que en lugar de thisusar el nombre completo de la clase YourSingleton.

Dominic Lee
fuente
0

Aquí hay otra forma de hacerlo con un enfoque de JavaScript más convencional utilizando 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();

Ver una demostración

JesperA
fuente
¿Cuál es la razón para agregar la Instancevariable? Podrías simplemente poner la variable y las funciones directamente debajo App.Counter.
fyaa 01 de
@fyaa Sí, podría hacerlo, pero la variable y las funciones directamente en App.Counter, pero creo que este enfoque se ajusta mejor al patrón singleton en.wikipedia.org/wiki/Singleton_pattern .
JesperA
0

Otra opción es usar símbolos en su módulo. De esta manera, puede proteger su clase, también si el usuario final de su API está usando 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";
    }
}

Uso:

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

Si el usuario está utilizando su archivo compilado API js, también recibirá un error si intenta crear una instancia manual de su clase:

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

No es un singleton puro (la inicialización puede no ser perezosa), sino un patrón similar con ayuda de namespaces.

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

Acceso al objeto de Singleton:

MyClass.instance
sergzach
fuente
0

Esta es la forma más sencilla

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

De esta manera podemos aplicar una interfaz, a diferencia de la respuesta aceptada de Ryan Cavanaugh.

usuario487779
fuente
-1

Después de recorrer este hilo y jugar con todas las opciones anteriores, me decidí por un Singleton que se puede crear con los constructores adecuados:

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

}

Se requeriría una configuración inicial (in main.tso index.ts), que puede implementarse fácilmente mediante
new Singleton(/* PARAMS */)

Luego, en cualquier parte de su código, simplemente llame Singleton.instnace; en este caso, para workterminar, llamaríaSingleton.instance.work()

TheGeekZn
fuente
¿Por qué alguien rechazaría una respuesta sin comentar realmente las mejoras? Somos una comunidad
TheGeekZn