Criando uma classe singleton verdadeira em delphi

O artigo descreve como criar uma classe que siga o padrão singleton. A classe descrita cuidará dos requisitos singleton e efeitos dele, efetivamente deixando o programador para usar a classe como todas as outras.

Um singleton é uma classe que suporta a criação de apenas um objeto. É como seu computador – há apenas um teclado. Então, se você estivera escrevendo em código Delphi que simulava seu computador, você iria querer apenas um objeto para lidar com a leitura do teclado, escrita e atividades de controle.

Artigos sobre classes singleton são raros, mas há alguns na internet. Eu tenho visto alguns em revistas também. Mas nenhum desses artigos inclui códigos de exemplo para criar uma classe singleton verdadeira.

Por “verdadeira” eu quero dizer que a classe se impõe a exigência da instância um, ao invés de deixar aquela tarefa para o programador. Todos os artigos que tenho visto tem exigido que o programador use a classe de modo especial para forçar o padrão singleton.

Neste artigo você verá como criar uma classe singleton característica que inclua lógica para forçar a regra da instancia um.

Nota: a abordagem convencional, na qual a regra da instância um é mantida explicitamente pelo programador, não está sem mérito. Classes singleton verdadeiras como a que eu apresento aqui escondem efetivamente os detalhes e a consciência do padrão singleton do programador. Ao programador é confiada a tarefa de forçar o padrão – que é bom -, mas o programador também pode estar desconhecendo a natureza especial da classe, o que é ruim. Se você não sabe que a classe é uma classe singleton, então todos os tipos de erros podem surgir. Você tem de estar prevenido!


Escrevendo o código


Nosso objetivo é escrever uma classe que possa ser usada como isso:

procedure Test;

var

  s1, s2 : TSingleton;

begin

  s1 := TSingleton.Create;

  s2 := TSingleton.Create;

  // Do something with s1 and s2 here

  s2.Free;

  s1.Free;

end;

(Eu deixei a tentativa…por fim, para simplificar bloqueios e outras salvaguardas)

O objetivo é fazer a classe TSingleton comportar-se de modo que tanto s1 quanto s2 se refiram ao mesmo objeto. Aqui está o que temos de fazer:
  • Explicar que o objeto é chamado Create na primeira vez (quando s1 é criado acima)
  • Assegurar que quando outro Create é executado (s2 acima), o objeto existente é reutilizado ao invés de outro ser criado
  • Evitar destruir o objeto quando ele não é a última referência que é destruída (quando s2 é liberado)
  • Destruir a instância quando a última referência for destruída ( quando s1 é liberado acima)
Há uma maneira de substituir a criação e destruição de um novo objeto Delphi? Claro que há. Em TObjetc (a mãe de todos objetos { trocadilho planejado}), há dois métodos que nós podemos utilizar:

class function NewInstance: TObject; virtual;

procedure FreeInstance; virtual;

NewInstance é responsável por alocar memória para suportar uma nova instância de classe, e FreeInstance é responsável por liberar aquela memória quando a classe está completa com ela.

Estes métodos controlam o que acontece quando o objeto é criado e quando o objeto é destruído. Se nós transcrevermos este código, nós podemos alterar o comportamento padrão para trabalhar a palavra que uma classe singleton requer.

Instâncias de controle são um pouco complicadas. Devemos:
  1. Acompanhar cada instancia existente de nossa classe
  2. Acompanhar quantas referências há para a instância
  3. Criar um novo objeto somente quando não existem instâncias
  4. Destruir o objeto quando a última referência é removida
Para acompanhar uma instancia existente, nós usaremos uma variável global. Na verdade, a variável, por fim, será declarada dentro da parte de implantação de uma unidade, assim ela não será uma variável global verdadeira. Mas o alcance deve ser suficiente para traçar todas as chamadas Create e Free. Nós chamaremos a variável de Instance, assim nós sabemos ao que ela se refere.

Para acompanhar quantas variáveis existem, nós precisamos de outra variável. Colocá-la-emos dentro da classe ou faremos um tipo de variável global como a Instance. Eu vou optar por esta maneira, mas você deve fazer o que achar melhor. Eu nomearei esta variável de Ref_Count.

Agora nós temos duas variáveis:

var

  Instance : TSingleton = nil;

  Ref_Count : Integer = 0;

