Desenvolvimento

7 jan, 2019

Arquitetura e desenvolvimento de software – Parte 11: Flyweight

Publicidade

E aí, galera! Tudo certo?

Retomando nossos estudos sobre design patterns, hoje vamos abordar o pattern Flyweight!

Flyweight

O padrão Flyweight é utilizado quando temos vários objetos que devem ser tratados em memória, onde muitos deles possuem informações repetidas. Considerando que memória é um recurso limitado, podemos segregar estas informações repetidas em um objeto adicional imutável e comparável.

Resumindo o parágrafo acima de uma forma mais simples e clara, usamos um objeto compartilhando ele o máximo possível, reduzindo a alocação de memória.

Um exemplo clássico do uso deste pattern é uma estrutura de dados para representação gráfica de caracteres em um editor de texto. O ideal é termos uma representação gráfica para cada caractere digitado, com sua fonte, tamanho, etc., mas alocar cada um consumiria muita memória. Em vez disso, para cada caractere, temos uma referência para um objeto que representa graficamente o caractere, sendo este objeto denominado um Flyweight, compartilhado entre todas as instâncias/ocorrências deste caractere.

De acordo com o livro Design Patterns do Gang of Four, o pattern Flyweight foi criado e muito explorado por Paul Calder e Mark Linton em meados de 1990, para gerenciar informações de forma efetiva em um editor de textos do tipo WYSIWYG.

Outra aplicação deste pattern é no desenvolvimento de jogos. Quem aqui já jogou Age of Empires II?

Tela do jogo Age of Empires II, onde temos muitos, mas muitos objetos em atividade na tela, mas com representações gráficas comuns, repetidas.

Cada representação gráfica distinta é instanciada apenas uma vez, e cada objeto, soldado, cavalo, elefante, etc, faz referência a essa instância de acordo com seu tipo de representação.

Um detalhe de atenção é que, para garantir o compartilhamento de forma segura, entre clientes e threads, objetos Flyweight devem ser imutáveis.

Vamos analisar os detalhes do pattern diretamente na ficha resumo e ver alguns exemplos utilizando C#.

Ficha resumo

  • Nome: Flyweight;
  • Objetivo/intenção: quando precisamos otimizar o uso de memória com estruturas de dados onde temos muitos dados repetidos;
  • Motivação: quando temos estruturas de dados grandes, com uma grande quantidade de dados repetidos, alocar todos em memória pode ser extremamente custoso, dado que memória é um recurso limitado. Este padrão surgiu justamente para otimizar o uso de memória, consistindo no compartilhamento de uma instância entre vários objetos diferentes.
  • Aplicabilidade: o padrão Flyweight é bastante utilizado quando queremos evitar a alocação de instâncias repetidas em memória, dado que este é um recurso limitado. Um exemplo de utilização é em editores de texto com fontes, onde temos uma única instância de representação gráfica para cada caractere, onde cada caractere digitado no documento apenas aponta para a instância correspondente;
  • Estrutura: no desenho abaixo, temos um exemplo UML de aplicação do pattern para desenhar círculos em diferentes localizações. Descrevendo cada parte, temos:
Representação UML do pattern Flyweight
  • ShapeFactory: possui um HashMap interno de círculos, tendo como chave de identificação a cor do círculo. Sempre que um novo círculo for requisitado com uma cor específica, ele irá verificar se já não construiu algum daquela cor. Se sim, fornecerá uma referência para a o círculo já existente, caso contrário, irá criar um novo, armazenar no HashMap e fornecer a referência dele.
  • FlyweightPatternDemo: classe de demonstração, apenas para exemplificar o cliente que está utilizando a Factory, solicitando os círculos e suas cores.
  • Shape e Circle: interface para definir uma forma gráfica e implementação da mesma como círculo, que são utilizadas pelo ShapeFactory e fornecidas ao cliente FlyweightPatternDemo.
  • Consequências: o Flyweight resolve problemas com alocação de memória, onde temos estruturas de dados grandes e com dados repetidos, garantindo uma redução na alocação de recursos limitados e otimizando a performance de aplicações, mas temos que tomar cuidado com a questão de concorrência, onde temos a criação de objetos em múltiplas threads. Se a lista de valores for limitada e tivermos conhecimento prévio disto, podemos instanciar antecipadamente e obter os objetos de um contêiner centralizado. Porém, se tivermos o instanciamento em múltiplas threads, temos algumas opções como tornar a instanciação para single threaded, ou através de semáforos, garantindo uma instância por valor, ou permitir a criação de mais de um valor do mesmo tipo, filtrando/eliminando ao obter os valores, mas somente se seu objeto for capaz de ser comparável com outros do seu tipo.
  • Implementações: Abaixo temos um exemplo de código em C# e Java para o pattern:
