terça-feira, 7 de agosto de 2012

Aula 6 - Resolvendo o Exercício da Garrafa e do Copo

Olá, diante do que discutimos na última aula, deixei a cargo de vocês resolverem um estudo de caso da garrafa e do copo. Para quem ainda não leu, acesse a aula 5 em http://linubr.blogspot.com.br/2012/08/aula-5-lapidando-o-aprendizado-da.html.

Como principal objetivo dessa aula faremos a transposição da modelagem abordada para o código Java, como também o entendimento da execução do programa na Stack e na Heap. Durante a aula presencial, nós (no caso, meus amigos da empresa e eu), fizemos uma conexão direta entre os diagrama de classe e sequência com o código resultante do exercício. Creio ter sido proveitosa essa abordagem, portanto vou repetí-la aqui.

Veja abaixo as fotos do quadro onde o código foi resolvido.


Vamos lá...

Classes envolvidas
Já foi acordado na aula 5 que utilizaríamos a classe utilitária Util para aproveitar os métodos escrever() e lerNumero(). Além dessa classe, será necessária uma classe auxiliar apenas para executar os passos definidos no caso de uso - onde criaremos o método main(). E é claro que precisamos ainda da classe do domínio do sistema, chamada Recipiente.


Veja que optei em chamar a classe do método main() de Principal. Lembre-se que esse nome é opcional. Poderíamos por exemplo chamá-la de Executora, Main, etc.
A classe Util foi disponibilizada para vocês na aula 3. Para baixá-la novamente acesse o link http://linubr.blogspot.com.br/2012/08/aula-3-static-tipos-primitivos-e.html.
Nosso foco estará nas classes Recipiente e Principal.


Essa dúvida é muito comum!
Imagine que você precise varrer uma sala que só tenha uma porta. Você prefere partir dos cantos da sala e ir limpando de acordo com que vai caminhando para a porta ou inicia pela porta e vai se aproximando dos cantos até que tudo esteja limpo?
A vantagem da primeira opção é que mesmo que você suje o local onde está pisando, você ainda passará por ele, pois sua única saída da sala é a porta. A desvantagem, porém, é que se voar uma sujeirinha para um canto já varrido, você precisará limpá-lo novamente e ir trazendo a sujeira até o ponto onde estava.
Já na segunda opção, você está iniciando pela porta e vai caminhando até os cantos da sala. Obviamente ao terminar talvez ainda fique algum trecho sujo durante o caminho, lhe obrigando a repassar a vassoura.
Podemos associar a primeira opção quando iniciamos a programação pelas classes do domínio do problema - neste caso, a classe Recipiente. Ou seja, você vai resolvendo as dependências de programação assim que caminha em direção ao resultado final, que é a parte de interação com o usuário (a porta). Eventualmente ficará algum erro nos códigos programados obrigando-o a revisá-los e alterá-los (como se um canto que você já varreu voltasse a ficar sujo).
Na segunda opção, em que iniciamos pela parte de fora do sistema (onde ocorre a interação com o usuário), você sabe que existem dependências no código que serão resolvidas somente quando chegar o momento. Também é uma boa técnica, onde a programação vai surgindo de acordo com a necessidade. Não é objeto de estudo dessa aula falar sobre Test Driven Development. Essa técnica defende a programação dirigida a testes, onde ocorre mais ou menos o que teríamos ao iniciar a programação pela classe Principal.

Eu fico mais a vontade de iniciar com as classes do domínio. Nesse exercício faremos assim! Futuramente podemos, se surgirem oportunidades, voltar a comentar sobre a técnica Test Driven Development.


Classe Recipiente
Não cabe a nós programadores discutir nesse momento o que foi modelado na Aula 5.
Briga entre os programadores e analistas: Tá certo, tá certo... esses analistas da Aula 5 são uns escrotos! Fica criando caixinhas aqui, bolinhas acolá! E o código? Quem faz o trabalho sujo somos nós!
Bom, enfim, desavenças à parte, vou replicar o diagrama de classes aqui.

 


