sexta-feira, 5 de novembro de 2010

Existem 1001 maneiras de preparar SINGLETON, invente uma!

Falando-se de POO e Padrões de projeto, não podemos deixar de falar em Singleton. Singleton é um tipo de classe que só pode ser instanciada uma e apenas uma vez. Esse tipo de classe é ideal para objetos que carregam configurações do sistema, objetos que manipulam o horario do sistema, objetos que usamo recursos compartilhados, e por isso necessitam ser serializados ou sincronizados pelas threads, por não poderem executar ao mesmo tempo.
Concrete Factories e Builders também são ótimos exemplos de padrões de projeto que podem ser criados a partir de um singleton. Afinal, se você precisa de um objeto que crie, ou que crie e configure outros objetos para você, seria ideal que todos os objetos criados e montados fossem fabricados do mesmo jeito. Logo, você não deveria ter duas instancias diferentes de um mesmo factory ou de um mesmo buider.
A maneira mais simples de se criar um singleton no Delphi é comona listagem abaixo.
unit uSingleton;

interface

uses
  DateUtils, SysUtils, Windows, dialogs;

type

  TObjetoNormal = class
  private
    FDataHora: string;
  public
    constructor Create; 
    function GetDataHora: string;
  end;


  TObjetoUnico = class
  private
    FDataHora: string;
  public
    constructor Create;
    class function CreateUnico: TObjetoUnico;
    destructor Destroy; override;
    function GetDataHora: string;
  end;

procedure VerificaObjetoUnicoCriado;

implementation




var
  _ObjetoUnico: TObjetoUnico;
  _Contador: Integer = 0;

procedure VerificaObjetoUnicoCriado;
begin
  if (_Contador >= 1) and (_ObjetoUnico <> nil) then
    ShowMessage('=========== verificação ===========' + #13#10 +
    'Nome da classe: ' + _ObjetoUnico.ClassName+ #13#10 +
    'Hora de Criação: ' + _ObjetoUnico.GetDataHora + #13#10 +
    'Endereço na memória: ' + IntToStr(Integer(_ObjetoUnico))
  );
end;

{ TObjetoNormal }

constructor TObjetoNormal.Create;
begin
  inherited Create;
  FDataHora := FormatDateTime('yyyy-mm-dd hh:nn:ss', now);
end;

function TObjetoNormal.GetDataHora: string;
begin
  Result := FDataHora;
end;

{ TObjetoUnico }

constructor TObjetoUnico.Create;
begin

    if (_Contador = 0) then
    begin
      inherited Create;  //aqui tudo bem usar o inherited create e destroy porque a classe base não faz nada de mais
      InterlockedIncrement(_Contador);
      FDataHora := FormatDateTime('yyyy-mm-dd hh:nn:ss', now);
    end
    else
      raise Exception.Create('Ei! Não use esse constructor, use o CreateUnico!');
    //o inherited fica dentro do if assim o objeto não será criado caso ja esteja o contador > 0

end;

class function TObjetoUnico.CreateUnico: TObjetoUnico;
begin
  if _ObjetoUnico = nil then
    _ObjetoUnico := TObjetoUnico.Create;
  Result := _ObjetoUnico;
end;

destructor TObjetoUnico.Destroy;
begin

  //aqui permitimos que o objeto seja destruido.
  //logico que isso dará problemas para quem tentar usar o objeto
  //mas caso necessário ele pode ser recriado automaticamente
  //desde que seja recriado uma unica vez.

  //por isso é importante não guardar referencias ao singleton em
  //variáveis, mas chama-lo apenas através de um método

  _ObjetoUnico := nil;
  InterlockedDecrement(_Contador);
  inherited;

end;

function TObjetoUnico.GetDataHora: string;
begin
  Result := FDataHora;
end;



initialization
  _ObjetoUnico := nil;
  _Contador := 0;

finalization
  if (_ObjetoUnico <> nil) then
  try
    _ObjetoUnico.Free;
  except
    //tratamento de excessão
  end;

end.


    

