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!
Introdução
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á? 🙂
Analogia
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.
Exemplo de codificação do Flyweight
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, além de 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.
Interface Flyweight
A primeira codificação é da Interface Flyweight, uma simples abstração que será implementada posteriormente pelos Concrete Flyweights:
1 2 3 4 5 6 7 8 9 10 11 |
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; |
Concrete Flyweights
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:
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 |
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.
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 |
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; |
Classe Factory
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
.
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 |
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!
Em ação!
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:
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 |
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.
Conclusão
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.
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.
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!
André! Obrigado por esse ótimo artigo. Não só por esse, mas por todos sobre design patterns. Li cada um. Obrigado!
Agora, e possÃvel usar o Flyweight para objetos visuais?
Estou tentando aplicar o conceito em um exemplo simples que é colocar 10 Tshape no form, cada um podendo ter a cor Amarelo, verde ou azul.
E ao invés de criar 10 objetos TShape, é criado apenas 3, um de cada cor.
O PROBLEMA É: Ao criar o TShape amarelo pela segunda vez por exemplo, o primeiro TShape amarelo some da tela.
Não sei se é possÃvel, mas aqui você fala em “uma rotina que desenha 1000 campos de texto em uma tela” usando o flyweight.
Agradeço muito quem puder tirar esse minha dúvida!
Segue o download do código fonte do seu exemplo adaptado para o que preciso:
https://files.fm/u/uunk6jnx#_
Obrigado!
Olá, Bruno, boa noite!
Excelente pergunta! Quando eu estudei o Flyweight, também esbarrei nessa mesma dúvida. Na época, eu assisti um vÃdeo sobre este padrão de projeto no canal de um desenvolvedor chamado Derek Banas, que desenvolve os exemplos em Java. Neste vÃdeo, ele apresentou este mesmo contexto das figuras com cores e, assim como você, não consegui reproduzi-lo no Delphi.
Após algumas pesquisas, eu encontrei um artigo que explicava a aplicação do Flyweight em cenários de jogos. As árvores que compõem os cenários, por exemplo, são desenhadas com o Flyweight para reduzir o tempo de processamento. Cada árvore é uma “cópia” de alguma árvore já existente. Com isso, eu finalmente compreendi que o Flyweight não se trata necessariamente do reaproveitamento de objetos, mas do reaproveitamento das propriedades dos objetos.
Ao criar o TShape amarelo pela segunda vez, portanto, não se deve usar o mesmo objeto. Cria-se um novo TShape, mas que receberá as propriedades do TShape existente, como se estivéssemos clonando o objeto. O objetivo do Flyweight, neste caso, é permitir o compartilhamento de propriedades entre os objetos. A cor amarela, por exemplo, se encaixa nesse compartilhamento.
Este contexto de figuras, no entanto, não representa claramente a vantagem do Flyweight. Imagine um pool de usuários no qual cada novo login deve abrir uma conexão com o banco de dados. Ao invés de termos várias conexões paralelas com o banco de dados (uma para cada usuário), poderÃamos usar o Flyweight para “compartilhar” o objeto de conexão entre todos os usuários. Esse é basicamente o propósito.
Abraço!