[Delphi] Design Patterns GoF – Singleton

[Delphi] Design Patterns - Singleton

Boa noite, pessoal, tudo certo?
Hoje é dia de finalizar a série de artigos sobre os padrões de projeto criacionais. A partir do próximo artigo, abordaremos os padrões da categoria “Estrutural”.
O último Design Pattern dessa natureza é o Singleton, do qual tenho certeza que já ouviram falar! Acompanhe o artigo para conhecer o propósito, a aplicabilidade e – o mais importante – os cuidados ao utilizar este padrão.

 

Já caíram em algumas situações em que era necessário instanciar e destruir objetos da mesma classe várias vezes? Por exemplo, imagine um classe que realiza cálculos para o Departamento Pessoal, como FGTS, INSS, abatimentos e créditos. Em cada uma dessas operações, é necessário instanciar um objeto da classe de cálculos, invocar a função desejada e então liberá-la da memória. Se os cálculos forem constantes, este procedimento torna-se redundante.


Bom, poderíamos ter um objeto global para ser utilizado em todos os métodos, certo?

Concordo. É uma solução. Porém, vou um pouco mais além: suponha, então, que essa classe de cálculos também é utilizada com frequência nos módulos fiscais, financeiros e contábeis. Em suma, precisamos utilizá-la em telas distintas. Um objeto global não funcionaria, ao menos que fosse um objeto global da aplicação, e não de uma tela específica.
Opa, cheguei aonde queria! Este objeto global da aplicação pode ser definido como um Singleton!


Como assim?

O padrão de projeto Singleton tem o propósito de fornecer um ponto único de acesso à instância de um objeto, de modo que qualquer local da aplicação consiga acessá-lo.
Uma necessidade conhecida do Singleton é a leitura do perfil de usuários. Geralmente, em sistemas multiusuários, é comum buscar as permissões de acesso e outros dados do usuário (como customizações, padrões, temas, etc…) para cada tela que for acessada. Pensando do modo tradicional, cria-se um objeto da classe, faz a leitura dos dados solicitados, e o destrói em seguida.
Para evitar essa redundância de criação e liberação de objetos, podemos utilizar um Singleton. O objeto da classe será criada apenas uma vez (como na inicialização da aplicação) e essa mesma instância será utilizada por todos os locais que a solicitarem. É como disse anteriormente: o Singleton assemelha-se com um objeto global de toda a aplicação.


Mas qual são as vantagens?

Uma delas é a redução de processamento ao criar e liberar objetos constantemente, contribuindo para o desempenho do sistema. Outra vantagem é que, com o Singleton, pode-se compartilhar dados entre telas, já que representa um único objeto. Por exemplo, se alterarmos a propriedade X do Singleton no Cadastro de Clientes, este mesmo valor será lido ao acessarmos a tela de Contas a Pagar. Podemos compará-lo a um repositório de dados compartilhados.

Antes de prosseguir, gostaria de pedir que vocês acompanhassem o artigo até o fim. Após a parte prática, achei necessário – ou, talvez, indispensável – manifestar algumas ressalvas e opiniões particulares sobre o Singleton. Do ponto de vista técnico, este padrão de projeto apresenta algumas restrições.

Para o nosso exemplo prático, achei interessante a ideia de uma funcionalidade que registra logs em uma aplicação que realiza sorteios (como manipulação do cadastro de participantes, pessoas sorteadas, etc). Criaremos, então, uma classe que registra logs de alguns eventos da aplicação, também conhecida como Logger. Independente da tela em que o usuário estiver, acessaremos o mesmo objeto do Logger para consumir os métodos. Será um endereço único na memória.
Primeiramente, para fins de compreensão, optei por disponibilizar a classe completa do Logger, que será o nosso Singleton:

unit uLoggerSingleton;
 
interface
 