Estou criando um objeto, e no momento da criação estou gravando em um atributo privado o horário de criação. Repare que um objeto normal permite que se crie várias instâncias, e cada nova instância vai sendo gravada com o novo horário, mas o objeto singleton permanece o mesmo.
Podemos também alterar o método create do objeto para que grave mensagens únicas em um listbox, num formulário. Se em cada linha do listbox adicionássemos um objeto diferente, o singleton seria adicionado uma única vez.
Veja também que para saber se o objeto já está criado usamos uma variavel estática. Mas ela não é Pública global. Ela não está na seção interface da unit, mas sim na seção implementation. E seu nome começa com "_". Com isso garantimos que ela será invisível às outras units e na própria unit ela não será usada "sem querer" por um programador desavisado.
Seria interessante se fosse possível criar um constructor privado. Só que existe um outro problema: Criar um constructor privado não impede de se chamar o método create da classe. Só que quando isso acontecer será executado o constructor create público da classe ancestral. Isso fará com que a string FDataHora interna da classe, e qualquer outro objeto a ser carregado no constructor não sejam carregados, setados e construidos corretamente. Isso criaria varios erros de access violations.
Construtores privados apenas no Prism e no C#.net :)
Podemos contornar esse problema mantendo o constructor público, mas implementando um contador de objetos. Se ele passar de 1, disparamos uma exception.
O grande problema de se criar uma classe singleton assim é que ela não pode ser facilmente herdada, a não ser que existam outras variáveis para controlar o contador e o conteiner de instancias da classe filha. Para isso todos os métodos devem ser sobrecarregados, ou seja, o singleton deve ser refeito.
Outra desvantagem é que é estritamente necessário criar esse objeto pela class function (método estático) CreateUnico.

É interessante salientar que o CreateUnico é um método de classe estático (construtores também são métodos estáticos) que poderia ser substituido por uma function estática global tradicional, mas resolvemos usar uma class function para usar o "namespace" (nome da classe) e não se distanciar muito da POO. O método CreateUnico também pode ser caracterizado como um factory method se você desejar, pois a função dele é criar um objeto. (construtores também são fábricas degeneradas)






Podemos usar o seguinte exemplo, na listagem abaixo, para testar nosso singleton. Repare que temos dois botões no formulário. (crie um formulário como o da figura)

Um vai criar uma instância de uma classe normal (TObjetoNormal) e o outro vai criar uma instância do singleton (TObjetoUnico) pelo método CreateUnico.
Estamos mostrando, para cada objeto criado, sua data de criação, seu endereço na memória (um integer, deve ser único) e o nome da classe. Monitore memory leaks no seu projeto.
Você verá que a cada instância do objeto normal, mesmo que gravado em uma mesmoa variável, é uma nova instância, com endereço de memória e data diferentes. Mas ao criar o singleton, mesmo que crie 200 vezes, o endereço será sempre o mesmo, e a data também, indicando que a instância é a mesma. Ao fechar o programa as várias instâncias da classe normal serão reportadas como leaks, mas a classe singleton será dstruida no finalization.
O controle de instâncias é feito por uma variável estática privada e por um contador de referências. Caso passar de 1 ou a variável do tipo singleton não for nil é disparada uma excessão (no método create). O método CreateUnico verifica se já está criada (variável _ObjetoUnico <> de nil e contador de referencias exatamente igual a 1). Se já estiver criada retorna a mesma, caso contrário cria armazena na hora de retornar e retorna (lazy binding).



codigo:

procedure TfrmUmaInstancia.btNormalClick(Sender: TObject);
var FObjetoNormal: TObjetoNormal;
begin

  //cuidado, você está prestes a criar varias instancias de
  //um objeto colocando na mesma variável, perdendo
  //posteriormente a referencia aos objetosanteriores
  //você não poderá destruir os objetos anteriores, causando
  //um memory leak
  FObjetoNormal := TObjetoNormal.Create;

  //mostrando a classe do objeto, hora de criação e endereço do objeto
  //veja que é sempre diferente
  ShowMessage('Nome da classe: ' + FObjetoNormal.ClassName+ #13#10 +
    'Hora de Criação: ' + FObjetoNormal.GetDataHora + #13#10 +
    'Endereço na memória: ' + IntToStr(Integer(FObjetoNormal))
  );
end;

procedure TfrmUmaInstancia.btUnicoClick(Sender: TObject);
var FObjetoUnico: TObjetoUnico;
begin

  //veja que é possivel executar o create
  //Mas dessa forma não é garantido que o objeto seja unico
  //para garantir que seja único é imprescindível o uso do método  CreateUnico

  FObjetoUnico := TObjetoUnico.CreateUnico;

  //mostrando a classe do objeto, hora de criação e endereço do objeto
  //veja que é sempre igual
  ShowMessage('Nome da classe: ' + FObjetoUnico.ClassName+ #13#10 +
    'Hora de Criação: ' + FObjetoUnico.GetDataHora + #13#10 +
    'Endereço na memória: ' + IntToStr(Integer(FObjetoUnico))
  );


end;

    




