Lab2

Objetivo

Neste laboratório iremos desenvolver mais a aplicação do último laboratório, treinando os conceitos de OO.

Aviso

Algumas pessoas me perguntaram sobre como ler o input do teclado na linha de comando. Recomendo utilizar a classe java.io.Console, descrita aqui e aqui. Basta instanciar e usar o método printf (igual ao printf de C) para imprimir, e o método readLine (imprime um pedido e lê todo conteúdo do da entrada até que seja dado um enter).

Desenvolvimento

1 - Criação da Classe Simulador

Até agora a nossa aplicação não tem muita utilidade para um usuário que não tenha conhecimentos de programação Java e orientação a objetos. Isto vai mudar.

Vamos criar uma classe Simulador, com um método main, que irá receber input do usuário, via linha de comando, e coordenar a interação entre os objetos. A classe simulador deverá possibilitar a criação de quantos alunos quanto o usuário desejar, perguntando, o nome e tipo do aluno ("esforçado", "safo preguiçoso"). Em seguida, deverá mandar os alunos fazerem e entregarem o relatório. Por último, irá pedir que o professor corrija os relatórios.

2 - Alteração nos Alunos

Atualmente, para criar um tipo diferente de aluno, devemos criar uma subclasse concreta da classe abstrata Aluno. Estas subclasses devem sempre implementar os métodos abstratos getInteligencia() e getDedicacao(). Este tipo de solução, embora funcional, tem algumas desvantagens:

  • Nada impede que um programador crie subclasses concretas para tipos de aluno que, além dos dois métodos acima, criem outros que não foram projetados de antemão na nossa aplicação e que não gostaríamos que existissem (ex.: poderia criar o método dormirEmAula() para o aluno safo).
  • A dependência entre a classe pai Aluno e suas subclasses não fica muito bem definida. Sabemos que as subclasses devem definir os dois métodos acima, mas não fica muito claro quais informações da superclasse são utilizadas pelas subclasses. As subclasses poderiam se utilizar de variáveis protegidas definidas na superclasse e este tipo de dependência é mais difícil de se visualizar, podendo causar problemas a medida que a sua aplicação cresce e o código da mesma se torna mais complexo.
  • Suponha que queiramos criar subclasses de Aluno que determinem em qual universidade o aluno estuda. Por exemplo, AlunoITA e AlunoUSP. Podemos ver que, para adaptar nossa solução de utilizar subclasses para determinar o comportamento do aluno a este cenário, deveríamos criar uma subclasse com um dado comportamento tanto para o AlunoITA quanto para o AlunoUSP. Rapidamente teríamos um grande número de subclasses. Além disto, a duplicação de código seria inevitável.
  • E se quisessemos possibilitar a um safo preguiçoso, depois de uma revisão de consciência, virar um aluno safo esforçado (também conhecido como AlunoSuma)? Com o design adotado na nossa aplicação isto seria impossível, pois uma vez criado um objeto, sua classe não pode ser alterada. Ok, o aluno poderia ser "morrer e nascer novamente" (poderíamos criar outro objeto), mas não gostaríamos de ser tão radicais.

figura 1 - explosão de subclasses

Pelos motivos acima, seria interessante pensar em outra forma de definir o comportamento dos alunos (ou seja, a inteligência e dedicação), sem o uso de subclasses diretas de Aluno.

Nossa solução será a seguinte: Vamos criar outra classe abstrata, chamada de Comportamento. Esta classe só terá os métodos abstratos getInteligencia() e getDedicação(). As subclasses definirão possíveis comportamentos dos alunos (Safo, Esforçado, SafoPreguicoso, etc).

A classe Aluno, por sua vez, deixará de ser abstrata. Ela terá uma variável interna comportamento, que irá determinar qual o comportamento daquele aluno. Aluno irá delegar os métodos getInteligencia() e getDedicacao() para o seu objeto comportamento. Aluno também passará a ter um método público setComportamento(Comportamento comportamento) que irá definir qual é o comportamento daquele aluno.

