[Delphi] Design Patterns – Flyweight

[Delphi] Design Patterns - Flyweight

Olá, amigos leitores! Feliz 2017!
O primeiro artigo do ano, avançando na nossa temporada sobre Design Patterns, apresenta o próximo padrão de projeto estrutural, chamado Flyweight. Quebrei um pouco a cabeça para imaginar os cenários que este padrão de projeto possa ser adequado, mas, após muita leitura, consegui desvendá-lo! Confira o artigo!

 

Um dos requisitos não-funcionais que sempre terá grande importância em um sistema de informação é o desempenho (ou performance). Até mesmo já escrevi um artigo sobre as consequências que um software lento pode trazer no cotidiano das pessoas. Na verdade, atualmente, o desempenho corresponde a um diferencial no sucesso de uma aplicação. Usuários esperam que sistemas sejam rápidos e com um consumo de memória satisfatório, ainda mais quando se tratam de aplicativos móveis.

Uma das formas de alcançar boas taxas de desempenho é trabalhar na redução de objetos criados, já que o processo de instanciação pode ser relativamente custoso, principalmente quando o construtor de uma classe inicia vários objetos internos e inicializa variáveis. Além disso, vale lembrar que o processo de construção também envolve gerenciamento de memória, ponteiros e contagens de referência.

O principal objetivo do padrão de projeto Flyweight é melhorar o desempenho de um procedimento através de compartilhamento de objetos com características similares. Em outras palavras, o padrão provê um mecanismo para utilizar objetos já existentes, modificando suas propriedades conforme solicitado, ao invés da necessidade de sempre instanciá-los.

Parece ser o propósito do Singleton…
Sim, a ideia realmente nos remete ao Singleton, porém, há uma diferença: com o Flyweight, é possível trabalhar com vários objetos de uma só vez. A grosso modo, podemos pensar que, basicamente, o Flyweight é uma “lista de Singletons“. Bem basicamente, tá? 🙂

 

Para facilitar a compreensão, imagine o Flyweight como uma “área de cache de objetos”. Quando precisamos utilizar um objeto de uma mesma classe pela segunda vez, mas com propriedades modificadas, em vez de instanciá-lo novamente, buscamos o objeto nesse cache, poupando um novo procedimento de instanciação. É por este motivo que utilizamos o termo “compartilhamento de objetos”.
Considere, por exemplo, uma rotina que desenha 1000 campos de texto em uma tela, cada um com uma das seguintes cores: azul, vermelho ou verde. Uma boa ideia é manter os objetos de desenho de cada cor na memória ao criá-los pela primeira vez. Dessa forma, ao desenhá-los pela segunda vez, basta utilizar os objetos já existentes. No entanto, há um complicador. Cada campo de texto é desenhado em uma nova posição na tela, portanto, não pode ser o mesmo objeto.

A rotina criaria, então, 1000 objetos?
Sim, ao menos que utilizemos o Flyweight para criar apenas 3 objetos!

Como assim? Cada campo de texto deve ser desenhado em uma nova posição na tela, não é?
Exato, mas o Flyweight fornece recursos para alterar algumas propriedades dos objetos compartilhados. Para isso, cada objeto Flyweight possui dois tipos de propriedades:

  • Intrínsecas: propriedades imutáveis, ou seja, que caracterizam o objeto compartilhado. No exemplo acima, é a cor do campo de texto;
  • Extrínsecas: propriedades variáveis que podem receber novos valores a cada acesso. São, portanto, as margens superior e esquerda (posição) do campo de texto.

Com base nessa divisão, podemos afirmar que buscamos o objeto por meio de propriedades intrínsecas e, após encontrá-lo, alteramos as propriedades extrínsecas para adaptá-lo ao novo contexto.

 

