Introdução
Pretendo criar tópicos práticos para todos os padrões de projeto (Design Pattern) GoF. A nomeclatura "GoF", derivada do título "Gang of Four" foi concebida ao longo do tempo para indicar os padrões de projeto tratados, com excelência, aliás, pelos autores Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides que publicaram o livro Design Patterns: Elements of Reusable Object-Oriented Software em 1995.
O uso dos padrões nem sempre fica claro quando estuda-se a concepção e utização dos mesmos diretamente. Nesse conjunto de tópicos trabalharemos sob uma perspectiva diferente: mostraremos um problema e construiremos uma solução. Ao final faremos a relação entre o padrão estudado e solução aplicada. Talvez dessa forma o entendimento do conceito, após a prática, seja beneficiado.
Outro detalhe: todos os padrões serão aplicados sob um projeto fictício de um Jogo 2D On-line. É claro que abordaremos casos pequenos e simples, para que não haja impensílios relacionados à codificação Java SE (ou EE).
Análise e Requisitos do Sistema
Nosso jogo fictício consiste numa série de mapas, onde múltiplos jogadores on-line lutam uns contra os outros com armas variadas. Um personagem poderá ter várias armas, obtidas aleatoriamente pelos mapas. Para melhorar a jogabilidade, as armas não serão pré definidas. Elas serão montadas a partir de classes de armas no momento em que o personagem a encontra. Em outras palavras, quando o jogador encontrar uma arma, o sistema do jogo escolherá um tipo de arma e criará suas características. Isso tudo será controlado de forma aleatória para maximizar a variedade de itens que existirão no jogo. Um detalhe importante: se um jogador pegar uma arma de outro jogador, a arma obtida já estará instanciada com os valores iniciais e não sofrerá nenhuma alteração.
Vamos considerar que existem apenas três tipos de armas:
SWORD
|
STAFF
|
RING
|
| | |
A espada (SWORD) provocará ataques físicos de corte, em geral, com danos maiores que as outras armas. O bastão (STAFF) lançará chamas no inimigo, provacando um ataque de FOFO com dano geralmente inferior ao da espada. Já o anel (RING) lançará magias de congelamento. Esse item oferece o menor dano dos três tipos de armas, porém, pode deixar o inimigo paralizado. A paralizia não será tratada aqui - foi apenas comentada para jutificar o pouco poder atribuído ao anel.
Modelagem do sistema
Vamos então fazer a modelagem apropriada para o sistema. De cara, podemos destacar uma classe para a arma. Essa será chamada de Weapon e terá os atributos power e type para definir o poder de ataque e o tipo de dano, respectivamente. O foco aqui é a modelagem das armas considerando o ataque, apenas. Questões como peso da armas, tamanho, preço, etc, foram por conveniência omitidos.
Será que essa é uma boa análise? Digamos que não! Para enxergarmos melhor se uma modelagem foi ou não bem implementada, devemos imaginar um caso real, utilizando as classes criadas. Então vamos lá!
Imagine um movimento de ataque contra o inimigo. O atacante deverá enviar, ao mesmo tempo, dois atributos para compor um ataque. Por exemplo, suponha que exista um Inimigo Misterioso que tenha uma forte defesa contra danos do tipo FOGO e uma defesa média contra danos físicos. Se este inimigo receber um ataque comum (físico) o dano será reduzido com intensidade inferior. Caso o ataque seja de FOGO, o dano será decrementado pela defesa de forma mais rígida. Isso justifica o envio do tipo de dano quando um ataque é realizado.
Veja que POWER e TYPE são características intrícicas de um movimento de ataque. Neste caso, uma melhor modelagem poderia separar as classes Weapons de seus danos. Um dano seria composto por power e type e uma Weapon estaria associada com um dano específico.
Neste caso estamos indicando que Weapon é formada por um dano, que por sua vez é formado por power e type. Esse tipo de modelagem representa melhor o mundo que estamos tratando e torna o sistema mais coerente. Observe que existe uma agregação por composição entre as classes. Isso significa que Weapon possui um atributo privado chamado damage e que este deve ser informado durante sua construção (na prática, damage será passado como parâmetro no constructor de Weapon).
Vamos pensar agora sobre os domínios dos atributos. O poder de um dano deve ser considerado como numérico. Se optarmos por um tipo primitivo double poderemos expressar com mais precisão o valor do dano. Para o tipo podemos definir um domínio padrão onde 0 (zero) seria o tipo FÍSICO, 1 o tipo FOGO e 2 o tipo GELO. A partir do Java SE 5, os EnumType nos oferece uma maneira hard-coded para definição de domínios como este. Vamos criar um Enum chamado DamageTypeET.
package br.com.linu.paper059;
public enum DamageTypeET {
PHYSICAL,
FIRE,
ICE;
}
PHYSICAL,
FIRE,
ICE;
}
Dessa forma podemos modelar nossos objetos conforme mostrado abaixo:
Então, Weapon possui um Damage que é definido no momento da criação do objeto e acessado através do método attack(). Damage possui o atributo power do tipo double e uma associação (que na prática também é um atributo) com DamageTypeET. Existem três possíveis tipos de dano: PHYSICAL, FIRE e ICE. Através da definição dessas classes nosso sistema já é capaz de tratar do problema apresentado.
Essa é uma modelagem básica que atende o problema, porém não é flexível o suficiente. Observe por exemplo que podemos criar Damage's para tipos de Weapon's diferentes, porém ainda não existem classes distintas para determinado Damage. Isso significa que outros objetos precisariam estar responsáveis pela correta definição dos atributos do Damage. Isso não seria interessante. O ideal aqui seria criar sub-classes de Damage para especificar tipos de danos diferentes. Aproveitando a situação, lembre-se que SEMPRE a programação por interfaces é favorecida se comparada com a programação direta pelas classes. Vamos, além de criar as sub-classes, criar as interfaces para Damage e Weapon.
Além das interfaces, nós também definimos alguns comportamentos inerentes aos danos. Uma BaseDamage classificará o tipo de dano físico. O cálculo do dano (power) será definido aleatoriamente na instanciação do objeto através de calculateRate(). Já os danos mágicos herdarão a classe abstrata MagicDamage. Além do power, existe também o bonusMagic que será utilizado para obter o valor total do dano - que também sofre implicações aleatórias. Seja como for, esses detalhes de definição dos danos são apenas ilustrativos. O que importa é que agora podemos declarar as armas de quaisquer tipos apenas variando o dano.
Essa é uma boa análise, mas ainda precisamos atentar-se a alguns detalhes. Quando um dano é deferido sobre um inimigo, o objeto deve trafegar com origem na arma e destino do inimigo. Atravrés de attack() (método da arma) obtemos o dano, que em seguida será passado para o inimigo. O dano, todavia, pode sofrer alterações quando em contato com seu destino. Suponha por exemplo que o inimigo esteja armando com um escudo. Com esse escudo, o valor de power do dano deve ser decrementado.
Inclusive tipos diferentes de danos podem sofrer diferentes interferências. Suponha que esse escudo tenha uma forte defesa sobre danos FIRE, uma defesa média contra PHYSICAL e nenhuma influência sobre danos de ICE. O mais importante detalhe é que, mesmo que o dano lançado contra o inimigo sofra alterações, o dano associado à arma deve permanecer inalterado - para que outros danos iguais possam ser lançados. Isso significa que quando uma Weapon lance um attack(), deve ser instanciado um novo objeto exatamente igual ao dano mantido na arma, porém esse objeto não pode ser uma referência ao próprio dano. Ou seja, o código abaixo está errado:
public Damage attack() {
return this.damage;
}
return this.damage;
}
Esse código, implementado dentro da classe Weapon, retorna a própria referência do dano. Quando esse Damage fosse alterado pelo escudo, o próprio objeto da arma (que na verdade, é o mesmo objeto) também será alterado. Isso não reflete a necessidade do sistema e não deve ser implementado assim. Na verdade, o que precisamos aqui é criar um clone do objeto Damage.
public Damage attack() {
return this.damage.clone();
}
return this.damage.clone();
}
Esse código cria uma cópia do objeto Damage. O clone do dano pode ser alterado de qualquer forma que nossa Weapon manterá sua integridade.
Vamos então adaptar nossa modelagem sob essa perspectiva. Primeiro vamos criar um método clone() comum aos objetos Damage. Um método subtract() também será acrescentado na interface Damage para podermos reduzir o valor do dano, quando necessário. Por fim, vamos criar uma classe fictícia chamada TestShield para representar o escudo do inimigo.
Vamos então adaptar nossa modelagem sob essa perspectiva. Primeiro vamos criar um método clone() comum aos objetos Damage. Um método subtract() também será acrescentado na interface Damage para podermos reduzir o valor do dano, quando necessário. Por fim, vamos criar uma classe fictícia chamada TestShield para representar o escudo do inimigo.
Veja agora que qualquer classe que implemente Damage é obrigada a implementar clone(). Então, como vimos, o código implementado em attack() retornará um clone de Damage, seja ele qual for.
Codificando o sistema
Vamos à codificação! O código de DamageTypeET mostrado permanece igual, portanto não vamos repetí-lo aqui. Todos os códigos serão implementados no pacote br.com.linu.paper059. Fique à vontade para fazer suas adaptações. Os código foram implementados no leve e modesto editor BlueJ (bons tempos de faculdade, hehe). Você pode baixá-lo aqui http://www.bluej.org/.
Primeiro veremos o código de Damage.java
package br.com.linu.paper059;
public interface Damage {
public abstract DamageTypeET getType();
public abstract double getPower();
public abstract void subtract(double defense);
public abstract Damage clone();
}
public abstract DamageTypeET getType();
public abstract double getPower();
public abstract void subtract(double defense);
public abstract Damage clone();
}
Como esperado temos os métodos GETTER's para obter o valor de power e type, além dos métodos subtract() para reduzir o valor de power e clone(), nossa alternativa para obter cópias dos objetos. Seguindo uma básica ordem de dependência (não implementar classes que dependem de outras ainda não implementadas), vamos passar para a BaseDamage.java.
package br.com.linu.paper059;
import java.lang.Math;
public class BaseDamage implements Damage {
private double power;
private double power;
public BaseDamage() {
power = this.calculateRate();
}
public BaseDamage(double power) {
this.power = power;
}
public DamageTypeET getType() {
return DamageTypeET.PHYSICAL;
}
public double getPower() { return power; }
private double calculateRate() {
return 8.5d * ( (Math.random() * 6) + 1 );
}
public void subtract(double defense) {
power = power - defense;
if (power < 0) power = 0d;
}
public Damage clone() {
return new BaseDamage( this.power );
}
power = this.calculateRate();
}
public BaseDamage(double power) {
this.power = power;
}
public DamageTypeET getType() {
return DamageTypeET.PHYSICAL;
}
public double getPower() { return power; }
private double calculateRate() {
return 8.5d * ( (Math.random() * 6) + 1 );
}
public void subtract(double defense) {
power = power - defense;
if (power < 0) power = 0d;
}
public Damage clone() {
return new BaseDamage( this.power );
}
}
Apesar de grande, essa classe é simples. Veja que em calculateRate() o cálculo inicial de power é definido durante a instanciação. Note ainda que subtract() está preparado para manter power maior ou igual a zero e que o tipo desse dano (como não podia deixar de ser) sempre será PHYSICAL. Uma boa prática para se clonar um objeto é declarar um constructor que receba parâmetros para todos os atributos. Neste caso, como só precisamos de power, este pode ser passado pela constructor polimórfico BaseDamage(double power). O método clone() cria uma cópia do objeto justamente por esse constructor.
Mostraremos agora a classe abstrata MagicDamage.java
package br.com.linu.paper059;
public abstract class MagicDamage implements Damage {
protected double power;
protected double bonusMagic;
protected DamageTypeET type;
private double getTotalDamage() {
return power + bonusMagic;
}
public double getPower() {
return getTotalDamage();
}
public void setPower(double power) { this.power = power; }
public void setBonusMagic(double bonusMagic) { this.bonusMagic = bonusMagic; }
public void setType(DamageTypeET type) { this.type = type; }
public void subtract(double defense) {
power -= defense;
if (power < 0) power = 0d;
}
protected double bonusMagic;
protected DamageTypeET type;
private double getTotalDamage() {
return power + bonusMagic;
}
public double getPower() {
return getTotalDamage();
}
public void setPower(double power) { this.power = power; }
public void setBonusMagic(double bonusMagic) { this.bonusMagic = bonusMagic; }
public void setType(DamageTypeET type) { this.type = type; }
public void subtract(double defense) {
power -= defense;
if (power < 0) power = 0d;
}
public abstract Damage clone();
}
}
Essa classe obtém o valor total do dano em getTotalPower(). O método subtract() possui um funcionamento idêntico ao da BaseDamage. Já o método clone() foi deixado para implementação nas sub-classes. Foram definidos métodos SETTER's para facilitar a cópia do objeto.
Abaixo o código de GreatMagicDamage.java.
package br.com.linu.paper059;
public class GreatMagicDamage extends MagicDamage {
public GreatMagicDamage() {
power = this.calculateRate();
bonusMagic = 7.5d;
type = DamageTypeET.FIRE;
}
public DamageTypeET getType() {
return type;
}
private double calculateRate() {
return 3d * ( (Math.random() * 6) + 1 );
}
public Damage clone() {
MagicDamage magic = new GreatMagicDamage();
magic.setPower( power );
magic.setBonusMagic( bonusMagic );
magic.setType( type );
return magic;
}
}
public GreatMagicDamage() {
power = this.calculateRate();
bonusMagic = 7.5d;
type = DamageTypeET.FIRE;
}
public DamageTypeET getType() {
return type;
}
private double calculateRate() {
return 3d * ( (Math.random() * 6) + 1 );
}
public Damage clone() {
MagicDamage magic = new GreatMagicDamage();
magic.setPower( power );
magic.setBonusMagic( bonusMagic );
magic.setType( type );
return magic;
}
}
Aqui o mecanismo de clonagem foi diferente. Quando GreatMagicDamage() é instanciado, valores padrões são definidos. Esse valores são sobrepostos pelos informados nos métodos de definição. A classe MagicDamageParalyzing é similar à anterior.
package br.com.linu.paper059;
public class MagicDamageParalyzing extends MagicDamage {
public MagicDamageParalyzing() {
power = this.calculateRate();
bonusMagic = 0d;
type = DamageTypeET.ICE;
}
public DamageTypeET getType() {
return type;
}
private double calculateRate() {
return 2.5d * ( (Math.random() * 6) + 1 );
}
public Damage clone() {
MagicDamage magic = new MagicDamageParalyzing();
magic.setPower( power );
magic.setBonusMagic( bonusMagic );
magic.setType( type );
return magic;
}
}
public MagicDamageParalyzing() {
power = this.calculateRate();
bonusMagic = 0d;
type = DamageTypeET.ICE;
}
public DamageTypeET getType() {
return type;
}
private double calculateRate() {
return 2.5d * ( (Math.random() * 6) + 1 );
}
public Damage clone() {
MagicDamage magic = new MagicDamageParalyzing();
magic.setPower( power );
magic.setBonusMagic( bonusMagic );
magic.setType( type );
return magic;
}
}
A diferença aqui é somente nos valores dos atributos. Agora temos já temos a família de danos definida. A classe TestShield foi elaborada de forma purista apenas para simular a defesa aplicada aos ataques (o ministério da OO adverte: usar "tripões" de códigos como esse faz mal à saúde). Existe apenas um método estático chamado defense() que reduz o valor do dano de acordo com seu tipo.
package br.com.linu.paper059;
public class TestField {
public static Damage defense(Damage damage) {
if (damage.getType() == DamageTypeET.PHYSICAL)
damage.subtract( 8d );
if (damage.getType() == DamageTypeET.FIRE)
damage.subtract( 11.5d );
/*
* Danos do tipo ICE não sofrem redução
*/
return damage;
}
if (damage.getType() == DamageTypeET.PHYSICAL)
damage.subtract( 8d );
if (damage.getType() == DamageTypeET.FIRE)
damage.subtract( 11.5d );
/*
* Danos do tipo ICE não sofrem redução
*/
return damage;
}
}
Como havíamos planejado, esse escudo terá maior influência sobre danos de fogo, e nenhum influência sobre danos de gelo. Para facilitar seu uso, retornamos a mesma instância do Damage após as alterações. A interface Weapon é muito simples. Veja seu código abaixo:
package br.com.linu.paper059;
public interface Weapon {
public abstract Damage attack();
}
public abstract Damage attack();
}
Ela apenas define o método attack(). A implementação dessa classe apenas recebe um Damage no constructor e programa o attack().
package br.com.linu.paper059;
public class WeaponImpl implements Weapon {
private Damage damage;
public WeaponImpl(Damage damage) {
this.damage = damage;
}
public Damage attack() {
return this.damage.clone();
}
public WeaponImpl(Damage damage) {
this.damage = damage;
}
public Damage attack() {
return this.damage.clone();
}
}
O grande detalhe aqui centra-se na chamada do clone() quando um attack() é realizado. Isso, conforme conversamos, faz a cópia do objeto de dano mantendo o atributo damage íntegro. Vamos agora criar uma classe de teste.
package br.com.linu.paper059;
public class Main {
public static void showDamage(Damage damage) {
String hit = "[" + String.format( "%-8s" , damage.getType() ) + "] " +
String.format( "%4.2f" , damage.getPower() );
System.out.println( hit );
}
String hit = "[" + String.format( "%-8s" , damage.getType() ) + "] " +
String.format( "%4.2f" , damage.getPower() );
System.out.println( hit );
}
public static void main(String args[]) {
Weapon sword, staff, ring;
{
Damage d1 = new BaseDamage();
sword = new WeaponImpl( d1 );
Damage d2 = new GreatMagicDamage();
staff = new WeaponImpl( d2 );
Damage d3 = new MagicDamageParalyzing();
ring = new WeaponImpl( d3 );
System.out.println( "\nDamage of weapons:" );
showDamage( d1 );
showDamage( d2 );
showDamage( d3 );
}
System.out.println( "\nAttacks defended by the shield:" );
showDamage( TestField.defense( sword.attack() ) );
showDamage( TestField.defense( staff.attack() ) );
showDamage( TestField.defense( ring.attack() ) );
System.out.println( "\nPower attacks:" );
showDamage( sword.attack() );
showDamage( staff.attack() );
showDamage( ring.attack() );
}
}
{
Damage d1 = new BaseDamage();
sword = new WeaponImpl( d1 );
Damage d2 = new GreatMagicDamage();
staff = new WeaponImpl( d2 );
Damage d3 = new MagicDamageParalyzing();
ring = new WeaponImpl( d3 );
System.out.println( "\nDamage of weapons:" );
showDamage( d1 );
showDamage( d2 );
showDamage( d3 );
}
System.out.println( "\nAttacks defended by the shield:" );
showDamage( TestField.defense( sword.attack() ) );
showDamage( TestField.defense( staff.attack() ) );
showDamage( TestField.defense( ring.attack() ) );
System.out.println( "\nPower attacks:" );
showDamage( sword.attack() );
showDamage( staff.attack() );
showDamage( ring.attack() );
}
}
Essa classe define em hard-coded as ações naturais que ocorreriam no jogo durante uma série de ataques. Primeiro são definidos os objetos para os três tipos de armas: sword, staff e ring. Em seguida é criado um escopo interno para a instanciação desses objetos. Veja que cada arma recebe seu dano respectivo. Antes do fechamento do escopo os atributos dos danos são exibidos com auxílio do método estático showDamage(). Agora as armas efetuam ataques contra o escudo. Por fim novos ataques são gerados para comprovarmos que os danos originais foram mantidos.
A implementação dessas classes nos BlueJ gerou o seguite diagrama (com as dependências inibidas):
Ao executarmos a class Main teremos o resultado abaixo - observe que como os valores são gerados aleatoriamente, outra execução resultaria em dados diferentes.
Damage of weapons:
[PHYSICAL] 28,25
[FIRE ] 27,65
[ICE ] 6,44
[PHYSICAL] 28,25
[FIRE ] 27,65
[ICE ] 6,44
Attacks defended by the shield:
[PHYSICAL] 20,25
[FIRE ] 16,15
[ICE ] 6,44
[PHYSICAL] 20,25
[FIRE ] 16,15
[ICE ] 6,44
Power attacks:
[PHYSICAL] 28,25
[FIRE ] 27,65
[ICE ] 6,44
[PHYSICAL] 28,25
[FIRE ] 27,65
[ICE ] 6,44
Veja que os primeiros valores mostrados foram, como esperado, mantidos na última exibição. Esse é o resultado esperado independentemente das alterações realizadas pelo escudo na segunda exibição. Dessa forma vários ataques podem ser deferidos e alterados sem que a referência principal dos danos seja modificada.
Essa foi nossa solução para os requisitos apresentados. A grande flexibilidade desse projeto centra-se no método clone(). Veremos mais detalhes sobre o padrão de projeto que sugere exatamente a modelagem que aplicamos neste exemplo.
Design Pattern Prototype
Utilizamos uma característica interessante no projeto acima: uma séria de objetos é criada através da cópia de um protótipo pré-definido. Isso é exatamente o que sugere o padrão de projeto Prototype.
Para um melhor entendimento, imagine que um sistema é composto por vários sub-sistemas. Quando um desses sub-sistemas deseja manter encapsulado a dificultade inerente à criação de vários objetos semelhantes para que seus sub-sistemas clientes não se preocupem com isso, podemos utilizar o padrão de protótipos. O sub-sistema fornecedor oferece uma interface pela qual os clientes possam copiar esses objetos. Neste caso, Client mantém um(s) protótipo que é copiado(s) sempre que operações relacionadas a ele precisam ser feitas.
Um detalhe interessante é que a cópia do protótipo não afeta o próprio protótipo e muitas delas podem ser espalhadas pelos sub-sistemas clientes. Um alternativa ao uso dessa padrão seria oferecer uma família de classes semelhantes, porém com minúcias distintas. O padrão prototype também poderia ser utilizado em conjunto com uma Abstract Factory (outro padrão de projeto) para facilitar a criação correta dos protótipos - a criação dos clones seria a mesma, neste caso.
Conclusão
Vimos neste material uma série de requisitos que podem ser implementados através do pattern prototype. Esse foi um exemplo de implementação de armas e danos fictício, mas que pode muito bem ser considerado em projetos reais. Não tenho, entretanto, referência concreta de que isso foi utilizado em algum jogo real - infelizmente não tenho acesso aos fontes dos games que costumo jogar. Mas de qualquer forma ele serviu para explicar detalhadamente o funcionamento do design pattern Prototype, o que é, na verdade, a principal intenção deste artigo.
"Não acordo entre Homens e Leões." Aquiles
Obrigado pela leitura.
Att, Guilherme Pontes

Linux
Curso Java
Papers Java
Database
Hardware
Informática
Gestão de TI
Diversão
Download
Potions
Homepage
Blog
Facebook
Youtube
Twitter
Linubr.org
Nenhum comentário:
Postar um comentário