É disso mesmo que vamos falar.
Como sabemos, por convenção, é recomendado, mas não obrigatório, a criação de uma classe por arquivo fonte. Isso significa que devemos criar o arquivo Recipiente.java na raiz de nosso projeto. Dentro desse arquivo vamos começar criando as chaves de escopo da classe.

public class Recipiente {
}

Com esses comandos acima definimos a classe Recipiente. Esse código já compila, mas não serve pra nada por enquanto (há controvérsias, mas não vamos entrar em detalhes).


Veja bem... O Java não cria empecilhos quanto a posição e ordem em que os métodos ou atributos são codificados. Todavia, por boas práticas, optamos em criar primeiro os atributos e depois os métodos. Se eu fizer ao contrário é um erro? Erro, não! É só uma questão de organização.

Ambos os atributos capMax e qtdeAtual são do tipo primitivo inteiro. Mostrar-lhe-eis abaixo o código resultante (caraca heim... mostrar-lhe-eis é sacanagem... putz).

public class Recipiente {
  int capMax, qtdeAtual;
}

Ué... eu posso fazer isso? Quando trabalhamos com entidades (JPA - não vamos falar sobre isso agora, ok?!), não seria uma boa prática fazer esse tipo de definição para os atributos de uma classe. Mas aqui optei por assim fazer apenas para mostrá-los que “sim”, é possível fazer dessa forma!

Vamos agora aos métodos. Para facilitar vamos tratá-los separadamente do código da classe. Daí, após o entendimento de todos eles, mostraremos o resultado final.


Meu caro Padawan, diz a lenda que você se tornará um poderoso Jedi após dominar os conceitos de abstração, polimorfismo, encapsulamento e herança. Hoje ainda estamos tratando da abstração. No futuro você será capaz de entender os mistérios dos maizinhos e menuzinhos da modelagem UML. Por enquanto contente-se com o seguinte: isso se trata de encapsulamento!


Que a força esteja com você...

Métodos definirCapMax() e mostrarQtdeAtual()


Resolvi tratá-los em conjunto por serem métodos muito simples. O método mostrarQtdeAtual() simplesmente exibe no prompt o valor do atributo qtdeAtual através da chamada do método estático escrever() da classe Util.

  void mostrarQtdeAtual() {
    Util.escrever("A quantidade atual e " + qtdeAtual);
  }

Já o método definirCapMax() serve apenas para guardar no atributo capMax o valor que será informado no parâmetro cap. Esse tipo de método também é conhecido como Setter. Nas convenções adotadas pela comunidade (e pela linguagem) utiliza-se o padrão setNomeAtributo() para um método como esse. Aqui optei por enquanto em utilizar a palavra definir.

Detalhes:
Do ponto de vista prático faz sentido definirmos um valor negativo para a capacidade máxima de um recipiente?
Por exemplo, existe algum copo que cabe -50ml? Claro que não! Então, já que temos um método só para definir o atributo capMax, é conveniente tratarmos de alguma forma o valor que será passado pelo parâmetro, evitando questões desagradáveis de termos instâncias com capacidade máxima negativa.
Existem N formas de resolver esse problema. Aqui vamos fazer o seguinte:

  void definirCapMax(int cap) {
    if (cap > 0) {
      capMax = cap;
    } else {
      capMax = 1;
    }
  }

Nesse caso, sempre que for passado como parâmetro um valor maior que zero, estaremos considerando como uma capacidade válida. Todavia, caso cap seja menor ou igual a zero, definiremos a capacidade máxima do objeto como “um” miligrama.

Método encher()
Vamos agora entender a lógica do método encher().


Pense bem! O que acontece no mundo real quando estamos enchendo um copo com água?
Suponha que o copo tem 200ml e está vazio.
Se enchermos com 50ml de água, o que acontece? Poisé, nada!
E se agora colocássemos mais 300ml? O copo ficará totalmente cheio (com seus 200ml) e estaremos diante de uma poça de 150ml.
Esse é exatamente o comportamento que precisamos definir para o método encher().