type
  TLoggerSingleton = class
  private
    // variável que aponta para o arquivo de log
    ArquivoLog: TextFile;
 
    // o construtor é declarado como privado
    // pois o método principal é "ObterInstancia"
    constructor Create;
  public
    // método principal do Singleton
    class function ObterInstancia: TLoggerSingleton;
 
    // método chamado pelo "Create" indiretamente
    class function NewInstance: TObject; override;
 
    // método para registrar o texto do parâmetro no arquivo de log
    procedure RegistrarLog(const Texto: string);
 
    destructor Destroy; override;
  end;
 
var
  Instancia: TLoggerSingleton;
 
implementation
 
uses
  Forms, SysUtils;
 
{ TLoggerSingleton }
 
constructor TLoggerSingleton.Create;
var
  DiretorioAplicacao: string;
begin
  // associa o aquivo "Log.txt" que está na pasta do projeto
  DiretorioAplicacao := ExtractFilePath(Application.ExeName);
  AssignFile(ArquivoLog, DiretorioAplicacao + 'Log.txt');
 
  // se o arquivo não existir, é criado
  if not FileExists(DiretorioAplicacao + 'Log.txt') then
  begin
    Rewrite(ArquivoLog);
    CloseFile(ArquivoLog);
  end;
end;
 
destructor TLoggerSingleton.Destroy;
begin
  // libera o Singleton da memória
  FreeAndNil(Instancia);
 
  inherited;
end;
 
class function TLoggerSingleton.NewInstance: TObject;
begin
  // se já houver uma instância, ela é retornada
  // caso contrário, o objeto é instanciado antes de ser retornado
 
  if not Assigned(Instancia) then
  begin
    // chama a função "NewInstance" da herança (TObject)
    Instancia := TLoggerSingleton(inherited NewInstance);
  end;
 
  result := Instancia;
end;
 
class function TLoggerSingleton.ObterInstancia: TLoggerSingleton;
begin
  // chama o método Create, que cria (uma única vez) e retorna a instância
  result := TLoggerSingleton.Create;
end;
 
procedure TLoggerSingleton.RegistrarLog(const Texto: string);
begin
  // abre o arquivo de log para edição
  Append(ArquivoLog);
 
  // escreve o texto no arquivo de log
  WriteLn(ArquivoLog, Texto);
 
  // fecha o arquivo
  CloseFile(ArquivoLog);
end;
 
end.

Observe que criamos uma variável da classe chamada “Instancia”, que será responsável por armazenar o ponto único de acesso ao objeto.
A mecânica do Singleton acontece no método Create (que é chamado pelo método ObterInstancia): se a variável de classe (Instancia) já existir na memória, será retornada ao chamador. Caso contrário, o objeto é criado antes de ser retornado. Na prática, o objeto será criado apenas na primeira vez em que é solicitado. Nas chamadas subsequentes, o objeto já estará instanciado. O objetivo principal é manter apenas uma instância na memória.
É importante também ressaltar que o método ObterInstancia é declarado como class function. Isso permite que podemos chamá-lo sem a necessidade de instanciar o objeto.


Mas, e esse método “NewInstance”. Por que existe?

Pois bem, suponha que o desenvolvedor esqueça do método ObterInstancia e acidentalmente invoca o método Create da classe do Logger:

Logger := TLoggerSingleton.Create;

Se isso ocorrer, o Singleton perde o sentido. Teremos duas (ou mais) instâncias da mesma classe, e não um objeto único. Para prevenir esse equívoco, sobrescrevemos a função NewInstance, que é indiretamente chamada pelo Create no ancestral TObject, para aplicar as nossas condições. Dessa forma, mesmo que o desenvolvedor utilize o método ObterInstancia ou Create, a mesma instância será retornada. Boa, hein? 🙂

 

Agora é simples: cada vez que precisarmos registrar um log, basta utilizarmos a codificação abaixo:

var
  Logger: TLoggerSingleton;