Preste atenção que um Comportamento deve possuir uma variável interna apontando para o aluno (setada no seu construtor). Assim definimos, explicitamente, que o comportamento pode depender de outras características do aluno. Por exemplo, sua inteligência pode depender do seu conhecimento. Ou sua dedicação pode depender do seu nome (esta é para os numerólogos de plantão).

Como o Aluno, no momento da criação, já deve ter um comportamento definido, faça com que o construtor de Aluno defina como comportamento inicial o comportamento Esforcado.

Os demais métodos de aluno (getNome(), etc) permanecem inalterados. Um diagrama de classe descrevendo estas alterações se encontra abaixo.

figura 2 - comportamento dos alunos

Vamos aproveitar que nossa aplicação ficou mais robusta, e definir outros comportamentos, além de SafoPreguicoso e Esforcado (que devem ser iguais aos do último lab):

  • Summa : tem a inteligência e dedicação sempre 1.
  • Pemba: tem a inteligência e dedicação como aleatórios variando de 0 a 0.5.
  • Imprevisivel: tem a dedicacao e inteligencia como aleatórios variando de 0 a 1.
  • Burro: tem a dedicacao e inteligencia sempre 0.

Pergunta (deve ser respondida no relatório): de que outra forma você solucionaria os problemas expostos acima?

3 - Alunos de instituições diferentes

Para complicar um pouquinho mais, vamos criar 3 subclasses de alunos, de acordo com a sua faculdade de origem: AlunoITA, AlunoUSP e AlunoUnicamp. Além disto vamos ser preconceituosos e definir que os alunos do ITA nunca podem ser burros, e os alunos da USP e Unicamp nunca serão nem Summas nem SafosPreguicosos. Vamos fazer isto, usando tratamento de exceções em Java.

Vamos explicar o essencial sobre tratamento de exceções: exceções são usadas para o tratamento de condições excepcionais e erros (como por exemplo, tentar atribuir o comportamento Summa a um AlunoUSP). As exceções são lançadas usando-se o keyword throw. Quando uma exceção é lançada, a execução do método atual é interrompida e a exceção vai sendo 'propagada' para o método que se encontra abaixo na pilha de execução (o método de onde foi chamado o método que lançou a exceção). Pode-se então 'tratar' a exceção (utilizando-se a construção try-catch), por exemplo exibindo uma mensagem de erro ao usuário, e continuar com a execução do código normalmente. Se a exceção não for tratada a execução deste método também é interrompida e a exceção vai se propagando até chegar no método 'inicial' da aplicação (o método main, em geral), que é então interrompida e exibe uma mensagem de erro "Unhandled Exception". Exceções são objetos e podem ser de vários tipos, mas aqui podemos simplesmente usar o tipo "Exception", mais geral.

Lançando uma exceção:

throw new Exception();

Tratando uma exceção:

try {
  objeto.metodoComExcecao();
} catch (Exception e) {
  //faz algo aqui, quando for lançada uma exceção
}

Queremos então lançar uma exceção do método setComportamento(). Para isto, devemos sobrescrever este método nas subclasses de Aluno (AlunoITA, AlunoUSP, etc), lançando a exceção nos casos de erro (atribuição de um comportamento incompatível com o tipo de aluno). Como o método setComportamento() vai lançar uma exceção, devemos declarar isto em todas as suas declarações (tanto na superclasse Aluno quanto nas subclasses):

public class Aluno {
  //...
  public void setComportamento(Comportamento comportamento) throws Exception {
    //...
  }
  //...
}

Nas subclasses devemos verificar os tipos de comportamento incompatíveis e lançar uma exceção.

public class AlunoITA extends Aluno {
  //...
  public void setComportamento(Comportamento comportamento) throws Exception {
    if(comportamento instanceof Burro) throw new Exception(); //iteanos não são burros!
    super.setComportamento(comportamento); //faz o que a superclasse Aluno faz.
  }
  //...
}

