Como definir Singleton no TypeScript

128

Qual é a melhor e mais conveniente maneira de implementar um padrão Singleton para uma classe no TypeScript? (Com e sem inicialização lenta).

maja
fonte

Respostas:

87

As classes Singleton no TypeScript geralmente são um anti-padrão. Você pode simplesmente usar espaços para nome .

Padrão singleton inútil

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

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

Equivalente de espaço para nome

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
fonte
55
seria bom agora por que o singleton é considerado um anti-padrão? considerar esta abordagem codebelt.com/typescript/typescript-singleton-pattern
Victor
21
Gostaria de saber por que os Singletons no TypeScript também são considerados antipadrão. E também se ele não possui nenhum parâmetro construtor, por que não export default new Singleton()?
emzero
23
A solução namespace parece mais com uma classe estática, não um singleton
Mihai Răducanu
6
Comporta-se da mesma maneira. Em C #, você não pode transmitir uma classe estática como se fosse um valor (ou seja, como se fosse uma instância de uma classe singleton), o que limita sua utilidade. No TypeScript, você pode passar um espaço para nome como uma instância. É por isso que você não precisa de aulas singleton.
Ryan Cavanaugh
13
Uma limitação do uso de um espaço de nome como um singleton é que ele não pode (que eu saiba) implementar uma interface. Você concordaria com isso @ryan
Gabe O'Leary
182

Desde o TS 2.0, temos a capacidade de definir modificadores de visibilidade em construtores ; portanto, agora podemos fazer singletons no TypeScript da mesma maneira que estamos acostumados em outras linguagens.

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

Obrigado à @Drenai por salientar que, se você escrever código usando o javascript compilado bruto, não terá proteção contra várias instâncias, pois as restrições do TS desaparecem e o construtor não fica oculto.

Alex
fonte
2
o construtor poderia ser privado?
Expert quer ser
2
@Expertwannabe Agora já está disponível no TS 2.0: github.com/Microsoft/TypeScript/wiki/…
Alex
3
Esta é a minha resposta preferida! Obrigado.
Martin Majewski
1
fyi, o motivo das várias instâncias foi a resolução do módulo do nó que atrapalhava. Portanto, se você estiver criando um singleton no nó, verifique se isso é considerado. Acabei criando uma pasta node_modules no meu diretório src e colocando o singleton lá.
Webteckie
3
@KimchiMan Se o projeto for usado em um ambiente não-datilografado, por exemplo, importado para um projeto JS, a classe não terá proteção contra instâncias adicionais. Ele funciona em um ambiente de TS pura apenas, mas não para JS desenvolvimento da biblioteca
Drenai
39

A melhor maneira que eu encontrei é:

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

}

Aqui está como você o usa:

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
fonte
3
Por que não tornar o construtor privado?
Phil Mander
4
Eu acho que o post é anterior à capacidade de ter construtores privados no TS. github.com/Microsoft/TypeScript/issues/2341
Trevor
Eu gosto desta resposta. Construtores privados são ótimos durante o desenvolvimento, mas se um módulo TS transpilado for importado para um ambiente JS, o construtor ainda poderá ser acessado. Com esta abordagem é quase protegidos contra o uso indevido .... a menos que o SingletonClass [ '_ instância'] é definido como nulo / indefinido
Drenai
O link está quebrado. Eu acho que este é o link real: codebelt.github.io/blog/typescript/typescript-singleton-pattern
El Asiduo
24

A abordagem a seguir cria uma classe Singleton que pode ser usada exatamente como uma classe 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()operação retornará a mesma instância. No entanto, isso pode ser inesperado pelo usuário.

O exemplo a seguir é mais transparente para o usuário, mas requer um 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
fonte
1
É assim que deve ser implementado. Se há uma coisa em que não concordo com A Gangue dos Quatro - e provavelmente é apenas uma - é o Padrão Singleton. Talvez o C / ++ impeça alguém de projetá-lo dessa maneira. Mas se você me perguntar, o código do cliente não deve saber ou se importar se é um Singleton. Os clientes ainda devem implementar a new Class(...)sintaxe.
Cody
16

Estou surpreso por não ver o seguinte padrão aqui, que realmente parece muito simples.

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

