Decorando um jogo com o padrão de projetos Decorator – Parte 1
Você e 4 amigos, uma esquadrilha de netherdrakes, helicópteros, grifos e hipogrifos, girando por Oshu’gun enquanto procuram o gigante comedor de dragões Durn the Hungerer. Apesar da ameaça, vocês estão confiantes em seu grupo. O entardecer de Nagrand mostra a silhueta do gigante no horizonte e vocês sabem que a hora chegou.
A luta foi perfeita: o gigante arrasou com o grupo nos 20s mais estilosos que qualquer monstro do World of Warcraft já viu. Depois de 15min de brigas sobre quem tem a culpa, alguém descobre que o grupo não estava tão preparado assim. Todos então resolvem melhorar seus equipamentos... Mas como?
Apresento-lhes a morte do grande gigante, o padrão Decorator.
Vários jogos permitem a seus jogadores um vasto leque de personalização. Eles permitem que configuremos os personagens para que eles sejam o mais parecido possível com a gente em muito mais que só altura e cor de cabelos. Tais jogos permitem que configuremos também a forma com que esse personagem reage ao mundo de jogo, definindo coisas como a velocidade, o tipo de magia que ele usa ou o quanto sustenta de dano. Alguns jogos permitem até que você configure os itens que eles usam. Assim é o World of Warcraft, a febre do momento no mundo dos RPGS massivos (os MMORPGS).
Para vencer os desafios no WoW, os jogadores podem utilizar encantamentos, gemas, itens e diversos efeitos diferentes para aumentar suas características. Se foi isso que faltou no grupo, é isso que daremos a ele.
Comecemos pegando uma das armas dos jogadores. Essa arma exemplo será a classe TArma abaixo:
package Efeitos
{
public class TArma
{
protected var _nome:String; // Damos um nome qualquer e …
protected var _efeito:String; // … um efeito…
public function TArma()
{
// ... mas setamos para os valores normais ...
// ... afinal de contas, a arma não tem nada de especial ...
// ... ainda
this._nome = "Arma sem Bônus";
this._efeito = "";
}
public function retornaNome():String
{ return this._nome; }
public function retornaEfeito():String
{ return this._efeito; }
}
}
Essa arma é o que os personagens têm no momento e, como já sabemos, isso não é o suficiente. Que tal incluirmos esses tais efeitos e encantamentos? Assim estaríamos adicionando a força necessária que os personagens precisam, não Tiago? É só criar uns métodos retornaEncantamento() como você fez e pronto.
Não só poderíamos adicionar tais características como várias outras características à arma. O problema é o futuro: a cada vez que um ou vários novos bônus aparecessem no nosso jogo, fossem modificados ou simplesmente excluídos do jogo (o que acontece com muita frequência), teríamos que atualizar essa classe Tarma, bem como TODO O CÓDIGO fazendo as chamadas a esses bônus, no caso deles simplesmente não existirem mais . Isso a torna alvo de alterações cíclicas, o que é longe de ser legal. A idéia aqui é encapsular essas alterações, aplicando-as como um skin ou tema à arma.
Para conseguirmos tal estrutura, usaremos o padrão Decorator: iremos “enfeitar” a arma com quantos bônus especiais precisarmos, aproveitando que ela já possui algo engatilhado para isso (afinal de contas, o método retornaEfeito() não está lá à toa) . Para isso, precisamos inicialmente criar a classe TDecoratorBonus:
package Efeitos
{
//import Status.TStatus;
public class TDecoratorBonus extends TArma
{
protected var _arma:TArma; // O Decorador possui uma instância de TArma.
/***************************************************************
Construtor da classe. Tem os atributos da TArma a ser decorada ...
... por ser um tipo de arma.
****************************************************************/
public function TDecoratorBonus()
{
this._nome = "sem nome";
this._efeito="";
}
/***************************************************************
Esse método sobrescreve (overrides) o método retornaNome() da ...
... classe TArma.
****************************************************************/
public override function retornaNome():String
{ return this._nome; }
}
}
Essa classe é o coração do nosso decorador: ela é a responsável pela estrutura de encapsulamento dos diversos efeitos. A principal estrela dessa clase é, sem dúvidas, o atributo protected var _arma:TArma. Sua função é guardar a camada de efeitos no caso dessa camada ser envolvida por outra camada. Não entendi nada, Tiago.Como assim camada envolvida por outra camada? Bom, se você pensar bem, estou falando de camadas como aquelas bonecas russas chamadas matrioshka que guardam outras bonecas idênticas dentro: essa instância seria uma "âncora" para que pudéssemos ir para a "boneca de dentro". Essa boneca de dentro, por sua vez, também tem sua boneca de dentro até o ponto em que não haja mais bonecas dentro.
Cada uma dessas bonecas é, no nosso exemplo, uma das camadas de bônus do padrão decorador, como a classe abaixo:
package Efeitos
{
public class TEfeito extends TDecoratorBonus
{
/***************************************************************
Construtor da classe recebe um objeto TArma. Ele é jogado para ...
... a instância this._arma, encapsulando assim mais um nível para ...
... a recursividade do padrão.
****************************************************************/
public function TEfeito(arma:TArma)
{
//Os seguintes atributos vieram por herança de TDecoratorBonus;
this._arma = arma;
this._efeito = "+10";
}
/***************************************************************
Método que sobrescreve o retornaEfeito da classe TDecoratorBonus, ...
... chamando ele próprio na instância this._arma da classe. Dessa ...
... forma, o método inicia uma cadeia de chamadas que termina ...
... na classe mais interna, a que não possui uma instância de ...
... _arma.
****************************************************************/
public override function retornaEfeito():String
{
// Por ser um padrão de recursividade, é obrigatório que...
// ... chamemos this._arma.retornaEfeito(). Só assim o ...
// ... método continuará chamando os efeitos encapsulados.
return (this._efeito + " " + this._arma.retornaEfeito());
}
}
}
Para fazer as camadas de bônus funcionarem, entra em ação uma coisa chamada recursividade: de forma simplificada (e, quem sabe, um dia eu escreva de forma completa), recursividade é a capacidade de um método ou função chamar a si mesma por várias vezes, retornando resultados para ela própria. Por enquanto, basta saber que o método retornaEfeito() é recursivo por chamae retornaEfeito() novamente, mas agora na camada de dentro (que, como já sabemos, é o atributo this._arma). A camada interna, por sua vez, chama retornaEfeito() de sua this._arma, que chama retornaEfeito() de sua this._arma, que chama retornaEfeito() de sua... Isso só acaba quando chegamos na arma inicial, a TArma, cujo método retornaEfeito() não chama outro retornaEfeito() e sim, exibe seu this._efeito.
Nesse momento, cada retornaEfeito() chamado retorna seu this._efeito para o método retornaEfeito() que o chamou, que concatena com seu próprio this._efeito e retorna para o método retornaEfeito() que o chamou, que concatena com seu próprio this._efeito e retorna para o método retornaEfeito() que o chamou, que concatena com seu próprio this._efeito e retorna... O fim desse ciclo se dá quando não houver mais retornos a chamadas a retornaEfeito(), o que no nosso exemplo acontece na classe TMain mais à frente.
Essa estrutura fica assim:

