Singleton: Lobo em pele de cordeiro, ou um pattern injustiçado?

March 20, 2010

Singleton é um design pattern (ou um anti-pattern, para alguns) que determina que só deve haver uma instancia de uma determinada classe em todo o sistema.

Há bastante controvérsia a respeito deste padrão, uns alegam que trata-se de um anti-pattern, isto é, algo que deve ser evitado. Enquanto os design-patterns são maneiras conhecidas e elegantes de resolver problemas recorrentes, os anti-patterns são maneiras que você NÃO deve utilizar para resolver problemas.

De fato, singletons, dependendo da maneira como são utilizados, podem gerar problemas com testes unitários, pois você não conseguirá alcançar o efeito de não deixar rastros do seus testes. Também não vai conseguir substitui-lo por um test-double, o que seria desejaval em alguns caso, como por exemplo se esse singleton acessa um arquivo no HD.

O problema nem está no fato de haver apenas uma instância do objeto. Em alguns casos isso é realmente necessário. O problema está na forma como esta instância é obtida.

Imagine a seguinte situação:

Seu sistema tem uma classe chamada TConfiguracoes que encapsula o acesso aos parâmetros de configuração do sistema, que são armazenados em um arquivo .ini junto à sua aplicação ou um banco de dados. Tal classe utiliza o padrão singleton, pois de fato não há sentido em ter mais de um objeto desta classe no sistema.

Você implementou a classe da seguinte forma:

unit UConfiguracoes;

interface

type
  IConfiguracoes = interface
    // guid
    // métodos de acesso aos parâmetros aqui
  end;

function Configuracoes: IConfiguracoes;

implementation

type
  TConfiguracoes = class(TInterfacedObject, IConfiguracoes)
  end;

var
  _Configuracoes: IConfiguracoes;

function Configuracoes: IConfiguracoes;
begin
  if not Assigned(_Configuracoes) then
    _Configuracoes := TConfiguracoes.Create;

  Result := _Configuracoes;
end;

end.

E usa da seguinte forma:

type
  IXXXService = interface
    // guid

    procedure FazAlgo;
  end;

  TXXXService = class(TInterfacedObject, IXXXService)
  private
    procedure FazAlgo;
  end;

implementation

{ TXXXService }

procedure TXXXService.FazAlgo;
begin
  if Configuracoes.ParametroY then
    FazIsso
  else
    FazAquilo;
end

Digamos que esta classe de configurações lê os seus parametros em um banco de dados. Durante os testes automatizados você terá que habilitar um banco de dados para esta classe poder funcionar, e correrá o risco de um teste enxergar mudanças feitas por testes anteriores, o que é algo ruim conforme eu expliquei anteriormente. É possível evitar isso se você zerar os dados do banco a cada teste, mas isso tornaria a execução dos mesmos mais demorada. Além do mais, seria uma configuração a mais para você se preocupar.

É desta forma que o singleton atrapalha os testes automatizados, sendo por isso tão criticado por alguns.

Como eu disse, o problema está na forma como a instância do singleton é obtida. Qual seria a forma mais apropriada então? Espero que a resposta seja obvia para quem leu este post. Para quem não leu, explico: Basta utilizar injeção de dependências. A nossa classe TXXXService ficaria assim:

type
  TXXXService = class(TInterfacedObject, IXXXService)
  private
    FConfiguracoes: IConfiguracoes;
    procedure FazAlgo;
  public
    constructor Create(const Configuracoes: IConfiguracoes);
  end;

implementation

{ TXXXService }

constructor TXXXService.Create(const Configuracoes: IConfiguracoes);
begin
  FConfiguracoes := Configuracoes;
end;

procedure TXXXService.FazAlgo;
begin
  if FConfiguracoes.ParametroY then
    FazIsso
  else
    FazAquilo;
end;

E para utilizar esta classe, seria assim:

var
  Service: IXXXService;
begin
  Service := TXXXService.Create(Configuracoes); { Lembrando que Configuracoes é uma função que criamos e que cuida do acesso ao nosso TConfiguracoes como singleton }
  Service.FazAlgo;
end;

E em testes unitários, basta criar uma implementação falsa de IConfiguracoes e passar como parâmetro no construtor de TXXXService.

Assim resolvemos o problema de testes de classes que dependem de singletons sem que precisemos deixar de utilizar singletons! Contudo, acho prudente fazer uma recomendação: Pense se você realmente precisa de um singleton. É uma [pequena] complexidade extra que você está inserindo no seu código, antes de fazer isso pense se realmente te trará alguma vantagem!

Agora, minha resposta à perguna-título do post? Eu diria que é um bom pattern, mas que requer muito cuidado de nossa parte para que não trata efeitos colaterais indesejaveis.

3 Responses to “Singleton: Lobo em pele de cordeiro, ou um pattern injustiçado?”

  1. Classes que interagem com o banco de dados realmente são mais trabalhosas para testar.

    Eu gosto de usar singletons em Delphi nos formulários que só precisam ter uma instância. No primeiro acesso, o formulário é criado e nos demais acessos, a primeira instância é aproveitada. Por tabela, isso também se torna um mecanismo de cache, evitando recriar um formulário que é aberto várias vezes durante a execução do programa.

    Parabéns por abordar um tema pouco conhecido entre desenvolvedores Delphi.

  2. Por isso é importante trabalhar os conceitos de separação de responsabilidades e abstrações, pois facilita os testes de classes que dependam de bancos de dados ou qualquer outro recurso externo.

    Quanto aos singletons de formulários, particularmente eu nunca fiz isso, mas acho seria um uso válido para singletons, sim. Uma pergunta: Em que momento a instância de um formulário é liberada da memória? Apenas no final da execução do aplicativo? E se o usuário carregar um grande conjunto de dados e fechar o formulário, tudo continuará na memória?

  3. Uso Singleton em formulários geralmente quando faz sentido que eles permaneçam na memória. Por exemplo, se a aplicação é para um consultório médico, o formulário da agenda merece ficar na memória mesmo depois de fechado, pois será reaberto com muita frequência. O mesmo para a ficha de paciente. Nesses casos, no final da aplicação o formulário é descarregado.

    Quanto a ter uma grande quantidade de dados carregada no formulário, lido com isso dependendo do caso. Se as informações serão reaproveitadas nas outras aberturas do formulário, posso considerar mais vantajoso deixa-las na memória. Senão, descarrego os dados que não serão reaproveitados ao fechar o formulário.

    Não tenho uma regra rígida; geralmente o bom senso é o meu guia. A sintonia entre performance e consumo de memória varia de acordo com a aplicação.

Leave a Reply

Spam protection by WP Captcha-Free