Suponha agora que pretende representar jogos cujo critério de terminação não é necessariamente o mesmo que definimos na classe Jogo
. Por exemplo, queremos poder representar jogos que terminam de uma das seguintes formas:
Tal como podemos ter critérios de terminação diferentes também podemos ter, associado a cada um deles, uma forma específica de determinar o(s) vencedor(es).
Como podemos alcançar isto?
Uma abordagem que "funciona" mas que é fraca do ponto de vista da qualidade do software, é associar a cada objeto do tipo Jogo
mais um atributo que define o tipo de jogo, no que diz respeito a terminação e cálculo de vencedor.
Podemos usar um enumerado, por exemplo, para definir os vários tipos de jogo que queremos representar:
public enum Terminacao {
TODOS_ALCANCAM, EXATAMENTE, JOGADAS_FEITAS, OBJETIVO_OU_JOGADAS;
}
Usamos agora o enumerado Terminacao
e modificamos a classe Jogo
onde necessário:
xpublic class Jogo {
...
// atributos que definem este jogo:
// A coleccao dos jogadores
ArrayList<Jogador> jogadores;
// Pontuacao objetivo
private int objetivo;
// Tipo de terminacao
private Terminacao termina;
// Numero maximo de jogadas
private int maximoJogadas;
// Numero de jogadas jah feitas
private int jogadas;
/**
* Construir um jogo.
* @param objetivo A pontuacao com que se ganha este jogo
* @param tipoTerm O tipo de terminacao deste jogo
* @param maxJogadas O numero maximo de jogadas deste jogo (se se aplicar)
* @requires tipoTerm != null
*/
public Jogo (int objetivo, Terminacao tipoTerm, int maxJogadas) {
this.objetivo = objetivo;
this.jogadores = new ArrayList<Jogador>();
this.termina = tipoTerm;
this.maximoJogadas = maxJogadas;
this.jogadas = 0;
}
...
/**
* O numero de jogadores que estao em jogo.
*/
public int quantosEmJogo () {
return this.jogadores.size();
}
/**
* Este jogo jah terminou?
*/
public boolean terminou () {
boolean resultado = false;
switch (this.termina){
case TODOS_ALCANCAM: resultado = this.quantosAlcancaram() == this.quantosEmJogo();
break;
case EXATAMENTE: resultado = this.algumExatamente();
break;
case JOGADAS_FEITAS: resultado = this.jogadas == this.maximoJogadas;
break;
case OBJETIVO_OU_JOGADAS: resultado = this.quantosAlcancaram() >= 1 ||
this.jogadas >= this.maximoJogadas;
break;
}
return resultado;
}
/**
* Quantos jogadores jah alcancaram o objetivo deste jogo?
*/
private int quantosAlcancaram() {
int quantos = 0;
for(Jogador jog : this.jogadores) {
if(jog.pontuacao() >= this.objetivo) {
quantos++;
}
}
return quantos;
}
/**
* Algum jogador tem exatamente a pontuacao objetivo deste jogo?
*/
private boolean algumExatamente() {
for(Jogador jog : this.jogadores) {
if(jog.pontuacao() == this.objetivo) {
return true;
}
}
return false;
}
...
}
Tivemos que adicionar três novos atributos: o tipo de terminação, o número máximo de jogadas e o número de jogadas já feitas. Estes dois últimos são necessários para os casos em que o critério de terminação depende (também) do número de jogadas já feitas.
No construtor tivemos que adicionar dois novos parâmetros para obter os valores para o tipo de terminação do novo jogo e o número máximo de jogadas (quando se aplica). Repare que na pré-condição não impomos qualquer restrição nem ao valor do objetivo nem ao número máximo de jogadas, pois em alguns casos os seus valores não são relevantes e podem ser quaisquer.
Finalmente, alterámos o método terminou
para acondicionar os vários tipos de critérios de terminação. Criámos também dois métodos auxiliar private
– quantosAlcancaram
e algumExatamente
– que calculam o número de jogadores em jogo que alcançaram o objetivo e se algum jogador tem o valor da pontuação igual ao objetivo, respetivamente, pois esses valores são necessários para decidir a terminação.
Esta solução está extremamente dependente das formas de terminação existentes. Durante a "vida" do nosso software, sempre que quisermos acrescentar uma nova forma de terminação temos que alterar o código da classe Jogo
e, eventualmente, o código de classes cliente desta (por exemplo, quando temos que adicionar novos parâmetros no construtor).
Isso é negativo em vários aspetos:
Jogo
, correspondentes a cada nova alteração, é um foco de instabilidade que devemos evitar.Além de tudo isto, temos também parâmetros do construtor e atributos de instância que só são relevantes para alguns tipos de terminação, o que não faz muito sentido.
Nos capítulos anteriores, as classes Jogador
e Jogo
foram sendo desenvolvidas com o intuito de ilustrar uma série de conceitos novos em Programação Orientada a Objetos.
Novos pormenores acerca desses conceitos foram sendo introduzidos a pouco e pouco, e só agora temos uma visão mais geral e completa do que pretendemos representar.
Porque é preciso saber o que representar antes de decidir como representar, vamos agora repensar a representação dos objetos e dos seus comportamentos, de uma forma mais estruturada.
Jogo
Com o que já sabemos acerca dos jogos que queremos representar, podemos concluir que o seguinte se aplica a qualquer um:
É a partir desta informação que vamos agora construir a classe Jogo
; de seguida vamos aprender a representar as variantes de que falámos anteriormente (e outras que ainda não estamos a prever) como especializações diferentes de um mesmo conceito – o Jogo
.
Comecemos por criar a nossa classe Jogo
, com tudo o que é comum a todos os jogos que queremos representar.
xxxxxxxxxx
import java.util.ArrayList;
import java.util.List;
/**
* Classe cujos objetos representam jogos
* @author Isabel Nunes
*/
public class Jogo implements Ganhavel {
// atributos que definem este jogo:
// Designacao
private String designacao;
// A coleccao dos jogadores
private ArrayList<Jogador> jogadores;
/**
* Inicializar um jogo.
* @param designacao A designacao do jogo.
* @requires designacao != null
*/
public Jogo (String designacao) {
this.designacao = designacao;
this.jogadores = new ArrayList<Jogador>();
}
/**
* A designacao deste jogo.
*/
public String designacao () {
return this.designacao;
}
/**
* O numero de jogadores que estao em jogo.
*/
public int quantosEmJogo () {
return this.jogadores.size();
}
/**
* Juntar um jogador com um dado nome a este jogo.
* @param nomeJog O nome do jogador.
* @requires !this.estahEmJogo(nomeJog)
*/
public void juntarJogador (String nomeJog) {
this.jogadores.add(new Jogador (nomeJog));
}
/**
* Retorna o jogador correspondente a um nome, ou null se nao houver.
* Metodo privado
* @param nomeJog O nome do jogador.
* @returns O jogador com este nome se existir no jogo;
* null caso contrario
*/
Jogador obterJogador (String nomeJog) {
for (Jogador jog : this.jogadores){
if (nomeJog.equals(jog.nome())){
return jog;
}
}
return null;
}
/**
* Registar a pontuacao, na jogada atual, de um dado jogador.
* @param nomeJog O nome do jogador.
* @param pontuacao A pontuacao obtida na jogada atual.
* @requires this.estahEmJogo (nomeJog)
*/
public void registarPontosJogada (String nomeJog, int pontuacao) {
this.obterJogador(nomeJog).registarPontos(pontuacao);
}
/**
* Um dado jogador estah em jogo?
* @param nomeJog O nome do jogador.
* @return true se o jogador com este nome estah inscrito neste jogo
* false caso contrario
*/
public boolean estahEmJogo (String nomeJog) {
return this.obterJogador(nomeJog) != null;
}
/**
* A pontuacao de um dado jogador deste jogo
* @param nomeJog O nome do jogador.
* @return A pontuacao do jogador com este nome
* @requires this.estahEmJogo (nomeJog)
*/
public int pontuacao (String nomeJog) {
return this.obterJogador(nomeJog).pontuacao();
}
/**
* A pontuacao maxima numa soh jogada de um dado jogador deste jogo
* @param nomeJog O nome do jogador.
* @return A pontuacao maxima numa soh jogada do jogador com este nome
* @requires this.estahEmJogo (nomeJog)
*/
public int maximoNumaJogada (String nomeJog) {
return this.obterJogador(nomeJog).maximoNumaJogada();
}
/**
* Este jogo jah terminou?
*/
public boolean terminou () {
??????
}
/**
* Vencedor(es) deste jogo
* @return O nome do jogador que teve melhor pontuacao. No caso de
* haver varios, aquele que teve a pontuacao mais alta numa
* soh jogada. No caso de haver varios, retorna os seus nomes
* @requires this.quantosEmJogo() > 0 && this.terminou()
*/
public List<String> vencedores () {
// Os jogadores que tiveram a melhor pontuacao ex-aequo
List<Jogador> melhores = this.osMelhores();
// calcular o maior dos maximos numa soh jogada
// o(s) jogador(es) que o obteve(iram) serah(ao) o(s) vencedor(es)
int maximo = melhores.get(0).maximoNumaJogada();
for (Jogador jog : melhores) {
int pontos = jog.maximoNumaJogada();
if (pontos > maximo) {
maximo = pontos;
}
}
List<String> vencedores = new ArrayList<String>();
for (Jogador jog : melhores) {
if (jog.maximoNumaJogada() == maximo) {
vencedores.add(jog.nome());
}
}
return vencedores;
}
/**
* Os jogadores que teem a maior pontuacao
* @return Os jogadores que teem a maior pontuacao
* @requires this.quantosEmJogo() > 0
*/
private List<Jogador> osMelhores () {
// Qual a melhor pontuacao?
int maiorPontuacao = this.maiorPontuacao();
ArrayList<Jogador> melhoresJogadores = new ArrayList<Jogador>();
for (Jogador jog : this.jogadores) {
if (jog.pontuacao() == maiorPontuacao) {
melhoresJogadores.add(jog);
}
}
return melhoresJogadores;
}
/**
* A maior pontuacao dos jogadores deste jogo
* @return O valor da maior pontuacao
* @requires this.quantosEmJogo() > 0
*/
public int maiorPontuacao () {
int maiorPontuacao = 0;
for (Jogador jog : this.jogadores) {
int pontos = jog.pontuacao();
if (pontos > maiorPontuacao) {
maiorPontuacao = pontos;
}
}
return maiorPontuacao;
}
/**
* Os nomes dos jogadores com pontuacao superior a um dado valor
* @param limiteInf O valor de comparacao.
* @requires this.quantosEmJogo() > 0
* @return Uma lista com os nomes dos jogadores deste jogo com
* pontuacao superior a limiteInf
*/
public List<String> pontuacaoSuperiorA (int limiteInf) {
List<String> melhores = new ArrayList<String>();
for (Jogador jog : this.jogadores){
if (jog.pontuacao() > limiteInf){
melhores.add(jog.nome());
}
}
return melhores;
}
/**
* Representacao textual deste jogo.
*/
public String toString () {
StringBuilder sb = new StringBuilder();
sb.append("Designacao: " + this.designacao() + "\n");
sb.append("Numero de jogadores: " + this.quantosEmJogo() + "\n");
for (Jogador jog : this.jogadores){
sb.append(jog.toString() + "\n");
}
return sb.toString();
}
}
Tenha em atenção os seguintes pontos:
Decidimos que não existe limite no número máximo de jogadores permitido em cada jogo; usamos uma lista, em vez de um array, para representarmos a coleção de jogadores em jogo;
Como nem todos os jogos em que pensámos envolvem uma pontuação objetivo, não temos nenhum atributo correspondente;
Os métodos que devolvem coleções de objetos têm List
como tipo de retorno;
A classe Jogador
que aqui é usada é a do capítulo 10, a que não envolve variados efeitos de jogadas;
Todos os métodos que envolvem jogadores, pontuações e jogadas, estão implementados;
O método vencedores
também está implementado: considerámos, como anteriormente, que vence o jogador que tem a maior pontuação geral e, em caso de haver mais do que um, os que também têm o maior valor de máximo numa só jogada;
Finalmente:
Pergunta: como implementar o método terminou
?
Resposta: depende! Já vimos que queremos poder variar o critério de terminação, entre os vários que elencámos na secção anterior, ou até ainda outro que não estamos a prever para já. Por isso não implementámos o método!
xxxxxxxxxx
/**
* Este jogo jah terminou?
*/
public boolean terminou () {
??????
}
Mas o compilador dará erro se deixarmos isto assim...
Antes de resolvermos este problema da definição do método terminou
na classe Jogo
, vamos ver como implementar os vários tipos de jogos que pretendemos – através da Herança – de forma a obter software que é extensível.
Anterior: 13.5. O interface genérico List
Seguinte: 14.1. Uma subclasse de Jogo