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

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

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.


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/


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


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

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

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.


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

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


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.


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

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.


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


¿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

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

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

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


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


¿Por qué alguien rechazaría una respuesta sin comentar realmente las mejoras? Somos una comunidad
TheGeekZn
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.