Se você ainda está com dúvida sobre o comportamento da água e do copo nesse estudo, sugiro fazer um teste real, tentando encher um copo de 200ml com uma garrafinha de água de 500ml totalmente cheia, ai, bem em cima do seu notebook. Cara, garanto que você nunca mais vai esquecer como o Java funciona.

Bom, vamos ao método:

  void encher(int qtde) {
    if ( qtde + qtdeAtual > capMax ) {
      int sobra = ( qtde + qtdeAtual ) - capMax;
      Util.escrever("Putz, que M! Entornou " + sobra );
      qtdeAtual = capMax;
    } else {
      qtdeAtual = qtdeAtual + qtde;
    }
  }

Bom, é isso!

Método esvaziar()


A ideia do método esvaziar() também é inspirada no mundo real. Se temos um copo cheio com 100ml e tentamos esvaziar 70ml, ficaremos com um restante de 30ml. Se em seguida esvaziarmos mais 70ml, o copo ficará totalmente vazio e só receberemos 30ml - que era a quantidade que o copo tinha.
É exatamente por isso que definimos um valor int de retorno neste método. O fato de informarmos um valor no parâmetro qtde não significa que conseguiremos obter do recipiente toda essa quantidade.


Pra variar um pouco postei a foto tirada em aula.

Classe Recipiente
Senhores, apresento-lhes a classe Recipiente.

 
Tu é chato pra caraca em bixo! Putz. Gastei um tempão pra fazer essas setinhas alá MS-Paint e você ainda reclama! Tá doido!
Tá certo, pega aeeeee....

public class Recipiente {
  int capMax, qtdeAtual;
  void encher(int qtde) {
    if ( qtde + qtdeAtual > capMax ) {
      int sobra = ( qtde + qtdeAtual ) - capMax;
      Util.escrever("Putz, que M! Entornou " + sobra );
      qtdeAtual = capMax;
    } else {
      qtdeAtual = qtdeAtual + qtde;
    }
  }
  int esvaziar(int qtde) {
    if (qtde > qtdeAtual) {
      int varTemp = qtdeAtual;
      qtdeAtual = 0;
      return varTemp;
    } else {
      qtdeAtual = qtdeAtual - qtde;
      return qtde;
    }
  }
  void mostrarQtdeAtual() {
    Util.escrever("A quantidade atual e " + qtdeAtual);
  }
  void definirCapMax(int cap) {
    if (cap > 0) {
      capMax = cap;
    } else {
      capMax = 1;
    }
  }
}

Classe Principal (onde fica o void main)
Bom, vamos então à classe Principal. Como vimos, será através do método main() que resolveremos os cenários discutidos no modelo de casos de uso. Na aula 5 chegamos a identificar mais minuciosamente o que cada um dos passos significa, em texto. Modelamos também o diagrama de sequência. Vou replicá-los aqui.

Passos próximos ao código do main() gerados a partir do cenário principal do modelo de casos de uso:
1- Criar uma instância da classe Recipiente chamada garrafa;
2- Ler um número inteiro através do método Util.lerNumero() e guardá-lo numa variável temporária;
3- Chamar o método definirCapMax() do objeto garrafa passando como parâmetro a variável temporária;
4- Criar uma instância da classe Recipiente chamada copo;
5- Ler um número inteiro através do método Util.lerNumero() e guardá-lo numa variável temporária;
6- Chamar o método definirCapMax() do objeto copo passando como parâmetro a variável temporária;
7- Ler um número inteiro através do método Util.lerNumero() e guardá-lo numa variável temporária;
8- Chamar o método encher() do objeto garrafa passando como parâmetro a variável temporária;
9- Ler um número inteiro através do método Util.lerNumero() e guardá-lo numa variável temporária;
10- Chamar o método esvaziar() do objeto garrafa passando como parâmetro a variável temporária e guardando o valor de retorno dentro de outra variável temporária chamada agua;
11- Chamar o método encher() do objeto copo passando como parâmetro a variável temporária agua;
12- Chamar o método mostrarQtdeAtual() do objeto garrafa;
13- Chamar o método mostrarQtdeAtual() do objeto copo.

