Singleton é um anti-padrão?


O padrão Singleton garante a existência de apenas uma instância de uma classe, mantendo um ponto global de acesso ao seu Objeto.

O conceito do Singleton não está errado, mas sua implementação clássica está.

Imagem

Ter um ponto global de acesso é bom.

Garantir uma única instância para um Objeto, pode ser bom.

O erro é ter uma única instância de uma classe.

Por que?

Simples. Você não deve utilizar pontos globais com Referência Direta em seu código.

Quando digo Referência Direta quero dizer:

  • Variáveis globais
  • Métodos estáticos que não trabalham com Interfaces

Variáveis globais

Você já deve saber que não devemos utilizar variáveis globais.

Seu uso é considerado um anti-padrão porque seu conteúdo pode ser modificado/acessado de qualquer parte do seu código. Haverá uma dependência direta dessa variável aumentando a complexidade do código. Qualquer alteração no valor dessa variável poderá ocasionar problemas em qualquer parte do código que a utilize.

Métodos estáticos

Um método estático é acessado diretamente na Classe. Assim como uma variável global, o método estático é uma dependência direta e isso não é bom. No entanto o valor retornado por um método estático pode ser dinâmico. Pode ser um Objeto. Pode ser uma instância de uma Interface.

O Método New() é estático, mas seu retorno é dinâmico. Ele também retorna uma instância de uma Interface ao invés de um tipo primitivo ou Classe.

O Padrão Singleton

Um Singleton utiliza uma variável global, que está encapsulada dentro da própria classe. Também utiliza um método estático, que irá retornar a instância única encapsulada na Classe. Essa instância é do tipo da própria Classe.

Precisamos utilizar esse Padrão?

Sim, por simplicidade.

Explico.

É possível nunca utilizar o Singleton. Basta você utilizar Injeção de Dependência nos construtores das Classes.

Mas para cada dependência teríamos que definir um argumento em todos os construtores, de todas as Classes, que necessitam utilizar a dependência.

Isso irá gerar Complexidade no código.

Você precisa de uma única instância de Objetos para:

  • Conexão com um SGBD
  • Log de execução/debugging
  • Configurações da Aplicação
  • Etc

São exemplos. Na verdade eu posso querer utilizar um pool de conexões para o SGBD; ter várias Classes que representam as Configurações, etc. Mas, você entendeu.

Então garantir a existência de apenas uma instância mantendo um ponto global de acesso ao Objeto é importante. Isso minimiza a complexidade. Ao invés de ter sempre um parâmetro a mais em todos os construtores das Classes — ou da maioria delas — eu só preciso ter um único ponto de acesso para “algum” Objeto.

O Singleton clássico parece resolver isso. Sim, mas ele nos traz um problema maior do que o benefício.

O Problema

Não há como implementar um Singleton sem haver uma variável para retornar uma única instância.

O problema não é a variável, pois ela estará encapsulada na Classe. Isso quer dizer que ela estará protegida. Bem, talvez não tão protegida mas ainda melhor que uma variável global.

O problema também não é o método estático — mesmo tendo seu uso desencorajado na maioria dos casos — pois podemos retornar qualquer valor.

O real problema do Padrão Singleton, em sua implementação clássica, é o tipo de retorno do método estático.

Implementação Clássica

Existem variações de implementação do Padrão Singleton que utilizam algumas técnicas para inicializar a variável encapsulada, ou seja, a instância que será retornada pelo método estático que, na maioria das vezes, é denominado GetInstance.

Na implementação mais básica, que pode ser feita em praticamente qualquer linguagem Orientada a Objetos, implementamos construtores da Classe como privados. Assim o utilizador da Classe não conseguirá instanciar Objetos dessa Classe diretamente. Depois é definido um (ou mais) método estático — GetInstance — para retornar uma instância da Classe… do mesmo tipo da Classe!

Aí está o problema.

Um grave problema.

Se você retornar uma instância do mesmo tipo da Classe, seu código terá uma dependência direta com essa Classe. Só isso pode arruinar qualquer tentativa de testes automatizados no código.

Veja um exemplo:

procedure TUserAction.ChangePassword(
  User: IUser; const NewPassword: string);
begin
  if NewPassword = '' then
    raise Exception.Create('Invalid Password');
  if Length(NewPassword) < 8 then
    raise Exception.Create('Must have 8 characters or more');
  User.Password := NewPassword;
  TMSSQL.GetInstance.Save(User);
end;

É um código idiota, mas serve ao propósito.

Vejamos.

Existe um Classe TUserAction que tem um método para alterar o password do usuário logado. Esse método tem algumas validações simples.

Alteramos seu password e depois essa alteração será persistida no SGBD.

A chamada TMSSQL.GetInstance é um Singleton. O método irá retornar uma conexão TMSSQLConnection para o MSSQL.