Agora, de onde quer que chamemos setComportamento(), devemos usar um try-catch para garantir que a exceção seja tratada caso seja lançada:

// código que define o comportamento - provavelmente no método main.
// neste caso a exceção vai ser lançada.
try {
  aluno.setComportamento( new Burro(aluno) );;
} catch (Exception e)
  System.out.println( "Você tentou fazer algo impossível!")
}

[ http://ces22.wikidot.com/local--files/lab2/lab2.png figura 3 - diagrama completo]

4 - Alterar Simulador

Feitas estas alterações, altere a classe Simulador para suportar estas mudanças. Agora permita que o usuário, além de entrar o comportamento do aluno (Safo, Burro, etc), possa dizer de qual instituição ele é (ITA, USP, Unicamp).

5 - Crie uma Queue com genéricos

Utilizando genéricos, conforme falado na aula 6, pode-se criar uma Queue cujo conteúdo interno não é restrito a objetos do tipo Relatório. Modifique a classe RelaQueue e crie uma classe Queue<T>. A classe RelaQueue deve ser uma subclasse de Queue<Relatorio>, com o corpo do código em branco. A finalidade de criar RelaQueue como subclasse de Queue<Relatorio> é manter a compatibilidade com o código atual, que se utiliza da classe RelaQueue.

Novo Código de RelaQueue.

public class RelaQueue extends Queue<Relatorio> {
}

O código de Queue<T> é por sua conta!

6 - Rode as Classes de Teste

Crie uma classe de testes e adicione os quatro métodos de teste abaixo. Todos devem passar.

public void testFuncionaComoOLab1() {
  //criando os objetos
  RelaQueue queue = new RelaQueue();
  Professor prof = new Professor(queue);
 
  // O que muda em termos de interface externa neste lab,
  // é a criação dos alunos. O que mostra que o nosso código
  // está bem desacoplado e a nossa refatoração não alterou
  // o funcionamento dos outros objetos (Professor, Relatório, etc).
    Aluno aluno1 = new Aluno("John Smith", queue);
    Aluno aluno2 = new Aluno("Mark Smith", queue);
    Aluno aluno3 = new Aluno("Joseph Smith", queue);
    Aluno aluno4 = new Aluno("Robert Smith", queue);
 try {
    aluno3.setComportamento( new SafoPreguicoso(aluno3) );
    aluno4.setComportamento( new SafoPreguicoso(aluno4) );
  } catch (Exception e) {
    fail("Não deveria lançar erro!");
  }
 
  //teste de alunoesforcado
  assertEquals(0.5, aluno1.getInteligencia(), 0.01);
  assertEquals(1, aluno1.getDedicacao(), 0.01);
 
  //teste de alunosafo
  assertEquals(1.0, aluno3.getInteligencia(), 0.01);
  assertTrue(aluno3.getDedicacao() != aluno3.getDedicacao()); //deve ser randomica
  assertTrue(aluno3.getDedicacao() < 0.5);
 
  //alunos comecam a fazer os relas
  aluno1.fazEEntregaRelatorio();
 
 //roubamos o relatório do primeiro aluno para investigá-lo
  Relatorio rela1 = queue.dequeue();
  assertEquals(aluno1, rela1.getAluno());
  assertTrue(rela1.getAluno().getNome().equals("John Smith"));
  assertTrue(rela1.getQualidade() < 0.9);
  assertTrue(rela1.getQualidade() > 0.8);
  assertTrue(rela1.getOriginalidade() < 0.7);
  assertTrue(rela1.getOriginalidade() > 0.6);
 
  //os outros continuam a fazer os relas
  aluno2.fazEEntregaRelatorio();
  aluno3.fazEEntregaRelatorio();
  aluno4.fazEEntregaRelatorio();
 
  //deve exibir a coreção dos relas dos aluno2, aluno3, e aluno4, nesta ordem
  //pois "roubamos" o relatório do aluno1
  prof.corrigirRelatorios();
}
 
public void testOsComportamentosFuncionamComoEspecificado() {
  RelaQueue queue = new RelaQueue();
  Aluno aluno1 = new Aluno("John", queue);
  Comportamento summa = new Summa(aluno1);
  Comportamento pemba = new Pemba(aluno1);
  Comportamento imprevisivel = new Imprevisivel(aluno1);
  Comportamento safo = new SafoPreguicoso(aluno1);
  Comportamento esforcado = new Esforcado(aluno1);
  Comportamento burro = new Burro(aluno1);
 
 //teste de esforcado
  assertEquals(0.5, esforcado.getInteligencia(), 0.01);
  assertEquals(1, esforcado.getDedicacao(), 0.01);
 
//teste de burro
  assertEquals(0.0, burro.getInteligencia(), 0.01);
  assertEquals(0.0, burro.getDedicacao(), 0.01);
 
//teste de summa
  assertEquals(1.0, summa.getInteligencia(), 0.01);
  assertEquals(1.0, summa.getDedicacao(), 0.01);
 
  //teste de safo
  assertEquals(1.0, safo.getInteligencia(), 0.01);
  assertTrue(safo.getDedicacao() != safo.getDedicacao()); //deve ser randomica
  assertTrue(safo.getDedicacao() < 0.5);
 
  //teste de imprevisivel
  assertTrue(imprevisivel.getInteligencia() != imprevisivel.getInteligencia()); //deve ser randomica
  assertTrue(imprevisivel.getInteligencia() < 1.0);
  assertTrue(imprevisivel.getDedicacao() != imprevisivel.getDedicacao()); //deve ser randomica
  assertTrue(imprevisivel.getDedicacao() < 1.0);
 
  //teste de pemba
  assertTrue(pemba.getInteligencia() != pemba.getInteligencia()); //deve ser randomica
  assertTrue(pemba.getInteligencia() < 0.5);
  assertTrue(pemba.getDedicacao() != pemba.getDedicacao()); //deve ser randomica
  assertTrue(pemba.getDedicacao() < 0.5);
 
}
 
public void testAsSubClassesDeAlunoTemRestricoes() {
    RelaQueue queue = new RelaQueue();
    AlunoITA alunoITA = new AlunoITA("John Smith", queue);
    AlunoUSP alunoUSP = new AlunoUSP("Mark Smith", queue);
    AlunoUnicamp alunoUnicamp = new AlunoUnicamp("Joseph Smith", queue);
 try {
    alunoITA.setComportamento( new SafoPreguicoso(alunoITA) );
    alunoITA.setComportamento( new Summa(alunoITA) );
    alunoUSP.setComportamento( new Burro(alunoUSP) );
    alunoUnicamp.setComportamento( new Burro(alunoUnicamp) );
  } catch (Exception e) {
    fail("Não deveria lançar erro!");
  }
 try {
    alunoITA.setComportamento( new Burro(alunoITA) );
    fail("Deveria lançar erro!");
  } catch (Exception e) {
  }
 try {
    alunoUSP.setComportamento( new SafoPreguicoso(alunoUSP) );
    fail("Deveria lançar erro!");
  } catch (Exception e) {
  }
 try {
    alunoUnicamp.setComportamento( new Summa(alunoUnicamp) );
    fail("Deveria lançar erro!");
  } catch (Exception e) {
  }
}
 
public void testRelaQueueEhSubclasseDoGenericoQueue() {
  Queue<Relatorio> q = new RelaQueue(); //propriedade básica do polimorfismo
}

Relatório

Faça o relatório seguindo as mesmas instruções do lab1. Anexe o código funcionando no passo 1 (com a classe Simulador sem as alterações) e depois no passo 6 (com todas as alterações).

Data limite (desta vez sem melação!): 02/09/2008

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License