Diagrama de Sequência:


Diante desse entendimento, fica fácil chegarmos à construção da classe Principal. Veja abaixo o código resultante dessa classe:

public class Principal {
  public static void main(String args[]) {
    Recipiente garrafa = new Recipiente();
    Util.escrever( "Garcom: informe a capacidade maxima da garrafa (ml):" );
    garrafa.definirCapMax( Util.lerNumero() );
    Recipiente copo = new Recipiente();
    Util.escrever( "Garcom: informe a capacidade maxima do copo (ml):" );
    copo.definirCapMax( Util.lerNumero() );
    Util.escrever( "Garcom: voce que encher a garrafa com quanto (ml)?" );
    garrafa.encher( Util.lerNumero() );
    Util.escrever( "Cliente: quantos mls de agua voce quer colocar no copo?" );
    int agua = garrafa.esvaziar( Util.lerNumero() );
    copo.encher( agua );
    Util.escrever( "Garrafa:" );
    garrafa.mostrarQtdeAtual();
    Util.escrever( "Copo:" );
    copo.mostrarQtdeAtual();
  }
}

Pessoal, não vou entrar em detalhes sobre a definição desses passos pois já discutimos isso na aula 5.

Enfim, vamos compilar os fontes. Lembre-se de copiar o arquivo Util.java para o diretório onde você criou esse projeto. Em seguida, execute o sistema.


Nessa execução, optei em definir uma garrafa de 500 e um copo de 200. Adiante enchi a garrafa com 400 e passei para o copo 100 ml. Daí no resultado final encontramos (graças a Deus) uma quantidade atual de 300ml para a garrafa e 100ml para o copo.


Porque você é assim Aristóteles? Tudo bem, vale a pena montarmos a Stack e a Heap para essa execução.

Stack e Heap para o exercício da Garrafa e do Copo
Vamos apresentar agora os passos que a JVM fez para executar a sequência mostrada no prompt tratado anteriormente. Utilizarei aqui os mesmos valores.
Dessa vez vamos fazer de forma diferente! Ao invés de mostrarmos os passos textualmente, vamos associá-los aos comandos do código.

Quando executamos o comando:

java Principal

A JVM levanta para a memória o método estático main() que existe nessa classe.


Esse método é então alocado na Stack, como mostrado abaixo:

Daí todos os comandos presentes nesse método são executados sequencialmente na ordem em que foram definidos. A partir de agora mostraremos os comandos e a memória correspondente, detalhando as operações quando necessário.

Primeiro comando:

Recipiente garrafa = new Recipiente();


Próximo comando:
Util.escrever( "Garcom: informe a capacidade maxima da garrafa (ml):" );

Esse comando apenas imprime no prompt a mensagem indicada. O comando seguinte é:

garrafa.definirCapMax( Util.lerNumero() );

Como eu entrei com o valor 500, foi alocado na Stack um espaço para o método definirCapMax() cujo valor do parâmetro cap é 500.


Resolvendo a condição, chegamos à definição do atributo capMax com o valor passado no parâmetro. Em seguida o método é retirado da Stack. Observe que esse método teve seus comandos pertinentes aos atributos da instância garrafa.


O próximo comando é:

Recipiente copo = new Recipiente();


É muito importante que vocês visualizem que agora existem dois objetos instanciados na Heap, cada um com seus próprios valores. Veja que eles são objetos da mesma classe (Recipiente) e que possuem os mesmos atributos, mas cada um com seus próprios valores. Note que para acessar os dados e comportamentos de cada um deles nós precisaremos trabalhar com o nome de suas variáveis de referência: garrafa ou copo.
Dando sequência:

Util.escrever( "Garcom: informe a capacidade maxima do copo (ml):" );

Esse comando só escreve na tela. O próximo é:

copo.definirCapMax( Util.lerNumero() );