Pois bem, pessoal, acredito que a aplicabilidade do Flyweight ficará ainda mais sólida com um exemplo prático. Para este artigo, pensei em uma geração em lote de cartões de agradecimento para leitores de um blog. Teremos um DataSet com leitores cadastrados de diferentes países e, para cada um deles, o sistema deverá gerar um cartão com a bandeira do respectivo país como imagem de fundo e uma mensagem personalizada com o nome do leitor. Já podemos identificar, então, que a imagem da bandeira será a propriedade intrínseca (sempre será a mesma para cada país) e a mensagem com o nome do leitor será a propriedade extrínseca, pois o valor será modificado para cada registro.

A primeira codificação é da Interface Flyweight, uma simples abstração que será implementada posteriormente pelos Concrete Flyweights:

type
  { Flyweight }
  ICartao = interface
    // setter da propriedade
    procedure SetNomeLeitor(const Nome: string);
 
    // método de exportação do cartão
    procedure Exportar;
 
    property NomeLeitor: string write SetNomeLeitor;
  end;

 

Os Concrete Flyweights implementam a abstração e correspondem às classes dos objetos compartilhados, ou seja, cada objeto de uma classe Concrete Flyweight será adicionado em uma lista (ou “área de cache”) sob demanda para que sejam “reaproveitados”.
Para evitar a duplicação de código, criaremos uma classe base para os Concrete Flyweights, que terá um objeto da classe TStringList para armazenar o conteúdo da mensagem e um objeto da classe TPNGImage (da biblioteca PNGImage) para trabalhar com arquivos de extensão PNG:

type
  { Concrete Flyweight - classe base }
  TCartao = class(TInterfacedObject, ICartao)
  protected
    PNGArquivo: TPNGImage;
    Mensagem: TStringList;
    FNomeLeitor: string;
 
    procedure SetNomeLeitor(const NomeLeitor: string);
  public
    constructor Create;
    destructor Destroy; override;
 
    procedure Exportar;
 
    // propriedade extrínseca
    property NomeLeitor: string write SetNomeLeitor;
  end;
 
implementation
 
{ TCartao }
 
constructor TCartao.Create;
begin
  // cria o objeto da classe TStringList para armazenar a mensagem do cartão
  Mensagem := TStringList.Create;
 
  // cria o objeto da classe TPNGImage para trabalhar com PNG
  PNGArquivo := TPNGImage.Create;
end;
 
destructor TCartao.Destroy;
begin
  // libera os objetos da memória
  FreeAndNil(Mensagem);
  FreeAndNil(PNGArquivo);
 
  inherited;
end;
 
procedure TCartao.Exportar;
begin
  // escreve o texto por cima da imagem
  PNGArquivo.Canvas.TextOut(5, 10, StringReplace(Mensagem[0], '%nome%', FNomeLeitor, []));
  PNGArquivo.Canvas.TextOut(5, 70, Mensagem[1]);
  PNGArquivo.Canvas.TextOut(5, 95, Mensagem[2]);
  PNGArquivo.Canvas.TextOut(5, 120, Mensagem[3]);
 
  // salva o arquivo
  PNGArquivo.SaveToFile(Format('C:\Cartoes\Cartao - %s.png', [FNomeLeitor]));
end;
 
procedure TCartao.SetNomeLeitor(const NomeLeitor: string);
begin
  // armazena o nome do leitor para concatenar no nome do arquivo
  FNomeLeitor := NomeLeitor;
end;

 

Agora, codificaremos os Concrete Flyweights específicos de cada país herdando da classe base TCartao. Cada objeto dessas classes deverá carregar a imagem da bandeira do país e preencher a mensagem conforme o idioma.

type
  { Concrete Flyweight - classe herdada }
  TCartaoBrasil = class(TCartao)
    constructor Create;
  end;
 
  { Concrete Flyweight - classe herdada }
  TCartaoEspanha = class(TCartao)
  public
    constructor Create;
  end;
 
  { Concrete Flyweight - classe herdada }
  TCartaoEUA = class(TCartao)
  public
    constructor Create;
  end;
 
implementation  
 