Repare que esse singleton que fizemos pode ser refeito em Lazarus sem grandes problemas. Mas seria difícil usar herança com ele. Para que uma classe derivada funcione corretamente sem misturar instâncias com a classe ancestral, sem gravar uma instância nas variáveis estáticas locais da unit ancestral e sem usar métodos indevidos da classe ancestral, como o método que incrementa a contagem, obrigatoriamente deve-se reimplementar todos os métodos da classe filha sem invocar o inherited.
Criar um descendente para este singleton não é impossível, e pode ser exemplificado pelo código abaixo:


unit uSingletonDerivado;

interface

uses
  uSingleton, SysUtils, DateUtils, Windows, dialogs;

type

  TObjetoUnicoDerivado = class(TObjetoUnico)
  private
    FDataHora: string;
  public
    constructor Create;
    class function CreateUnico: TObjetoUnicoDerivado;
    destructor Destroy; override;
    function GetDataHora: string;
  end;

procedure VerificaObjetoUnicoDerivadoCriado;

implementation

var
  _ObjetoUnico: TObjetoUnicoDerivado;
  _Contador: Integer = 0;

procedure VerificaObjetoUnicoDerivadoCriado;
begin
  if (_Contador >= 1) and (_ObjetoUnico <> nil) then
    ShowMessage('=========== verificação ===========' + #13#10 +
    'Nome da classe: ' + _ObjetoUnico.ClassName+ #13#10 +
    'Hora de Criação: ' + _ObjetoUnico.GetDataHora + #13#10 +
    'Endereço na memória: ' + IntToStr(Integer(_ObjetoUnico))
  );
end;

{ TObjetoUnicoDerivado }

constructor TObjetoUnicoDerivado.Create;
begin

    if (_Contador = 0) then
    begin
      //aqui há o perigo de criar mais um objeto _ObjetoUnico que perderá sua referência e causará leak
      //ou, no mínimo, incrementar o seu contador
      //experimente descomentar o inherited para ver o que acontece
      //inherited Create;
      InterlockedIncrement(_Contador);
      FDataHora := FormatDateTime('yyyy-mm-dd hh:nn:ss', now);
    end
    else
      raise Exception.Create('Ei! Não use esse constructor, use o CreateUnico!');
    //o inherited fica dentro do if assim o objeto não será criado caso ja esteja o contador > 0

end;

class function TObjetoUnicoDerivado.CreateUnico: TObjetoUnicoDerivado;
begin
  //aqui não se pode usar inherited porque senão trará uma instância do objeto ancestral
  if _ObjetoUnico = nil then
    _ObjetoUnico := TObjetoUnicoDerivado.Create;
  Result := _ObjetoUnico;
end;

destructor TObjetoUnicoDerivado.Destroy;
begin
  _ObjetoUnico := nil;
  InterlockedDecrement(_Contador);
  //aqui há o perigo de destruir um objeto _ObjetoUnico que pode estar em uso na unit do ancestral
  //expedrimente descomentar o inherited para ver o que acontece
  //inherited;
end;

function TObjetoUnicoDerivado.GetDataHora: string;
begin
  //aqui não haveria perigo de chamar inherited
  //mas se você não chamar o inherited do create então  o campo privado
  //FDataHora ficará vazio (seria usado o do ancestral)
  //por isso é melhor refazer
  //isso é polimorfismo :)
  Result := 'Data desta classe nova: ' +  FDataHora;;
end;

initialization
  _ObjetoUnico := nil;
  _Contador := 0;

finalization
  if (_ObjetoUnico <> nil) then
  try
    _ObjetoUnico.Free;
  except
    //tratamento de excessão
  end;


end.

    


E testado assim:

procedure TfrmUmaInstancia.btDerivadoClick(Sender: TObject);
var FObjetoUnico: TObjetoUnicoDerivado;
begin
  //veja que é possivel executar o create
  //Mas dessa forma não é garantido que o objeto seja unico
  //para garantir que seja único é imprescindível o uso do método  CreateUnico

  FObjetoUnico := TObjetoUnicoDerivado.CreateUnico;

  //mostrando a classe do objeto, hora de criação e endereço do objeto
  //veja que é sempre igual
  ShowMessage('Nome da classe: ' + FObjetoUnico.ClassName+ #13#10 +
    'Hora de Criação: ' + FObjetoUnico.GetDataHora + #13#10 +
    'Endereço na memória: ' + IntToStr(Integer(FObjetoUnico))
  );

end;
    


Uma outra maneira muito elegante de criar singletons em Delphi muito mais facilmente "herdáveis" seria cria-los usando class properties e class vars estáticas (e privadas) assim a classe filha poderia herdar essas caracteristicas já adaptadas para a nova classe, sem misturar instâncias.
A desvantagem é que dessa maneira ele não funcionaria com o lazarus, pelo menos não enquando o lazarus não suportar propriedades e campos de classe.
Na parte 2 desse post veremos como implementar com class vars no Delphi XE.