begin
  // obtém a instância do Singleton para registrar um log
  Logger := TLoggerSingleton.ObterInstancia;
  Logger.RegistrarLog('Aplicação iniciada!');
end;

Perfeito!

Ops, esqueci de uma pequena observação: no código em que ocorre todo o mecanismo, temos uma condição IF para verificar se o objeto já existe. Isso implica que, toda vez que chamarmos o método ObterInstancia, essa condição será processada. Ora, se o objeto é criado somente na primeira chamada, essa condição torna-se inútil em todas as chamadas posteriores, concordam?
Uma alternativa para solucionar essa pendência é criar o objeto na inicialização da aplicação, ao invés de criá-lo quando o Singleton for requisitado na primeira vez. O método NewInstance, por sua vez, seria ajustado para apenas retornar a instância, já que haverá uma garantia de que ela já foi criada:

result := Instancia;

 

Deseja ver o Singleton funcionando em um projeto? Baixe o exemplo no link abaixo (com várias melhorias!), execute o projeto, faça algumas operações e, por fim, clique no botão Abrir Log. Todo o conteúdo do arquivo será gerado pelo Singleton.

Exemplo de Singleton com Delphi

 

Leitores, agora é hora de bater um papo sério…
Tudo em excesso é ruim. O Singleton, quando utilizado abusivamente, torna-se um Anti-Pattern (ou Anti-Padrão). Este termo é aplicado a práticas e/ou soluções que, quando empregados incorretamente, são contra-produtivos.

O propósito do Singleton, de criar objetos globais, tornou-se relativamente comum na programação, principalmente por facilitar e agilizar as atividades de codificação. Na verdade, muitos fazem uso dessa prática mas não sabem que é um Singleton. Além disso, alguns desenvolvedores também adquiriram o hábito de criar classes “utilitárias”, que possuem funções úteis de formatações e regras de negócio para serem compartilhadas em qualquer local do sistema. Desculpem-me pela sinceridade, mas, na minha opinião, isso é ruim e desnecessário. Ao invés de reduzir o processamento, é possível que o desempenho do sistema seja prejudicado em função de um objeto que ficará residente na memória o tempo todo. O problema agrava-se ainda mais quando há vários Singletons no projeto e todos são criados (e mantidos) de uma vez só. Acredite: isso não é uma boa prática. Embora seja um padrão de projeto, o Singleton deve ser empregado em situações especiais, e não como um mero agregador de funções úteis ou agente de reaproveitamento de código.

O Logger apresentado no artigo é um exemplo de situação especial, pois reflete um objeto que será utilizado com altíssima frequência. Se o uso for eventual ou intervalado, esqueça o Singleton! Crie os objetos sob demanda. O código fica mais profissional e o consumo de memória da aplicação será menor.
Eu, particularmente, penso três vezes antes de utilizá-lo. Recomendo que todos façam o mesmo.

 

Próximo artigo: início dos padrões estruturais.
Aguardo vocês lá! 🙂


Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

2 comentários

  1. Muito boa sua séries sobre Design Patterns no Delphi.

    Uma dúvida: Sei que é um exemplo, mas seria mais recomendável a variável “Instancia” estar abaixo da seção “Implementation”? Impedindo assim que a variável fique acessível fora da unit uLoggerSingleton;

    1. Olá, Rafael, como vai?
      Ótima pergunta! Tive essa mesma dúvida quando estudei o padrão.
      A variável “Instancia” foi declarada fora do escopo de visibilidade porque é utilizada pelo método estático (class function) “NewInstance”.
      Nas versões mais recentes do Delphi (a partir da 2006, salvo engano), é possível declarar variáveis estáticas, portanto, esse problema seria resolvido da seguinte forma:

      TLoggerSingleton = class
      private
        class var Instancia: TLoggerSingleton;
        ...

      No Delphi 7, que usei para o exemplo, infelizmente não traz esse recurso.

      Obrigado pelo comentário!
      Abraço!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.