{ TCartaoBrasil }
 
constructor TCartaoBrasil.Create;
begin
  inherited;
 
  // carrega a imagem da bandeira do Brasil
  PNGArquivo.LoadFromFile('C:\Imagens\Brasil.png');
 
  // preenche a mensagem em português
  Mensagem.Add('Olá, %nome%!');
  Mensagem.Add('Feliz Ano Novo!');
  Mensagem.Add('Sempre visite o blog');
  Mensagem.Add('para ler os novos artigos! :)');
end;
 
{ TCartaoEspanha }
 
constructor TCartaoEspanha.Create;
begin
  inherited;
 
  // carrega a imagem da bandeira da Espanha
  PNGArquivo.LoadFromFile('C:\Imagens\Espanha.png');
 
  // preenche a mensagem em espanhol
  Mensagem.Add('Hola, %nome%!');
  Mensagem.Add('Feliz Año Nuevo!');
  Mensagem.Add('Siempre visite el blog');
  Mensagem.Add('para leer los nuevos artículos! :)');
end;
 
{ TCartaoEUA }
 
constructor TCartaoEUA.Create;
begin
  inherited;
 
  // carrega a imagem da bandeira dos EUA
  PNGArquivo.LoadFromFile('C:\Imagens\EUA.png');
 
  // preenche a mensagem em inglês
  Mensagem.Add('Hello, %nome%!');
  Mensagem.Add('Happy New Year!');
  Mensagem.Add('Remember to always visit the blog');
  Mensagem.Add('to check out the newest posts! :)');
end;

 

O último elemento do Flyweight – e o mais importante – é o Factory, responsável, finalmente, por identificar se o objeto existe na lista de objetos compartilhados. Em caso positivo, é retornado para o chamador. Caso contrário, o objeto é criado e adicionado na lista de objetos compartilhados para ser reaproveitado posteriormente. Essa lista também será um objeto da classe TStringList, pois fornece meios para trabalhar com a combinação chave/objeto, conforme veremos no método GetCartao.

type
  TFlyweightFactory = class
  private
    // variável para armazenar os objetos compartilhados
    ListaCartoes: TStringList;
  public
    constructor Create;
    destructor Destroy; override;
 
    // método principal do Flyweight,
    // responsável por encontrar e retornar o objeto já criado
    // ou criá-lo caso não exista, adicionando-o na lista de objetos compartilhados
    function GetCartao(const Pais: string): TCartao;
  end;
 
implementation
 
{ TFlyweightFactory }
 
constructor TFlyweightFactory.Create;
begin
  // cria a lista de objetos compartilhados
  ListaCartoes := TStringList.Create;
end;
 
destructor TFlyweightFactory.Destroy;
begin
var
  Contador: integer;
begin
  // libera os objetos compartilhados
  for Contador := 0 to Pred(ListaCartoes.Count) do
    ListaCartoes.Objects[Contador].Free;
 
  // libera a lista de objetos
  FreeAndNil(ListaCartoes);
  inherited;
end;
 
function TFlyweightFactory.GetCartao(const Pais: string): TCartao;
var
  Indice: integer;
begin
  result := nil;
 
  // tenta encontrar o objeto compartilhado através do nome do país
  if ListaCartoes.Find(Pais, Indice) then
  begin
    // caso seja encontrado, o objeto compartilhado é retornado
    result := (ListaCartoes.Objects[Indice]) as TCartao;
    Exit;
  end;
 
  // caso não seja encontrado, é criado
  // obs: aqui podemos utilizar um Factory Method
  if Pais = 'Espanha' then
    result := TCartaoEspanha.Create
  else if Pais = 'EUA' then
    result := TCartaoEUA.Create
  else if Pais = 'Brasil' then
    result := TCartaoBrasil.Create;
 
  // ... e depois adicionado na lista de objetos compartilhados
  // para que não precise ser criado novamente nas próximas iterações
  ListaCartoes.AddObject(Pais, result);
