Evitando duplicação de objetos com Flyweight
Entenda como o design pattern Flyweight pode te ajudar a economizar memória
Durante o desenvolvimento nos deparamos com algumas situações em que precisamos utilizar várias instâncias de classes e, muitas vezes, essas instâncias são repetidas. No final, acabamos com várias objetos do mesmo tipo em memória.
No desenvolvimento de jogos de aventura ou plataforma, por exemplo, onde utilizamos várias imagens que representam as entidades que compõe o jogo como cenários, inimigos, personagens, golpes, magias etc é comum termos vários objetos do mesmo tipo em memória. Neste caso, em um determinado cenário podemos ter vários inimigos do mesmo tipo que carregam o mesmo conjunto de imagens, ou seja, os "mesmos" tipos de objetos e podem efetuar vários golpes repetidamente.
No desenvolvimento orientado a objetos normalmente criamos uma classe para cada entidade, sejam eles personagens, cenários, movimentos etc. Isso faz parte da modelagem quando trabalhamos com este paradigma. O problema é que em muitos desses casos as classes instanciadas são as mesmas, eles estão apenas ocupando espaço na memória sem necessidade. Além disso, criar objetos é uma tarefa muito cara para JVM.
Para entender melhor o problema, vejamos um exemplo.
Imagine que queremos implementar uma pequena parte de um jogo de luta, apenas a que tenha responsabilidade de efetuar os golpes em sequência. Claro que iremos fazer um rascunho disso,somente para imprimir os golpes no console:
Soquinho apelativo...
Soquinho apelativo...
Socão na cara...
Chute...
Frontal nos peitos...
Como disse, o foco é apenas mostrar a idéia e facilitar o exemplo.
Inicialmente um desenvolvedor poderia criar uma coleção e nela adicionar as entidades que representam cada golpe, como abaixo:
List<Object> combo = Arrays.asList(new Soco(), new Chute(), new Soco(), new Soco(), new CruzadoDeDireita(), new UpperCut(), new Soco());
Observe que cada golpe é representado por uma classe e cada instância é armazenada numa coleção de tipos Object
.
Podemos melhorar um pouco esse código substituindo o tipo Object
, que é muito genérico, por uma interface que represente melhor os golpes, como a interface Golpe
:
public interface Golpe {
public String executa();
}
Dessa forma, cada classe de golpe irá implementar nossa nova interface:
public class Soco implements Golpe {
public String executa() {
return "Socão na cara...";
}
}
Vejamos outro exemplo, agora a classe Soquinho
:
public class Soquinho implements Golpe {
public String executa() {
return "Soquinho apelativo...";
}
}
Nossa coleção de golpes mudaria para:
List<Golpe> combo = Arrays.asList(new Soco(), new Chute(), new Soco(),
new Soco(), new CruzadoDeDireita(), new UpperCut(), new Soco());
Por fim, para executar essa sequência de golpes, bastaria um loop como a seguir:
for (Golpe golpe : combo) {
System.out.println(golpe.executa());
}
Agora, se criarmos novamente a mesma sequência de golpes e armazenarmos numa segunda coleção, acabaremos criando novas instâncias para cada golpe:
// lista #1
[ufc.Soco@677327b6, ufc.Chute@14ae5a5, ufc.Soco@7f31245a, ufc.Soco@6d6f6e28, ufc.CruzadoDireita@135fbaa4, ufc.UpperCut@45ee12a7, ufc.Soco@330bedb4]
// lista #2
[ufc.Soco@2503dbd3, ufc.Chute@4b67cf4d, ufc.Soco@7ea987ac, ufc.Soco@12a3a380, ufc.CruzadoDireita@29453f44, ufc.UpperCut@5cad8086, ufc.Soco@6e0be858]
O "soco" será o mesmo (imagem, danos, velocidade etc), porém temos aqui 8 instâncias diferentes para ele. Nosso exemplo é pequeno, mas imagine um jogo de verdade, será que daremos apenas 10 ou 20 golpes?
Esse código não está errado e é até utilizado no cotidiano. Porém, as vezes não tomamos ciência em como isso pode ser prejudicial ao desempenho do nosso jogo ou mesmo sistema. Nestes casos iremos ter um grande problema de uso desnecessário da memória.
Precisamos de alguma forma diminuir o número de instâncias do mesmo objeto em memória. Queremos algo assim:
List<String> combo = Arrays.asList(
"soco","soco","cotovelada","cruzado-direita","cruzado-esquerda","soco","upper");
Veja que nesse exemplo usamos o soco 3 vezes, porém, queremos que ele tenha sido instanciado apenas uma única vez. O que queremos no final é: não precisar instanciar várias vezes o mesmo objeto.
O que devemos resolver agora é o problema das instancias desnecessárias e para isso iremos criar uma nova classe que terá essa responsabilidade de criar apenas uma instância de cada golpe, a classe GolpesPesoMosca
.
Iremos criar um atributo estático nesta classe:
private static Map<String, Golpe> golpes = new HashMap<>();
Esse mapa de golpes será utilizado para guardar as instâncias dos objetos que queremos trabalhar, como soco, soquinho, cotovelada, frontal, entre outros. Para simplificar, iremos identificar cada instância de objeto pelo nome em formato String
mesmo.
Agora precisamos que esses golpes sejam instanciados uma única vez e para isso utilizaremos um bloco estático na classe:
static {
golpes.put("soco", new Soco());
golpes.put("soquinho", new Soquinho());
golpes.put("chute", new Chute());
golpes.put("cotovelada", new Cotovelada());
golpes.put("cruzadoDeDireita", new CruzadoDeDireita());
golpes.put("cruzadoDeEsquerda", new CruzadoDeEsquerda());
golpes.put("frontal", new Frontal());
golpes.put("upper", new UpperCut());
}
Lembre-se que um bloco estático é executado quando a JVM carrega pela primeira vez a classe, dessa forma ele é executado apenas uma única vez.
Precisamos agora de um método para pegar esses objetos no mapa. Vamos criar então o método get
que irá receber a chave do mapa, que no nosso caso é o nome do golpe:
public static Golpe get(String nome) {
return golpes.get(nome);
}
Esse método apenas recebe como parâmetro o nome do golpe que você deseja e devolve a instância do mesmo.
No fim, nossa classe GolpesPesoMosca
ficará semelhante a esta:
public class GolpesPesoMosca {
private static Map<String, Golpe> golpes = new HashMap<>();
static {
golpes.put("soco", new Soco());
golpes.put("soquinho", new Soquinho());
golpes.put("chute", new Chute());
golpes.put("cotovelada", new Cotovelada());
golpes.put("cruzadoDeDireita", new CruzadoDeDireita());
golpes.put("cruzadoDeEsquerda", new CruzadoDeEsquerda());
golpes.put("frontal", new Frontal());
golpes.put("upper", new UpperCut());
}
public static Golpe get(String nome) {
return golpes.get(nome);
}
}
O que vai acontecer aqui é o seguinte: Quando instanciarmos a classe GolpesPesoMosca
ela irá possuir todos os golpes já instanciados que podemos utilizar no jogo, assim podemos utilizá-los quantas vezes acharmos necessário sem que precisemos criar novos objetos, dessa forma acabamos tendo uma economia considerável de memória, pois estamos compartilhando informações.
Vamos testar nosso programa, iremos criar uma classe com o método main
e utilizar o que fizemos até agora:
public class Programa {
public static void main(String[] args) {
GolpesPesoMosca golpes = new GolpesPesoMosca();
List<Golpe> combo = Arrays.asList(
golpes.get("soquinho"),
golpes.get("soquinho"),
golpes.get("soco"),
golpes.get("chute"),
golpes.get("frontal"));
System.out.println(combo);
}
}
Ao executar esse programa você poderá observar que na saída padrão no console os objetos "repetidos" na coleção são os mesmos, ou seja, mesma instância em em memória. E toda vez que precisarmos utilizar a instância de um deles iremos sempre utilizar a mesma.
[ufc.Soquinho@677327b6, ufc.Soquinho@677327b6, ufc.Soco@14ae5a5, ufc.Chute@7f31245a, ufc.Frontal@6d6f6e28]
Observe a saída do Soquinho
e veja que é a mesma instância: [Soquinho@677327b6, Soquinho@677327b6]
.
Para finalizar, vamos imprimir no console alguns golpes, simulando uma sequência de golpes que o Rafael Ponte adora usar no jogo The King of Fighters 2002. Para isso, vamos criar a classe ApelacaoKof2002
.
public class ApelacaoKof2002 {
public static void main(String[] args) {
GolpesPesoMosca golpes = new GolpesPesoMosca();
List<Golpe> combo = Arrays.asList(
golpes.get("soquinho"),
golpes.get("soquinho"),
golpes.get("soquinho"),
golpes.get("soco"),
golpes.get("chute"));
System.out.println(combo);
for (Golpe golpe : combo) {
System.out.println(golpe.executa());
}
}
}
O resultado será algo assim:
Soquinho apelativo...
Soquinho apelativo...
Soquinho apelativo...
Socão na cara...
Chute...
Esse é um tipo de problema comum no desenvolvimento. A solução que acabamos de utilizar para esses tipos de situações de duplicação de objetos é um desing pattern chamado de Flyweight que na tradução literal seria "Peso Mosca". O objeto continua com a sua força, porém sem ocupar muito espaço em memória.
Existem outras maneiras de implementar esse padrão de projeto, esse foi apenas um exemplo simples e didático. Você pode utilizar classes concretas, enums e até outros padrões auxiliares, como o padrão Composite, por exemplo.
Enfim, existem várias maneiras e combinações de implementá-lo, mas o importante é você entender que o padrão Flyweight tem uma finalidade bem definida:
“Usar compartilhamento para suportar eficientemente grandes quantidades de objetos de granularidade fina.” -- GAMMA, Erich.
O padrão Flyweight é apenas mais um dos muitos design patterns que temos disponíveis, assim como os padrões Factory, DAO entre outros. Padrões esses que nos ajudam a tornar o código mais coeso, fácil de testar e manter. É por isso que nos nossos cursos sempre estamos abordando vários deles e discutindo em sala o uso de cada um.
E ai? Você tem algum código no seu projeto na qual você pode aplicar esse design pattern?
Desenvolvedor, Fundador da TriadWorks e da JavaCE