Utilizamos então o método Save(User) para persistir as alterações.

Não importa como Save(User) sabe como persistir um usuário. Isso não é relevante. O problema aqui é como iremos testar ChangePassword sem fazer a integração/conexão real com o SGBD (MSSQL).

Eu só quero testar o método utilizando um teste automatizado. Eu não quero ter que conectar num SGBD real, utilizar user/password reais e muito menos alterar algum dado.

Quero fazer testes de unidade, não testes de integração.

Bem, se TMSSQL.GetInstance me retorna uma instância de TMSSQLConnection, uma Classe concreta, não é possível fazer o teste automatizado.

É um exemplo simples. Vejo isso na maioria dos códigos “Object Pascal” — que não tem nada de Orientação a Objetos.

Na verdade, a maioria iria utilizar uma instância de algum DataModule e iria substituir a chamada TMSSQL.GetInstance por apenas DM.conMSSQL ou seja, duas variáveis. Triste.

Entendeu por que não devemos utilizar uma instância de Classe concreta?

E qual a solução?

Retornar uma instância de Interface ao invés de uma instância do tipo Classe.

Implementação Sugerida

O que irei sugerir para a implementação de um Singleton é tão simples quanto parece, mas com um ganho incontestável: Desacoplamento.

Utilize Interfaces.

Ao invés de TMSSQL.GetInstance retornar um TMSSQLConnection, o método irá retornar um IConnection ou seja, uma Interface.

Se agora temos um retorno que é uma Interface, podemos ter qualquer Classe que implemente essa Interface.

A Classe TMSSQLConnection deverá implementar IConnection.

E onde está desacoplamento?

Você poderá retornar qualquer instância que implemente IConnection e isso pode ser feito de várias formas.

Se quiser escrever menos, utilize diretivas de compilação.

class function TMSSQL.GetInstance: IConnection;
begin
  if not Assigned(FInstance) then
  begin
    {$IFDEF TEST}
      FInstance := TFakeConnection.Create;
    {$ELSE}
      FInstance := TMSSQLConnection.Create('user', 'password');
    {$ENDIF}
  end;
  Result := FInstance;
end;

Essa é a maneira “Rápido e sujo”. Você pode começar por aí caso nunca tenha pensado nisso. Quando estiver em “modo de teste”, ative a diretiva TEST, do contrário a Classe de produção será utilizada.

A Classe TFakeConnection não se conecta em nenhum SGBD. Talvez você possa utilizá-la para fazer um log das instruções SQL ou não fazer nada quando chamar o método Save.

Se você está testando as Regras de Negócio, a persistência é irrelevante.

Depois você pode refatorar esse código e deixá-lo mais Orientado a Objetos, utilizando Injeção de Dependência em conjunto com Abstract Factory Pattern.

Como?

Bem, em algum lugar no seu código você deverá ter uma chamada para “inicializar” a Classe TMSSQL.

class function TMSSQL.Initialize(Factory: IConnectionFactory);
begin
  FFactory := Factory;
end;

initialization
  TMSSQL.Initialize(TDbConnectionFactory.New('mssql'));

end.

Mais um vez estaremos utilizando métodos estáticos. Mas estamos em busca de simplicidade. Não devemos ser puristas em Orientação a Objetos se não há nenhum benefício. Sempre haverá partes do código que poderiam melhorar, refatorar, eliminar, etc. Enfim.

A Classe TMSSQL deverá ser bem pequena e sem complexidade.

O atributo FFactory é um class var assim como FInstance.

Então você poderá ter novas Classes que implementam IConnectionFactory.

No exemplo temos TDbConnectionFactory. Optei por utilizar um parâmetro que me diz qual “tipo” de Classe será utilizada. Se eu quiser testar o código bastaria chamar TDbConnectionFactory.New('test').

Utilize seu estilo.

Após essa alteração o método GetInstance pode ser refatorado:

class function TMSSQL.GetInstance: IConnection;
begin
  if not Assigned(FInstance) then
    FInstance := FFactory.NewConnection(FUser, FPassword);
  Result := FInstance;
end;

A chamada a FFactory.NewConnection irá gerar uma nova instância de IConnection. Qual a Classe que estará sendo utilizada é irrelevante para o código que utiliza TMSSQL.GetInstance em todos os lugares, como deve ser. O retorno poderia ser TMSSQLConnection, TFakeConnection ou qualquer outra classe que implemente IConnection.

Esse é um Singleton simples, desacoplado e Orientado a Objetos.

Até logo.

Posts Relacionados

  • Mutabilidade Encapsulada

  • Atributos Primários e Secundários

  • Estado do Objeto

  • Herdar para Rotular

  • Observando Objetos

  • Eventos e Objetos

  • Organização de Projetos

  • James e Testes de Unidade

  • James Package

  • Objetos Puros