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.
Introduçã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, descontos 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!
Singleton
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.
ara 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.
Uma das vantagens é 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.
Exemplo de codificação do Singleton
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, denominada 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
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); 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; 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; initialization finalization // libera o Singleton da memória FreeAndNil(Instancia); 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.
O que é o método NewInstance?
Pois bem, suponha que o desenvolvedor esqueça do método ObterInstancia
e acidentalmente invoca o método Create
da classe do Logger:
1 |
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:
1 2 3 4 5 6 7 |
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. Oras, 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 haveria uma garantia de que ela já foi criada:
1 |
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.
Cuidados e ressalvas
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. Outros também adquirem o hábito de criar classes “utilitárias”, que possuem funções úteis e regras de negócio para serem compartilhadas em qualquer local do sistema.
Na minha opinião, isso é arriscado. 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 representa um objeto que será utilizado com muita 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á! 🙂
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;
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:
No Delphi 7, que usei para o exemplo, infelizmente não traz esse recurso.
Obrigado pelo comentário!
Abraço!
André boa noite, muito bom artigo parabéns!
Uma dúvida, como você comentou no final o problemas de ter vários singleton criados, exemplo é alguns cadastros que precisa sempre de consultar e acabamos criando vários métodos espalhado, eu pensei usar singleton, seria uma boa ideia ou existe outro padrão que posso aplicar com segurança ?
Olá, Júnior, bom dia.
Eu não recomendaria o uso do Singleton para consultas de dados compartilhada por vários locais no sistema. Essa abordagem pode gerar um alto consumo de recursos devido às chamadas estáticas.
Neste caso, portanto, acredito que uma das melhores ações é modelar uma classe exclusiva para essas consultas, de forma que ela possa ser instanciada nos diferentes locais que a usam. Uma das maiores vantagens é a facilidade na manutenção, já que a consulta será centralizada em um único local.
Alternativamente, pode-se empregar os padrões Façade e Proxy de acordo com a complexidade dessas consultas, fornecendo uma camada de encapsulamento.
Grande abraço!
Oi André, primeiramente parabéns pelos artigos que você publica, realmente são muito bons! Tenho recorrido a eles sempre que tenho dúvidas, e por falar em dúvidas, tenho uma para lhe questionar: neste exemplo, ficou um Memory Leak (TLoggerSingleton). Ao debugar eu percebi que o breakpoint não passa pelo Destructor. Está faltando alguma chamada? Eu percebi este mesmo problema em outro exemplo que achei na internet e estou quebrando a cabeça aqui para tentar resolver a questão, mas acredito que você já saiba como rsrsrs, deixe-me saber ok, um abraço!
Olá, Fernando, tudo bem?
Você fez uma excelente observação! Executei o projeto de exemplo e o Memory Leak realmente existe. Falha minha!
A aplicação cuida da instanciação do objeto, mas não invoca o destructor ao ser encerrada, mantendo o Singleton em memória.
Fiz a correção no artigo, adicionando o comando
FreeAndNil
na seçãofinalization
do Singleton. O destructor, portanto, não é mais necessário.Muito obrigado pela colaboração, Fernando! Grande abraço!
Muito boa a ideia do NewInstance, sem ele a classe iria ficar vulnerável. Eu confesso que eu não sabia que o NewInstance é quem faz o trabalho pesado, enquanto o constructor ganha a fama. Gaiatinho esse constructor hein?! rsrs. André, agora este mero aprendiz que vos escreve ficou com uma dúvida: Como “raios” é que o Create “chama” o NewInstance, se não tem nada dentro do método lá na classe TObject?? Abraços!
Olá, Rafael, boa noite! Desculpe-me pela demora.
Safadinho esse constructor, né? O mérito realmente deveria ir para o NewInstance, rsrs.
Rafael, segundo a documentação da Embarcadero, o método NewInstance é chamado automaticamente por todos os construtores. Não vemos essa chamada de forma explícita, mas ela ocorre “por trás das cortinas”, rsrs.
Grande abraço!