Você pode fazer download desse exemplo aqui. O uso da FastMM4 vai depender de você estar usando Delphi 7 ou não. Este exemplo foi compilado em Delphi XE.

Até + :)



Links úteis, leia todos ;)



Existem 1001 maneiras de preparar SINGLETON, invente uma! - Parte 1

http://blog.vitorrubio.com.br/2010/11/existem-1001-maneiras-de-preparar.html

Existem 1001 maneiras de preparar SINGLETON, invente uma! - Parte 2

http://blog.vitorrubio.com.br/2011/01/existem-1001-maneiras-de-preparar.html

Existem 1001 maneiras de preparar SINGLETON, invente uma! - Parte 3

http://blog.vitorrubio.com.br/2011/02/existem-1001-maneiras-de-preparar.html

Existem 1001 maneiras de preparar SINGLETON, invente uma! - Parte 4
http://blog.vitorrubio.com.br/2011/02/existem-1001-maneiras-de-preparar_08.html

Criando uma classe singleton verdadeira em delphi

http://www.comofazertudo.com.br/computadores-e-internet/criando-uma-classe-singleton-verdadeira-em-delphi

Creating a real singleton class in Delphi 5

http://edn.embarcadero.com/article/22576

Introdução: Singleton - Design Pattern Delphi - Parte 1

http://www.devmedia.com.br/post-17889-Introducao--Singleton-Design-Pattern-Delphi-Parte-1.html

Tentativa de Singleton usando Delphi

http://www.marcosdellantonio.net/2006/12/01/tentativa-de-singleton-usando-delphi/

Implementing the Singleton pattern in delphi

http://www.delphi3000.com/articles/article_1736.asp?SK=

Essa é uma abordagem nova que eu nunca imaginei:

http://stackoverflow.com/questions/1409593/creating-a-singleton-in-delphi-using-the-new-features-of-d2009-and-d2010

Class (, Static, or Shared) Constructors (and Destructors)

http://blogs.embarcadero.com/abauer/2009/09/03/38898

Design Patterns in Delphi

http://delphi.about.com/od/oopindelphi/a/aa010201a.htm

No forum antigo:

Tópico no forum devmedia sobre singleton

no forum novo:

http://www.devmedia.com.br/forum/viewtopic.asp?id=374670

3 comentários:

  1. Olá Vitor,

    Excelente post, eu estava mesmo precisando disso.
    Os outros artigos sobre singleton em Delphi são muito confusos.

    Parabéns o site tá bem bacana.

    ResponderExcluir
  2. Obrigado pelo Feedback Jhonny. Realmente alguns artigos sobre singleton são confusos. Mas é mais por falta de explicação de uns conceitos básicos do que por problemas nos artigos mesmo. Como diz o título ... existem 1001 maneiras, por isso estou trabalhando em uma usando métodos e propriedades estáticos e em outro artigo sobrescrevendo o método NewInstance, alterando assim a forma do funcionamento do Create. Esses exemplos serão mais "elegantes" e menos sujeitos a erros do que esse primeiro, mais fácil. Quando eu digo "elegante" quero dizer do ponto de vista da POO: não vão ter métodos e variáveis globais, direto na unit, mas todos os métodos serão parte de classes, mesmo que estáticos. Postarei em breve, não perca ;)

    ResponderExcluir
  3. O objeto Printer é um singleton e a implementação é bem simples, vale a pena dar uma olhada na unit Printers, para quem tem a versão com fontes do Delphi...

    ResponderExcluir

Postagens populares

Marcadores

delphi (60) C# (31) poo (21) Lazarus (19) Site aos Pedaços (15) sql (13) Reflexões (10) .Net (9) Humor (9) javascript (9) ASp.Net (8) api (8) Básico (6) Programação (6) ms sql server (5) Web (4) banco de dados (4) HTML (3) PHP (3) Python (3) design patterns (3) jQuery (3) livros (3) metaprogramação (3) Ajax (2) Debug (2) Dicas Básicas Windows (2) Pascal (2) games (2) linguagem (2) música (2) singleton (2) tecnologia (2) Anime (1) Api do Windows (1) Assembly (1) Eventos (1) Experts (1) GNU (1) Inglês (1) JSON (1) SO (1) datas (1) developers (1) dicas (1) easter egg (1) firebird (1) interfaces (1) introspecção (1) memo (1) oracle (1) reflexão (1)