Eu inicio as variáveis para que inicialmente elas não contenham nenhum lixo. Eu sei que o compilador faz isso automaticamente, então isso é apenas uma questão de legibilidade.

Nós precisaremos declarar a classe TSingleton acima do bloco variável, e se você dar uma olhada nos arquivos de exemplo que você pode baixar no final deste artigo, você verá que eu  coloquei a declaração na interface da unidade para que ela seja visível fora daqui.
Aqui está a declaração da classe TSingleton:
type

  TSingleton = class

  public

    class function NewInstance: TObject; override;

    procedure FreeInstance; override;

    class function RefCount: Integer;

  end;

Eu adicionei a função RefCount para que nós possamos ver que ela de fato trabalha, e é normalmente útil por ser capaz de ler muitas referências para o objeto existir. Não é requerido, de forma que você não tem de adicioná-lo para as suas classes singleton se você não precisa dele.

Agora, para a implementação dos três métodos:

procedure TSingleton.FreeInstance;

begin

  Dec( Ref_Count );

  if ( Ref_Count = 0 ) then

  begin

    Instance := nil;

    // Destroy private variables here

    inherited FreeInstance;

  end;

end;



class function TSingleton.NewInstance: TObject;

begin

  if ( not Assigned( Instance ) ) then

  begin

    Instance := inherited NewInstance;

    // Initialize private variables here, like this:

    // TSingleton(Result).Variable := Value;

  end;

  Result := Instance

  Inc( Ref_Count );

end;



class function TSingleton.RefCount: Integer;

begin

  Result := Ref_Count;

end;

E é isso!

Eu tenho substituído o método New Instance. Assim, ele alocará memória somente se nenhuma instancia de classe existir. Se há uma instancia existente, a função simplesmente retorna aquela instancia para o construtor e, assim, ela será reutilizada.

Se nós chamarmos o construtor três vezes, um objeto somente é criado na primeira vez. Nas outras duas chamadas  simplesmente reutiliza-se o primeiro objeto. A variável de contagem de referência deixa-nos saber que nós temos três referências para a única instancia.

Quando o programa chama o destruidor, uma chamada para FreeINstance é colocada para liberar a memória alocada no construtor. Este método, também, é substituído para que o objeto seja destruído somente quando a última referência for removida.

Se você pretende usar um singleton em um programa multifilamentado, trate o objeto como você faria com qualquer variável que você divide entre filamentos. Porque aquilo é apenas o que você faz: divide-a entre os filamentos. Então, você deve tomar atenção especial quando modificar os dados.

Simplicidade

Como você pode ver, criar uma classe singleton não requer muito esforço, apenas o conhecimento certo e algumas linhas de código. Meu código trabalha bem em Delphi 5. A técnica provavelmente funcionará bem com versões mais velhas de Delphi, mas eu não as testei ainda, então não posso dar qualquer garantia.

Antes que eu dê o arquivo para você usar, deixe-me dar algumas palavras de alerta para você:
  • Não descenda de uma classe singleton. A razão para isso é que há somente uma instancia e uma variável de contagem de referência. Se você derivar duas classes de TSingleton, somente um objeto será criado. A outra classe reutilizará o objeto, que será uma instância de uma classe diferente.  Não desça essa estrada!
  • Você não pode produzir componentes singleton pela simples razão de propriedade. Um componente é propriedade de uma forma e um componente não pode pertencer a outros componentes.
  • Lembre que o construtor e o destruidor são chamados para cada nova referência, já que elas são criadas ou destruídas.  Não inicie variáveis privadas no construtor e não as libere no destruidor. Ao invés disso, construa aquele código pelos métodos  NewInstance e FreeInstance, como mostrado nos comentários.
  • A ordem de ajustamento Ref_Count nos dois métodos em combinação com o resto do código naqueles método é crítica. Ela tem de fazer a criação e destruição apropriada quando algo dá errado. Se seu código de inicialização gera uma exceção, a ordem de fazer as coisas como mostrado acima certificará que a classe seja destruída. Altere este código em seu próprio risco!
O arquivo que você pode baixar é apenas uma cópia da unidade que declara a classe singleton descrita neste artigo.

Link do arquivo: CodeCentral entry 15083.

Eu não tenho certeza se você pode encontrar alguns lugares aonde uma classe singleton venha de forma útil e agora você tem as ferramentas para criar a sua própria! Se você quiser entrar em contato comigo, meu email é lasse@cintra.no

Boa programação!