Camadas
Com o padrão conseguimos criar uma arma genérica que possui ao seu redor várias camadas de efeitos e que funciona do exato mesmo jeito que uma arma que não possui efeito algum. Além disso, podemos ter diferentes tipos de efeitos, cada um implementado por uma classe diferente. Vou criar agora um TEncantamento:
package Efeitos
{
import Efeitos.TDecoratorBonus;
public class TEncantamento extends TDecoratorBonus
{
public function TEncantamento(arma:TArma)
{
this._arma = arma;
this._efeito = "Mongoose";
}
public override function retornaEfeito():String
{
return (this._arma.retornaEfeito() + " " + this._efeito);
}
}
}
Por fim, basta decorarmos a arma:
package
{
import flash.display.MovieClip;
import Efeitos.TDecoratorBonus;
import Efeitos.TEncantamento;
import Efeitos.TEfeito;
import Efeitos.TArma;
public class TMain extends MovieClip
{
public function TMain()
{
// Criamos uma arma qualquer ...
var arma = new TArma();
trace ("Antes dos efeitos"); // ... e exibimos seus bônus iniciais.
trace ( arma.retornaEfeito() );
trace ();
trace("---------------------------------------");
trace ();
// Agora o padrão entra em ação. Criamos um efeito TEfeito que ...
// encapsula a arma atual e é passado para ela mesma. Agora ...
// arma tem dentro de si um TEfeito.
// Efeito +10
arma = new TEfeito(arma);
// Pegamos agora a TEfeito e jogamos dentro de TEncantamento, ...
// ... passando novamente para a arma. Agora ela tem um ...
// TEncantamento que tem um TEfeito.
arma = new TEncantamento(arma);
// ... que vai dentro de mais um TEfeito ...
arma = new TEfeito(arma);
// ... que termina dentro de outro TEfeito
arma = new TEfeito(arma);
// Ao realizarmos a chamada a arma.retornaEfeito(), o próprio ...
// ... método se encarrega de chamar retornaEfeito() das ...
// ... instâncias encapsuladas, de forma recursiva.
trace ( arma.retornaEfeito() );
}
}
}
No próximo artigo eu disponibilizo o projeto (Flash CS3) e os arquivos das classes (Action Script 3.0). Falo também do diagrama de classes dessa brincadeira toda. Até lá, caso alguém tenha qualquer dúvida, basta perguntar.
No final de uma longa batalha, O gigante finalmente tomba. Seu colossal corpo vai agora servir de alimento para as criaturas que das quais sua linhagem alimentou-se durante eras. Os heróis retornam para suas montarias e tudo volta a ser tranqüilo, graças ao padrão Decorator.
outubro 10th, 2009 - 13:32
Excelente artigo, Frossard. Vou recomendá-lo para os camaradas do serviço.
Parabéns.
outubro 11th, 2009 - 10:09
Valeu, Mário. E vê se aparece mais vezes, teu sumido.
dezembro 27th, 2009 - 11:53
Para ser sincero, não entendi a utilidade nem a praticidade disso tudo…
Creio que o certo seria uma classe “TArmaBase” e uma classe “TArmaItem”. O item tem referência à arma base, mais um vetor com quaisquer os efeitos que seja necessário adicionar. Uma função na classe TArmaItem avalia os efeitos (se isso for necessário).
Por exemplo, a base da arma – digamos, Espada Larga – tem 12 de força. Você recebe uma Espada Larga no jogo e um Item é adicionado ao seu inventário. Se, por exemplo, você levar a espada a um ferreiro e ele aumentar a força em 5 pontos, você adiciona o efeito “força +5″ ao item. Para retornar a força final, você chama uma função retornaForça() que calcula a força final.
Não por menos, o artigo está muito bem escrito, parabéns. Mas sua lógica não funciona muito bem na prática…
dezembro 28th, 2009 - 00:12
Pois então, Leonardo. Inicialmente, quero esclarecer que essa não é uma lógica minha e sim uma padrão de projetos já catalogado é utilizado há anos, como visto aqui http://nusseagora.blog.br/padroes-de-projeto-o-que-sao-e-pra-que-servem/ .
Pensando imediatamente, sua solução parece ser bem melhor. Porém, a longo prazo, se eu entendi bem, ela vai gerar problemas de manutenibilidade.
Você disse para colocarmos em um array os valores dos bônus. Isso implica na reprodução do mesmo efeito em várias armas diferentes, um +5 em cada. Porém, quando tivermos que balancear esse efeito para, por exemplo, +6, teremos que ir na mão em todos os objetos e alterar tal valor, já que ele não está centralizado em uma classe específica.
Além disso, vc diz para que um método em TArmaItem avalie os efeitos possíveis de TArmaItem. Isso também parece o mais simples a ser feito, porém torna-se complicado trabalhar com essa lógica em um jogo como o World of Warcraft (do meu exemplo) onde temos mais de 200 efeitos possíveis em um item. Teríamos um método gigantesco com um switch case que gastaria processamento e memória para descobrir, por exemplo, que somente 1 desses efeitos estava ativo no seu item.
Também há o cenário onde tal método fosse obrigado a testar vários bônus que não fossem específicos daquela arma.
O Decorator resolve esses problemas de uma forma prática, extremamente compacta e manutenível por dar a um objeto novas responsabilidades em tempo de execução e não em tempo de desenvolvimento. O código base nunca muda, independente de quantos efeitos uma arma venha a ter, levando também em conta dela não ter efeito algum.
Espero que tenha endendido a vantagem do padrão. Para mais informações, não deixe de ler também o artigo sobre ele na wikipédia: http://en.wikipedia.org/wiki/Decorator_pattern