quarta-feira, 8 de agosto de 2012

Aula 11 - Resolvendo o exercício da Pilha e Fila


Daremos agora continuidade à aula 10 (http://linubr.blogspot.com.br/2012/08/aula-10-heranca-o-que-e-e-para-que-serve.html), onde foram tratados alguns assuntos, sendo a Herança o de mais importância. Nesse artigo vamos resolver o exercício da Pilha e Fila, desde a modelagem UML até o código Java. Acompanhe-nos, vai ser divertido!
Considerações gerais sobre o exercício
Confesso que esse exercício excede um pouco a dificuldade dos exemplos que temos tratados até o momento.
Isso é ruim ou bom? Se você compreendeu o que deve ser feito: é bom! Se tentou fazer: é melhor ainda! Essa é uma oportunidade real de testarmos nossos passos até o momento. Caso você tenha conseguido resolver todo o exercício, ótimo! Caso não tenha, ótimo também! Vamos construir nossos alicerces através da técnica errar, aprender e acertar! É mais ou menos o que ensina naquela sábia canção: beber, cair e levantar...
Bom, então vamos lá!
Por onde começamos?
Pelos requisitos funcionais, claro! Vou replicá-los aqui.
Requisitos do Sistema 
1. Modelar um sistema que permita o usuário escolher entre duas alternativas de estrutura de dados: uma Pilha ou uma Fila. 
2. Ambas estruturas devem ser capazes de armazenar um conjunto de até 10 números inteiros. 
3. O sistema deve oferecer a possibilidade do usuário colocar ou retirar elementos da estrutura, de forma iterativa. Mensagens devem ser mostradas na tela quando os limites superior e inferior da estrutura forem alcançados.
A partir do enunciado podemos considerar o seguinte seguimento textual (entenda isso como um modelo descritivo do sistema ou uma prévia para os Casos de Uso).
O usuário do sistema será capaz de informar qual estrutura de dados deseja trabalhar. Como o próprio item 1 reflete, as estruturas possíveis são Pilha ou Fila. Para resolver esse tipo de situação, imagino um menu (em prompt mesmo) onde o usuário possa escolher entre duas opções: 1 para Pilha e 2 para Fila.
Depois de selecionada a estrutura, devemos ser capazes de definir a quantidade máxima de elementos.
O item dois diz que as estruturas devem ser capazes de armazenar um conjunto de até dez números. Daí, estou deduzindo que o usuário poderá informar o tamanho das estruturas, sendo inválidos valores maiores que 10.
Após essa definição, visualizo uma estrutura de repetição na qual o usuário poderá optar em retirar ou incluir elementos da estrutura escolhida (em cada iteração). Os requisitos funcionais ainda apontam para a exibição na tela (prompt) de mensagens indicando inconsistências nas operações - por exemplo, quando um usuário tentar incluir um quinto elemento numa fila com capacidade para quatro números.
De forma geral acho que chegamos a um entendimento superficial.
Modelando os Casos de Uso
Esse texto que discutimos acima foi capaz de identificar minúcias implícitas nos requisitos. A priori ele serve como referência para um entendimento da necessidade do sistema. Todavia, como sabemos, a modelagem de sistemas Orientado a Objetos emprega uma linguagem padrão definida pela UML. O que vamos tentar fazer agora é justamente converter o texto descritivo em Diagrama e Modelo de Casos de Uso.
De praxe vou começar tentando eleger um Ator que seja coerente com o sistema. Veja que em nenhum momento identificamos uma entidade forte o suficiente para assumir esse papel. Portanto vou optar em chamar nosso Ator de Usuário. Fique à vontade de utilizar outro nome, se isso lhe agradar.
Agora precisamos identificar as funcionalidades disponíveis no sistema. Sabe-se que será necessário escolher uma estrutura, definir seu tamanho e gerenciar a inclusão e remoção de números. Podemos então resumir essas ações no seguinte diagrama.
Veja que optei em agrupar as funcionalidades de escolher uma estrutura e definir tamanho de uma estrutura num único casos de uso chamado Definir Estrutura. A partir daí o usuário poderá acrescentar ou retirar números dessa estrutura no caso de uso Controlar Estrutura. A notação extends aplicada na seta que liga os Casos de Uso Incluir Número e Retirar Número ao Controlar Número denotam que, opcionalmente, o cenário proposto pelos Casos de Uso Incluir Número e Retirar Número serão realizados dentro do Controlar Estrutura. Ou seja, em Controlar Número o usuário poderá optar (ou não) de executar as funcionalidades extendidas nos outros dois Casos de Uso.
Faremos agora o Modelo de Casos de Uso que reflete o diagrama.
Definir Estrutura
Pré-condição: nenhuma.
Pós-condição: Execução do Caso de Uso Controlar Estrutura.
Cenário Principal:
1. O usuário deverá escolher uma das duas estruturas: Pilha ou Fila;
2. O usuário deverá definir o tamanho de elementos da estrutura;
Cenários Alternativos:
1.1. Caso o usuário não escolha uma das duas estruturas, o sistema deixará como estrutura default a Fila;
2.1. Caso o tamanho definido seja inválido, o sistema criará uma estrutura com 10 elementos (valor default);
Controlar Estrutura
Pré-condição: Execução do Caso de Uso Definir Estrutura.
Pós-condição: nenhuma.
Cenário Principal:
1. O usuário deve escolher entre as opções de inserir, retirar ou sair do sistema, de forma iterativa (looping).
Cenários Alternativos:
1.1. Caso a opcão selecionada seja sair do sistema, o programa será finalizado.
Incluir Número
Cenário Principal:
1. O usuário seleciona a opção de incluir número;
2. O usuário informa o número que será incluído;
3. O sistema inclui o número na estrutura.
Cenários Alternativos:
3.1. Caso não exista espaço suficiente na estrutura, o sistema deverá mostrar uma mensagem de erro.
Retirar Número
Cenário Principal:
1. O usuário seleciona a opção de retirar número;
2. O sistema retira o número da estrutura.
Cenários Alternativos:
2.1. Caso não existam números na estrutura, o sistema deverá mostrar uma mensagem de erro.
A partir desse entendimento estaremos mais preparados para modelar as classes.
Modelando o Diagrama de Classes
Não existe receita de bolo para se iniciar a modelagem das classes. Com a prática nós vamos ficando mais permissíveis na contextualização orientada a objetos e passamos a enxergar com mais nitidez quais as classes que compõem o sistema. Já tenho em mente uma solução e vou tentar (talvez sem sucesso) transmití-la para vocês, aos poucos. Nosso amigo ultraman vai nos ajudar...
Pense comigo... Uma Pilha é capaz de incluir e retirar elementos, todavia com uma condição especial na retirada: o elemento que sai da pilha é sempre aquele que entrou por último. Lembre-se de uma pilha de pratos!
Agora vamos pensar na Fila! Numa fila também estão disponíveis ações de incluir e retirar elementos, entretanto sob uma condição que o primeiro elemento que entrou na fila é o primeiro a sair!
Bom... é claro que não devemos considerar a fila de idosos nesse exemplo! Pense apenas numa fila comum.
Diante dessas considerações podemos entender que tanto numa pilha quanto numa fila é necessário armazenar um conjunto de elementos (que neste exercício, são números inteiros), sendo que existe uma pequena particularidade na retirar dos elementos em cada uma delas. Por que não armazená-los num array?
É verdade! Ao utilizarmos arrays temos que ter cuidado em guardar ou retirar elementos! Por exemplo, se tivéssemos uma array com 5 posições e tentássemos colocar um número na sexta posição, teríamos um erro de execução no programa! Uma forma coesa de resolver esse complicador seria implementar, através do encapsulamento, métodos públicos para incluir e retirar elementos do array.
Isso significa que na classe Pilha faremos o gerenciamento do array de forma a evitar o direcionamento para uma posição inválida tanto no método de inclusão, quanto no de retirada. Repetiríamos esse mesmo código na classe Fila e tudo estaria resolvido... será mesmo?
Veja que existe uma certa complexidade no tratamento do array em ambas as classes. Existe ainda uma semântica natural entre elas (ambas devem guardar seus elementos num atributo array que ficará encapsulado através de métodos públicos). Que recurso da Orientação a Objetos poderíamos utilizar nessa modelagem afim de evitar a replicação de código desnecessária?
Muito bem. Pela herança seremos capazes de definir uma super-classe só para gerenciar o comportamento de inclusão e retirada de elementos de um array numérico. Nas suas sub-classes faremos apenas o gerenciamento da posição em que os elementos serão colocados ou retirados. Vejamos abaixo um primeiro esboço da solução.
Da forma que conversamos, dados é um array que será gerenciado apenas pela classe Array que criamos. Veja que estão disponíveis métodos públicos:
definirTamanho() - onde poderemos informar o tamanho do array dados e instanciá-lo;
incluir() - onde informamos o valor e a posição de onde o elemento será incluído e
retirar() - que recebe como parâmetro a posição e retorna o valor do elemento.
Ainda nesse diagrama definimos que as classes Pilha e Fila são subclasses da classe Array. A classe Pilha contém um atributo topo (para indicar a posição do topo da pilha ) e os métodos incluir() e retirar(). Veja que não há necessidade de informar a posição em que os elementos serão incluídos ou retirados. Isso ocorre devido ao fato de que nesse tipo de estrutura não deve existir essa flexibilidade. A classe Fila é similar, todavia existem algumas diferenças: a primeira delas gira em torno do atributo ultimo. Na prática talvez seu comportamento seja igual ao encontrado no atributo topo de Pilha, mas para esse problema específico, seria mais conveniente chamá-lo de ultimo - afinal, se chamássemos de topo ficaria estranho...
A segunda diferença está na forma de implementar o método retirar(). Na classe Pilha esse método é trivial - sendo apenas necessário chamar o comportamento retirar() de sua classe-pai. Já o método retirar() da Fila é mais complicado. Ele precisará reorganizar toda a fila a partir do momento em que o primeiro elemento sai - ou seja, o segundo passa a ser o primeiro, o terceiro passa a ser o segundo, etc.
O que o ultraman quer dizer é o seguinte: o objetivo final do sistema é permitir que sejam incluídos e retirados elementos de uma Pilha ou Fila. Se os métodos incluir() e retirar() da superclasse Array forem públicos, os outros objetos que estiverem interagindo com Pilha ou Fila também terão acesso a eles.
E daí? O método inserir() da classe Array permite que coloquemos elementos em qualquer posição. Um objeto que estiver trabalhando com uma instância de Fila, por exemplo, poderia chamar diretamente o método inserir() herdado da classe Array e incluir um número no meio da fila. Do ponto de vista das estruturas de dados Pilha e Fila, não podemos inserir ou retirar elementos de forma aleatória. Somos obrigados a respeitar a regra que cada uma propõe.
Exercício de Fixação
Para lembrar como funciona uma fila, proponho que você visite a UPA do Jacarezinho na próxima segunda-feira às 6 da manhã e tente entrar no meio da fila. Se alguém olhar de cara feia pra você, não se preocupe! Eles estarão agindo assim porque só aprenderam a programar em Fortran e sabem que você é um programador Java.
Então, qual seria a visibilidade correta para esses métodos? Se colocarmos private, as próprias filhas de Array também não poderão acessar os métodos. Daí perderíamos a vantagem de que Array já é responsável por gerir a inserção e remoção de elementos em dados. O ideal nesse caso seria adotar a visibilidade protected. Assim, os objetos que estiverem fora da hierarquia das classes Array, Pilha e Fila não poderão acessar tais métodos.
Nota importante: como ainda não trabalhamos com pacotes, na implementação desse exercício a classe Controladora também teria acesso aos métodos devido ao funcionamento do protected no Java: que também permite que as classes do mesmo pacote tenham acesso aos recursos protegidos. Num projeto completo, onde o conceito de pacote também seria utilizado, a classe Controladora estaria em outro pacote (normalmente). Daí nosso Encapsulamento estaria garantido.
Essas três classes já são suficientes para resolver parte dos requisitos. Além de gerenciarmos o comportamento da Fila e Pilha, devemos ser capazes de permitir que o usuário escolha uma das estruturas, inclua e retire elementos. Normalmente temos colocado tais interações numa classe auxiliar (Mãe, Executora, etc). Todavia adotaremos uma estratégia distinta aqui: vamos modelar também uma classe de Controle do sistema. Esse tipo de classe geralmente é modelada para atribuir comportamento adequado às classes do domínio do problema (que neste caso são: Array, Pilha e Fila). Esse tipo de classe de controle também pode ser chamada de classe de Serviço ou de Negócio. A ideia básica é essa, mas ao longo do tempo vamos refinando esse entendimento.
Então, qual seria o papel da classe Controladora?
Permitir que o usuário escolha sua estrutura de dados, defina o tamanho dessa estrutura e, iterativamente (dentro de um looping, por exemplo), permita que ele inclua e exclua elementos da estrutura. Para facilitar a implementação e organizar as responsabilidades em métodos distintos, optei em modelar essa classe conforme imagem abaixo.
Vou dividir a explicação em duas partes: (1) UML e (2) entendimento da modelagem.
(1) É importante notar que as setas tracejadas com ponta aberta que ligam a classe Controladora às classes Pilha e Fila representam uma dependência. O que é uma dependência na UML? O relacionamento de dependência denota que a classe de onde a seta se origina depende da existência (no sentido de precisar que elas tenham sido modeladas) das classes apontadas. Isso significa que a classe Controladora não poderá exercer sua função sem as classes Fila e Pilha. O que significa a dependência no Java? No Java é mais simples: quando uma classe depende da outra, significa que em alguma parte do código da classe Controladora faremos referência às classes Pilha e Fila. Ah sim, na prática também não será possível compilar a classe Controladora antes de criarmos (e compilarmos) as classes Pilha e Fila.
(2) Agora se concentrando na modelagem. Optei em criar os métodos gerenciarPilha() e gerenciarFila() com o intuito de separar o gerenciamento dessas estruturas em operações diferentes. Isso apenas nos ajudará a organizar o código. Note também que coloquei ambos métodos como private. Não há necessidade deles estarem disponíveis para outros objetos do sistema. O método gerenciar() permitirá que o usuário crie sua estrutura desejada, informando o tamanho. Internamente nesse método faremos chamadas aos métodos gerenciarPilha() e gerenciarFila() acordando com a opção selecionada pelo usuário.
Nota: Veja que alterei a visibilidade do atributo dados para protegido. Isso se fez necessário porque, conforme conversamos, o método retirar() da classe Fila precisará reorganizar os elementos no array dados. Uma forma de permitirmos isso é colocando esse atributo como protected. Outra forma seria deixá-lo privado, porém disponibilizar métodos Getter e Setter.
Implementando o Exercício
Primeiro veremos quais arquivos fontes vamos precisar.
As classes auxiliares Util e Executora já são nossas conhecidas. A classe Controladora terá os recursos para interação com o usuário. As demais classes compõe o domínio do sistema. Dessa vez vamos iniciar a programação em ordem de dependência das classes. Nesse caso, a primeira classes que precisaríamos programar seria a classe Util. Essa classe você deverá pegar dos exercícios passados. Ela foi disponibilizada na aula 3 (http://linubr.blogspot.com.br/2012/08/aula-3-static-tipos-primitivos-e.html).
A próxima classe que mais gera dependência é a classe Array. Vamos ao seu código.
Arquivo Array.java
public class Array {
  protected int dados[];
  public void definirTamanho(int t) {
    int tamanho = 10;
    if ( t > 0 && t <= 10 ) {
      tamanho = t;
    }
    dados = new int[ tamanho ];
  }
  protected void incluir(int posicao, int valor) {
    if ( posicao > -1 && posicao < dados.length ) {
      dados[ posicao ] = valor;
    } else
      System.out.println( "Nao foi possivel incluir o elemento!" );
  }
  protected int retirar(int posicao) {
    int retorno = 0;
    if ( posicao > -1 && posicao < dados.length ) {
      retorno = dados[ posicao ];
      dados[ posicao ] = 0;
    } else
      System.out.println( "Nao foi possivel retirar o elemento!" );
    return retorno;
  }
  /*
   * Esse método não foi modelado, porém optei
   * em codificá-lo apenas para validar o sistema.
   * Ele será chamado sempre que for necessário
   * mostrar os dados do array.
   */
  protected void print() {
    System.out.print( "Estrutura de " + dados.length + " elementos: [ " );
    for (int i = 0; i < dados.length; i++) {
      System.out.print( dados[i] + " " );
    }
    System.out.println( "]" );
  }
}
Note que em definirTamanho() nós restringimos os tamanho entre 1 e 10 (segundo orientação dos requisitos). Por definição dos Casos de Uso mantivemos em 10 o valor default. O método incluir() verifica se a posição em que o valor será incluído é válida. O método retirar() faz verificação similar. Ambos apresentam mensagens de erro caso a operação tenha apresentado falha.
Importante: como se trata de um programa que de certa forma apresenta particularidade de código que pode levar ao erro de programação (estruturas de decisão, loopings, etc), optei em criar um método print() auxiliar que inclusive não tinha sido projetado nos diagramas UML. Esse método só será utilizado para validarmos os valores da estrutura durante o teste do sistema. Na prática faremos sua chamada a partir das subclasses Pilha e Fila.
Seguindo nossa ordem de dependência podemos criar as classes Pilha ou Fila. Vamos criar a classe Pilha primeiro - que é mais fácil.
Arquivo Pilha.java
public class Pilha extends Array {
  private int topo;
  public void incluir(int valor) {
    incluir( topo , valor );
    topo++;
    if ( topo > dados.length ) topo = dados.length;
    // Mostra a estrutura neste momento
    print();
  }
  public int retirar() {
    topo--;
    int retorno = retirar( topo );
    if (topo < 0) topo = 0;
    // Mostra a estrutura neste momento
    print();
    return retorno;
  }
}
Note o uso da sintaxe extends para indicar que Pilha é uma subclasse de Array. Veja que dentro do comportamento dos métodos incluir() e retirar(), fazemos referência aos métodos de mesmo nome da superclasse Array. Foi necessário também criar estruturas IFs para restringir o avanço ou regresso demasiado do atributo topo além dos limites inferiores e superiores.
Como disse, controlar o comportamento de topo faz parte da responsabilidade dessa classe, todavia, não precisamos nos preocupar com o estouro do array aqui (estouro no sentido de tentar guardar elementos fora dos limites)! A superclasse Array já incluiu regras de validação nos métodos de inclusão e exclusão de elementos. Aqui apenas utilizamos essa inteligência através das chamadas dos métodos incluir() e retirar() da superclasse, respectivamente.
Nota: Veja que fiz chamadas ao método print() da superclasse para exibir os valores do Pilha sempre que conveniente.
Vamos à classe Fila.
Arquivo Fila.java
public class Fila extends Array {
  private int ultimo;
  public void incluir(int valor) {
    incluir( ultimo , valor );
    ultimo++;
    if ( ultimo > dados.length ) ultimo = dados.length;
    // Mostra a estrutura neste momento
    print();
  }
  public int retirar() {
    int retorno = retirar( 0 );
    if (ultimo > 0) {
      ultimo--;
      int novoDados[] = new int[ dados.length ];
      for (int i = 0; i < (dados.length - 1); i++) {
        novoDados[ i ] = dados[ i+1 ];
      }
      dados = novoDados;
    }
    // Mostra a estrutura neste momento
    print();
    return retorno;
  }
}
No geral a implementação dessa classe é similar à da classe Pilha. Todavia, existe uma maior complexidade no método retirar(). Vamos descrevê-lo em detalhes agora:
  • No comando int retorno = retirar( 0 ); estamos guardando numa variável local inteira de nome retorno o valor contido no elemento de índice zero do array dados. A obtenção desse valor foi possível pela chamada do método retirar() da superclasse Array. Apontamos sempre para o índice zero pela característica da fila - que é permitir que o primeiro elemento da fila (índice zero) saia antes dos demais.
  • Observe que agora existe um trecho de código que só será executado caso ainda existam elementos na fila. Ou seja, se o índice do último elemento for maior do que zero. 
    • Como primeiro comando do escopo do IF, nós diminuímos em 1 o tamanho da fila através do comando ultimo--; 
    • No comando int novoDados[] = new int[ dados.length ]; estamos criando um novo array novoDados com o mesmo tamanho do array dados. Esse array guardará a nova formação da fila (lembre-se que vamos retirar um elemento... é como se diz por aí: a fila anda!
    • No looping representado abaixo  
    • nós percorremos os elementos do array original (dados) e colocamos no novo array (novoDados) com o cuidado de reorganizar os elementos. Veja que pegamos o elemento de índice 1 e colocamos na posição 0 do novo array. Isso se repete até o último elemento do array dados (que será guardado na penúltima posição do array novoDados).
    • No próximo comando (após a conclusão do FOR), dados = novoDados;, estamos fazendo com que o atributo de referência dados aponte para outro objeto na memória (novoDados). Na prática, dados, que apontava para um array na Heap, passará a apontar para outro array (que neste caso, é o mesmo array apontado por novoDados). Assim, dados assumiu sua nova formação.
  •  Por fim, após o fim do escopo do IF, a chamada print() mostrará o valor do array dados e o comando return retorno; retornará o valor da variável interna retorno.
Confesso que realmente existe um certo grau de dificuldade na codificação desse método. Uma pitadinha de emoção no exercício não faz mal a ninguém, né?!
Vamos à classe Controladora.
Arquivo Controladora.java
public class Controladora {
  private void gerenciarPilha(int tamanho) {
    Pilha p = new Pilha();
    p.definirTamanho( tamanho );
    int opcao = 0;
   
    while ( opcao != 3 ) {
      System.out.println( "Digite:" );
      System.out.println( "   1 - inserir um elemento" );
      System.out.println( "   2 - retirar um elemento" );
      System.out.println( "   Qualquer outro valor para SAIR" );
      opcao = Util.lerNumero();
     
      switch ( opcao ) {
        case 1: {
          System.out.println( "Digite o valor que deseja incluir:" );
          int valor = Util.lerNumero();
          p.incluir( valor );
        } break;
        case 2: {
          int valor = p.retirar();
          System.out.println( "A estrutura retornou o elemento " + valor );
        } break;
        default: { opcao = 3; } break;
      }
    }
  }
  private void gerenciarFila(int tamanho) {
    Fila f = new Fila();
    f.definirTamanho( tamanho );
    int opcao = 0;
   
    while ( opcao != 3 ) {
      System.out.println( "Digite:" );
      System.out.println( "   1 - inserir um elemento" );
      System.out.println( "   2 - retirar um elemento" );
      System.out.println( "   Qualquer outro valor para SAIR" );
      opcao = Util.lerNumero();
     
      switch ( opcao ) {
        case 1: {
          System.out.println( "Digite o valor que deseja incluir:" );
          int valor = Util.lerNumero();
          f.incluir( valor );
        } break;
        case 2: {
          int valor = f.retirar();
          System.out.println( "A estrutura retornou o elemento " + valor );
        } break;
        default: { opcao = 3; } break;
      }
    }
  }
  public void gerenciar() {
      System.out.println( "Escolha sua estrutura:" );
      System.out.println( "   1 - Pilha" );
      System.out.println( "   2 - Fila (default)" );
      int escolha = Util.lerNumero();
      System.out.println( "Digite o tamanho da estrutura:" );
      int tamanho = Util.lerNumero();
      if ( escolha == 1 ) {
        gerenciarPilha( tamanho );
      } else {
        gerenciarFila( tamanho );
      }
  }
}
Apesar de extensa, essa classe é bem simples. Os métodos gerenciarPilha() e gerenciarFila() são praticamente iguais - exceto pelo fato de um instanciar uma Pilha e o outro uma Fila. Depois que foi criada a estrutura, esses métodos permitem que o usuário escolha entre as opção de incluir e retirar elementos. Existe também a opção de sair do sistema. Tudo isso fica dentro de um looping WHILE que permanece em execução enquanto a opção de escolha do usuário seja diferente de 3 (poderia ser qualquer outro número aqui, ok?). Dentro do looping existe um SWITCH que controla qual ação executar na estrutura variando com a escolha do usuário. Porém, lembre-se...
Já o método gerenciar() permite a definição da estrutura e do número de elementos. A estrutura de condição IF direciona para um dos métodos internos gerenciarPilha() ou gerenciarFila() acordando com a escolha do usuário.
Resta-nos por fim a classe Executora - que é muito simples.
Arquivo Executora.java
public class Executora {
  public static void main(String args[]) {
   
    Controladora c = new Controladora();
    c.gerenciar();
  }
}
Veja que seu único papel é instanciar a classe Controladora e chamar o método gerenciar().
Executando o Sistema
Vamos apresentar agora dois simples casos de execução. O primeiro deles diz respeito a um teste da Pilha.
Note que por conta do método print() - que extrapola a modelagem UML - fomos capazes de observar que, ao retirar um elemento, nossa pilha permaneceu com o número 50 no índice 0 - pois o número 100 era o topo da Pilha.
Vamos fazer agora um pequeno teste na Fila.
Nessa execução tentamos exceder o limite de 2 elementos da Fila (valor definido no início da execução) e o sistema retornou (conforme orientação dos requisitos) uma mensagem de erro. Veja também que ao retirarmos um elemento da Fila, o número de índice 1 (segundo elemento da fila) passou a ficar localizado no índice 0. Isso pôde ser obsesrvado na mensagem
Estrutura de 2 elementos: [ 5 0 ]
impressa no prompt.
Fica como dever de casa tentar implementar esse sistema observando a modelagem e o código criados. Tente também comparar sua solução com a que propusemos aqui. Vou deixar também que a completude dos testes seja realizada por vocês, ok?!
Considerações finais sobre esse projeto
Deve ter ficado claro para vocês que o código apresentado não foi dígno da Orientação a Objetos. Sempre há um boato que na Orientação a Objetos existe muita reutilização de código, elegância na solução, etc. Nesse exercício, apesar de atendermos os requisitos, pode-se notar uma grande repetição de código e uso inapropriado de estruturas de condição (Switch). Veja por exemplo que os métodos gerenciarPilha() e gerenciarFila() da classe Controladora são praticamente idênticos. Note também que no método gerenciar() fizemos uso de um Switch para controlar o fluxo de ação do sistema.
Será que não existe uma solução mais apropriada?
Sim! Existe... Porém só poderemos implementá-la após aprendermos os conceitos de Polimorfismo. Aliás, chegamos a esbarrar no conceito de sobrecarga de métodos (que faz parte do Polimorfismo) nesse exercício, mas não comentei nada com vocês (ainda.... ops!!).
Vamos combinar o seguinte: assim que terminarmos de estudar os princípios básicos da Orientação a Objetos e alguns conceitos gerais da API (como Tratamento de Exceções, por exemplo), vamos tentar remodelar esse exercício aplicando todo o poder da Modelagem O.O. (Orientação a Objetos).
Ficamos combinados assim?
É isso ai pessoal, obrigado pela leitura!
Abraços, Guilherme Pontes

Nenhum comentário:

Postar um comentário