end;

 

Muito bem, pessoal, o nosso padrão de projeto está pronto! O próximo passo é utilizá-lo!
No Client (classe consumidora do padrão), criaremos o Factory e utilizaremos uma variável do tipo TCartao para receber as instâncias dos objetos compartilhados durante as iterações. Olhem só a facilidade:

var
  FlyweightFactory: TFlyweightFactory;
  Cartao: TCartao;
  FieldPais: TField;
  FieldNome: TField;
begin
  // cria o objeto da classe Factory do Flyweight
  FlyweightFactory := TFlyweightFactory.Create;
  try
    FieldPais := ClientDataSet.FindField('Pais');
    FieldNome := ClientDataSet.FindField('Nome');
 
    ClientDataSet.First;
    // executa um loop em todos os registros
    while not ClientDataSet.EOF do
    begin
      // chama o método GetCartao para retornar o objeto compartilhado
      // através do nome do país
      Cartao := FlyweightFactory.GetCartao(FieldPais.AsString);
 
      // altera a proprieade extrínseca, que é o nome do leitor
      Cartao.NomeLeitor := FieldNome.AsString;
 
      // chama o método para exportação do cartão
      Cartao.Exportar;
 
      ClientDataSet.Next;
    end;
  finally
    // libera o objeto da classe Factory do Flyweight
    FreeAndNil(FlyweightFactory);
  end;
end;

 

Para verificar a vantagem de desempenho, fiz questão de também codificar o mesmo procedimento sem utilizar o Flyweight. Nos meus testes de benchmarking, em um DataSet com 500 registros, houve um ganho de 3 segundos. Com 1000 registros, um ganho de 8 segundos. Pode parecer pouco, mas é importante destacar que os objetos criados neste artigo são bem básicos, com poucos atributos, e a quantidade de registros também é pequena. Em ambientes robustos, com classes compostas por dezenas de atributos e com um processamento envolvendo milhares de registros, a diferença será bem visível! Na verdade, acho correto afirmar que a eficiência do Flyweight é proporcional à dimensão de processamento. Quanto mais pesado, no sentido de utilização de objetos, mais rápido será o Flyweight em comparação com uma abordagem tradicional.

 

Qual a diferença de já criar os três objetos antes de iniciar o processamento?
Bom, e se não houver leitores da Espanha? O objeto da classe TCartaoEspanha será criado desnecessariamente, concorda? O Factory do Flyweight possui justamente essa responsabilidade de criar os objetos sob demanda.

 

Quando utilizarei este padrão de projeto?
Algumas literaturas sobre Design Patterns mencionam que o Flyweight é utilizado em casos bem específicos, o que implica na sua baixa utilização. No entanto, jamais será um padrão de projeto desdenhado. Um exemplo clássico, comum de se encontrar na internet sobre o Flyweight, é a reutilização de objetos de caracteres em processadores de texto. Ao invés de criar um objeto para cada caractere digitado – no qual carrega informações como fonte, tamanho, cor e formato – utiliza-se o Flyweight para compartilhá-los, reduzindo bastante o consumo de memória. Outro exemplo são exploradores de arquivos que podem compartilhar objetos do mesmo tipo de arquivo para reduzir o tempo de carga e exibição.

No link abaixo, disponibilizo o exemplo deste artigo com algumas melhorias. Neste projeto, há dois botões: o primeiro executa a rotina utilizando o Flyweight e o segundo botão executa a rotina de uma forma tradicional, criando e destruindo os objetos dentro do loop. No final de cada processamento, exibo o tempo gasto para comparar as duas formas.

Exemplo de Flyweight com Delphi

 

Pessoal, em caso de qualquer dúvida, sugestão ou correção no artigo, deixe um comentário! Muitos leitores estão agregando valor nos artigos através dos comentários publicados! Aproveitando, deixo aqui o meu forte agradecimento a todos vocês!
Abração!


 

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

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.