using System;
using System.Collections;

namespace DemoCSharp.FlyweightPattern
{
    //Classe abstrata Flyweight
    abstract class Flyweight
    {
        public abstract void Operacao(string parametro);
    }

    //Classe concreta, implementando a classe abstrata acima
    class ImplementacaoFlyweight : Flyweight
    {
        private string _parametro;
        
        public ImplementacaoFlyweight()
        {
            _parametro = "";
        }
        
        public override void Operacao(string parametro)
        {
            _parametro = _parametro + "|" + parametro;
            Console.WriteLine("Implementação de Operação: " + _parametro);
        }
    }

    //Factory para produzir e controler a classe Flyweight
    class FlyweightFactory
    {
        private Hashtable _flyweights = new Hashtable();

        public FlyweightFactory()
        {
            _flyweights.Add("TipoA", new ImplementacaoFlyweight());
            _flyweights.Add("TipoB", new ImplementacaoFlyweight());
            _flyweights.Add("TipoC", new ImplementacaoFlyweight());
        }

        public Flyweight ObterFlyweight(string chave)
        {
            return((Flyweight)_flyweights[chave]);
        }
    }

    //Classe Main para execução do App
    class MainApp
    {
        static void Main()
        {
            var parametroA = "xpto";
            var parametroB = "xpto2";
            
            var factory = new FlyweightFactory();
            
            var fA = factory.GetFlyweight("TipoA");
            fA.Operacao(parametroA);
            
            var fB = factory.GetFlyweight("TipoB");
            fB.Operacao(parametroB);
            
            var fA2 = factory.GetFlyweight("TipoA");
            fA2.Operacao(parametroA);
            
            Console.ReadKey();
        }
    }
}
//------------------------------------------------------
// Shape.java
// Interface Shape, representando uma forma gráfica
public interface Shape {
    void draw(); 
}
//------------------------------------------------------

//------------------------------------------------------
// Circle.java
// Classe Circle, implementando a interface Shape
public class Circle implements Shape {
    private String color;
    private int x;
    private int y;
    private int radius;
  
    public Circle(String color) {
        this.color = color; 
    }
  
    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Circle: Draw() [Color : " + color + ", x : " + x + ", y :" + y + ", radius :" + radius);
    }
}
//------------------------------------------------------

//------------------------------------------------------
// ShapeFactory.java
// Classe Factory para produzir e controlar Shapes
import java.util.HashMap;

public class ShapeFactory {

   @SuppressWarnings("unchecked")
   private static final HashMap circleMap = new HashMap();

   public static Shape getCircle(String color) {
      Circle circle = (Circle)circleMap.get(color);

      if(circle == null) {
         circle = new Circle(color);
         circleMap.put(color, circle);
         System.out.println("Creating circle of color : " + color);
      }
      return circle;
   }
}
//------------------------------------------------------

//------------------------------------------------------
// FlyweightPatternDemo.java
// Classe cliente que utiliza nosso factory
public class FlyweightPatternDemo {
   private static final String colors[] = { "Red", "Green", "Blue", "White", "Black" };
   public static void main(String[] args) {

      for(int i=0; i < 20; ++i) {
         Circle circle = (Circle)ShapeFactory.getCircle(getRandomColor());
         circle.setX(getRandomX());
         circle.setY(getRandomY());
         circle.setRadius(100);
         circle.draw();
      }
   }
   private static String getRandomColor() {
      return colors[(int)(Math.random()*colors.length)];
   }
   private static int getRandomX() {
      return (int)(Math.random()*100 );
   }
   private static int getRandomY() {
      return (int)(Math.random()*100);
   }
}
//------------------------------------------------------
  • Usos conhecidos: Muito utilizado na construção de aplicações com grandes estruturas de dados ou objetos, como editores de texto e jogos, onde otimizamos o uso de memória e até processamento, trabalhando com referências para instâncias únicas de objetos que podem se repetir por N vezes durante o fluxo de execução do programa.
  • Padrões relacionados: Bridge, Proxy.

Concluindo

Este foi o padrão Flyweight, com o qual podemos otimizar o uso de recursos como memória na utilização de grandes estruturas de dados, principalmente na produção de jogos.

Na próxima parte da série abordaremos o padrão Facade (ou “Façade”, como pronunciado em alguns lugares).

Um abraço a todos e até a próxima!