export let Shout = new ShoutSingleton();

Uso

import { Shout } from './shout';
Shout.helloWorld();
Romain Bruckert
fonte
Recebi a seguinte mensagem de erro: A variável exportada 'Shout' possui ou está usando o nome privado 'ShoutSingleton'.
Twois
3
Você também deve exportar a classe 'ShoutSingleton' e o erro desaparece.
Twois
Certo, também estou surpreso. Por que se preocupar com a aula? Os singletons devem esconder seu funcionamento interno. Por que não apenas exportar a função helloWorld?
Oleg Dulin
veja esta questão github para mais informações: github.com/Microsoft/TypeScript/issues/6307
Ore4444
5
Acho que nada está impedindo que os usuários criem apenas uma nova Shoutclasse
#
7

Você pode usar expressões de classe para isso (a partir da versão 1.6, acredito).

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

ou com o nome se sua turma precisar acessar seu tipo internamente

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

Outra opção é usar uma classe local dentro do seu singleton usando alguns membros 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
fonte
7

Adicione as 6 linhas a seguir a qualquer classe para torná-la "Singleton".

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

Exemplo de teste:

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

[Editar]: Use a resposta de Alex se você preferir obter a instância através de uma propriedade e não de um método.

Flavien Volken
fonte
O que acontece quando eu new MySingleton()digo, digamos 5 vezes? seu código reserva uma única instância?
Hlawuleka MAS
você nunca deve usar "new": como Alex escreveu, o construtor deve ser "privado", impedindo a criação de "new MySingleton ()". O uso correto é obter uma instância usando MySingleton.getInstance (). AKAIK nenhum construtor (como no meu exemplo) = um construtor vazio pública
Flavien Volken
"você nunca deve usar" novo "- exatamente o que quero dizer:". Mas como sua implementação me impede de fazer isso? Não vejo nenhum lugar em que você tenha um construtor privado em sua classe?
Hlawuleka MAS
@HlawulekaMAS eu não ... Por isso, eu editei a resposta, nota que um construtor privado não era possível antes TS 2.0 (ou seja, no momento em que escrevi a resposta primeiro)
Flavien Volken
"ou seja, no momento em que escrevi a resposta primeiro" - faz sentido. Legal.
Hlawuleka MAS
3

eu acho que talvez use genéricos seja massa

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

Como usar

passo 1

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

passo 2

    MapManager.Instance(MapManager).init();
Sanye
fonte
3

Você também pode fazer uso da função Object.Freeze () . É simples e 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
fonte
Kenny, bom ponto no freeze (), mas duas notas: (1) depois de congelar (singleton), você ainda pode modificar o singleton.data. Não é possível adicionar outro atributo (como data2), mas o ponto é esse congelamento ( ) não está congelado :) e (2) sua classe Singleton permite criar mais de uma instância (exemplo obj1 = novo Singleton (); obj2 = novo Singleton ();), portanto, seu Singleton não é Singleton
:)
Se você importar a Classe Singleton em outros arquivos, sempre obterá a mesma instância e os dados em 'data' serão consistentes entre todas as outras importações. Isso é para mim um singleton. O congelamento ao garantir que a instância Singleton exportada seja criada apenas uma vez.
Kenny
Kenny, (1) se você importar sua classe para outros arquivos, não receberá instância. Ao importar, você simplesmente coloca a definição de classe no escopo para poder criar novas instâncias. Em seguida, você pode criar> 1 instâncias da classe especificada, seja em um arquivo ou em vários arquivos, o que desafia todo o propósito da idéia de singleton. (2) Dos documentos: O método Object.freeze () congela um objeto. Um objeto congelado não pode mais ser alterado; congelar um objeto impede que novas propriedades sejam adicionadas a ele. (final da citação) O que significa congelar () não impede a criação de vários objetos.
Dmitry Shevkoplyas
É verdade, mas neste caso será, porque o membro exportado já é uma instância. E a instância mantém os dados. Se você colocar uma exportação na classe também, estará certo e poderá criar várias instâncias.
Kenny
@kenny se você sabe que vai exportar uma instância, por que se preocupar com if (!this.instance)o construtor? Isso é apenas uma precaução extra caso você tenha criado várias instâncias antes da exportação?
24419 Alex
2