Assim como aconteceu para o objeto garrafa, ao invocarmos o método definirCapMax() de copo, teremos uma nova alocação na Stack, porém, dessa vez apontando para os atributos do copo. Como eu informei o valor 200, o parâmetro cap será inicializado com esse valor.


Na sequência, após a execução do IF, esse método definirá o valor do atributo capMax do objeto copo.


O comando seguinte mostrou uma String na tela. O próximo permitiu informar o valor (em ml) que deverá ser guardado na garrafa. Eu entrei com o número 400 no prompt. Daí, esse número foi passado ao método encher().

Util.escrever( "Garcom: voce que encher a garrafa com quanto (ml)?" );
garrafa.encher( Util.lerNumero() );


Como a condição do IF não foi satisfeita, entramos no escopo do else onde o valor de qtdeAtual do objeto garrafa foi definido com a soma de qtdeAtual e qtde da própria garrafa - ou seja, um valor de 400 como resultado. Esse valor é então guardado no atributo e o método encher() é retirado da Stack.


Segundo a análise (Casos de Uso), a partir de agora os comandos de entrada e mensagens do sistema serão direcionados ao ator cliente.

Util.escrever( "Cliente: quantos mls de agua voce quer colocar no copo?" );

Esse comando apenas escreveu na tela a pergunta ao cliente. O comando seguinte lê um número digitado no teclado e o passa para o métdo esvaziar() da garrafa.


Como a quantidade solicitada para ser retirada da garrafa é inferior à quantidade atual, a condição é levada para o else, onde reduzimos o valor de qtdeAtual de qtde (neste caso, 400 - 100).


Em seguida o método esvaziar() de garrafa é retirado da Stack retornando o valor de qtde para uma variável local ao método main chamada agua.


Como próximo comando temos:

copo.encher( agua );

Assim, o método encher() do objeto copo é alocado na Stack e recebe como parâmetro qtde o valor da variável local agua.


Observe que o método encher() alocado pertence ao objeto copo, logo, suas ações somente terão efeito sobre os atributos desse objeto.
Mais uma vez caímos no escopo do else (porque a qtde informada no parâmetro mais a qtdeAtual de copo não excedem o valor de capMax). Ao final da execução desse método, copo passará a guardar o valor 100 no seu atributo qtdeAtual. O método encher() é retirado da Stack.


Os quatro últimos comandos do main são:

Util.escrever( "Garrafa:" );
garrafa.mostrarQtdeAtual();
Util.escrever( "Copo:" );
copo.mostrarQtdeAtual();

Aqui apenas são mostrados no prompt os valores dos atributos qtdeAtual de garrafa e copo, respectivamente. Após o término dos comandos em main() o programa termina sua execução. Observe que toda a Stack foi liberada, porém os objetos alocados continuarão na Heap aguardando a limpeza do Garbage Collection.


Como vocês puderam perceber, os valores que entrei nessa execução foram bem comportados, não gerando em nenhum momento fluxos do cenário alternativo do modelo de casos de uso.

Outra execução do sistema
Veja abaixo uma execução mais divertida.


Note que ao informar valores excedentes aos definidos na capMax dos objetos, caímos nos fluxos alternativos dos métodos encher() e esvaziar().


Ótima ideia!

Faça você mesmo...
Deixo agora para vocês, como tarefa de casa, a representação da Stack e Heap dessa última execução que fizemos no prompt mostrado anteriormente. É importante que você tente fazê-la para tentar compreender como a JVM trabalha.


Atenção: eu não farei a Stack e Heap novamente para esse exercício. Fica a seu cargo reforçar esse aprendizado.

Chegamos então, meus amigos, ao fim de mais uma aula sobre Abstração. Acredito que estamos preparados para absorver o próximo conteúdo, que será, encapsulamento. Todavia, como procuro montar a sequência do material conforme a necessidade dos meus amigos em sala de aula, pode ser que haja oportunidade de novos exercícios sobre os assuntos já discutidos.
Bom, enfim, vai depender da próxima aula presencial!

Obrigado pela companhia.
Abraços, Guilherme Pontes

Nenhum comentário:

Postar um comentário