Eu encontrei uma nova versão disso com a qual o compilador Typescript está totalmente de acordo, e acho que é melhor porque não requer a chamada getInstance()constante de um método.

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

Isso vem com uma desvantagem diferente. Se você Singletontiver alguma propriedade, o compilador Typescript será um ajuste, a menos que você as inicialize com um valor. É por isso que incluí uma _expresspropriedade na minha classe de exemplo, porque, a menos que você a inicialize com um valor, mesmo que você a atribua posteriormente no construtor, o Typescript pensará que ela não foi definida. Isso pode ser corrigido desativando o modo estrito, mas prefiro não, se possível. Há também outra desvantagem desse método que devo salientar, porque o construtor está realmente sendo chamado, cada vez que faz outra instância é tecnicamente criada, mas não acessível. Isso poderia, em teoria, causar vazamentos de memória.

Eagerestwolf
fonte
1

Este é provavelmente o processo mais longo para criar um singleton em texto datilografado, mas em aplicativos maiores é o que funcionou melhor para mim.

Primeiro você precisa de uma classe Singleton em, 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; }
    }
}

Agora imagine que você precisa de um roteador 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();
}

Nice !, agora inicialize ou importe onde você precisar:

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

import router = navigation.Router;

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

O bom de fazer singletons dessa maneira é que você ainda usa toda a beleza das classes datilografadas, isso proporciona um bom senso de sentido, a lógica do singleton se mantém de alguma maneira separada e é fácil de remover, se necessário.

a.guerrero.g87
fonte
1

Minha solução para isso:

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
fonte
1
No construtor, em vez da exceção, você pode return Modal._instance. Dessa forma, se você newclassifica essa classe, obtém o objeto existente, não um novo.
Mihai Răducanu
1

No texto datilografado, não é necessário necessariamente seguir a new instance()metodologia Singleton. Uma classe estática importada e sem construtores também pode 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
   }
}

Você pode importar a classe e consultar YourSingleton.doThing()em qualquer outra classe. Mas lembre-se, como essa é uma classe estática, ela não tem construtor, por isso geralmente uso um intialise()método chamado de uma classe que importa o Singleton:

import {YourSingleton} from 'singleton.ts';

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

Não esqueça que, em uma classe estática, todos os métodos e variáveis ​​também precisam ser estáticos, portanto, em vez de thisvocê usar o nome completo da classe YourSingleton.

Dominic Lee
fonte
0

Aqui está mais uma maneira de fazer isso com uma abordagem javascript mais convencional usando um 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 uma demonstração

JesperA
fonte
Qual é o motivo para adicionar a Instancevariável? Você pode simplesmente colocar a variável e as funções diretamente abaixo App.Counter.
fyaa 01/02
@fyaa Sim, você poderia, mas a variável e as funções diretamente no App.Counter, mas acho que essa abordagem se adapta melhor ao padrão singleton en.wikipedia.org/wiki/Singleton_pattern .
JesperA
0

Outra opção é usar símbolos no seu módulo. Dessa forma, você pode proteger sua classe, também se o usuário final da sua API estiver 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();

Se o usuário estiver usando o arquivo js da API compilada, também ocorrerá um erro se ele tentar instanciar manualmente sua classe:

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

Não é um singleton puro (a inicialização pode não ser preguiçosa), mas padrão semelhante com a ajuda de namespaces.

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

Acesso ao objeto de Singleton:

MyClass.instance
sergzach
fonte
0

Esta é a maneira mais simples

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

Dessa forma, podemos aplicar uma interface, diferente da resposta aceita por Ryan Cavanaugh.

user487779
fonte
-1

Depois de vasculhar esta discussão e brincar com todas as opções acima - resolvi com um Singleton que pode ser criado com os construtores adequados:

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

}

Isso exigiria uma configuração inicial (em main.ts, ou index.ts), que pode ser facilmente implementada por
new Singleton(/* PARAMS */)

Então, em qualquer lugar do seu código, basta ligar Singleton.instnace; neste caso, para workterminar, eu chamariaSingleton.instance.work()

TheGeekZn
fonte
Por que alguém rebateu uma resposta sem comentar sobre as melhorias? Somos uma comunidade
TheGeekZn