Análise Retrospectiva dos Projetos de Desenvolvimento: Exercícios 4 a 6

Análise dos Exercícios de Programação 4 a 6

Este documento apresenta uma análise retrospectiva e detalhada dos exercícios de programação do quarto ao sexto ciclo, abordando a complexidade, as estratégias de design, a implementação de funcionalidades, os desafios encontrados e as oportunidades de melhoria. Cada ciclo representou uma evolução em termos de requisitos e complexidade técnica, exigindo abordagens de design e codificação cada vez mais sofisticadas.

  1. Dificuldade dos Exercícios

1.1. Exercício 4

O quarto exercício, uma iteração sobre os três primeiros, introduziu um nível de complexidade significativamente maior. Os principais desafios residiam na leitura e interpretação precisas das entradas, especialmente em questões de múltipla escolha onde a pontuação parcial era necessária, e no tratamento eficiente de múltiplas provas.

1.2. Exercício 5

O quinto exercício foi um novo desafio, com uma dificuldade moderada. A abordagem modular e o uso intensivo de princípios de programação orientada a objetos, como herança e polimorfismo, foram cruciais para a modelagem de diferentes dispositivos e seus comportamentos. Estruturas de dados como Map e Set foram empregadas para acesso rápido e ordenação dos dispositivos.

1.3. Exercício 6

O sexto exercício, embora uma continuação do quinto, apresentou um salto considerável na dificuldade, aproximando-se de requisitos de sistemas do mundo real. A complexidade aumentou devido à necessidade de gerenciar circuitos elétricos mais elaborados, incluindo conexões em série e paralelo.

  1. Design e Análise da Implementação

2.1. Exercício 4: Sistema de Gestão de Provas

2.1.1. Design de Classes

  • Questao: Representa uma questão com ID, enunciado, resposta correta e tipo. Inclui métodos para verificar a resposta e calcular a pontuação.
  • Prova: Representa uma prova com ID e um mapeamento de questões para suas respectivas pontuações. Oferece métodos para adicionar questões e calcular a pontuação total.
  • Gabarito: Representa a folha de respostas de um aluno, contendo ID da prova, ID e nome do aluno, e suas respostas. Permite adicionar respostas do aluno.

2.1.2. Análise da Implementação de Funcionalidades

O sistema processava entradas formatadas para gerenciar questões, provas e respostas. As questões eram identificadas por prefixos como #N:, #Z:, #K: e armazenadas em um Map<Integer, Questao> para acesso eficiente. As provas, identificadas por #T:, também eram armazenadas em um mapa, com validação da pontuação total (deveria somar 100 pontos). Informações de alunos (#X:) eram armazenadas em Map<Integer, String>. As respostas dos alunos (#S:) eram coletadas em uma List<Gabarito>. Questões marcadas para exclusão (#D:) eram armazenadas em um Set<Integer> para desconsideração durante a avaliação.

A avaliação era realizada por gabarito, considerendo o tipo de questão:

  • Múltipla Escolha: Resposta deve ser totalmente idêntica.
  • Preenchimento: Aceita múltiplos padrões de resposta, desde que uma correspondência seja encontrada.
  • Resposta Aberta: Suporte a pontuação parcial.

O sistema também incluía tratamento de erros para entradas inválidas ou IDs inexistentes.

2.1.3. Diagrama de Classes

Um diagrama de classes detalhava as relações e atributos das entidades Questao, Prova e Gabarito, ilustrando a estrutura orientada a objetos do sistema.

2.2. Exercício 5: Sistema de Gerenciamento de Dispositivos Eletrônicos

2.2.1. Design de Classes

  • DispositivoControle: Classe base abstrata para controladores, com propriedades de tensão de entrada e saída e um método abstrato para atualizar a tensão de saída.
  • ChaveControle: Deriva de DispositivoControle, implementando um interruptor simples com estados ligado/desligado.
  • ReguladorEscalonado: Deriva de DispositivoControle, para controle de velocidade em passos (0 a 3).
  • ReguladorContinuo: Deriva de DispositivoControle, permitindo variação contínua da tensão de saída.
  • ComponenteEletrico: Classe base para componentes que consomem energia, com propriedade de diferença de tensão.
  • Ventilador, LampadaIncandescente, LampadaFluorescente: Subclasses de ComponenteEletrico, cada uma implementando seu comportamento específico (e.g., cálculo de velocidade, brilho).
  • GerenciadorComponentes: Orquestra a gestão e conexão dos dispositivos, mantendo coleções (TreeMap) para diferentes tipos de dispositivos e processando comandos de controle.

2.2.2. Análise da Implementação de Funcionalidades

O sistema gerenciava a conexão de componentes através do método processarConexao, armazenando a topologia. O método inicializarDispositivos percorria a lista de conexões, começando pela fonte de tensão (VCC), e configurava as tensões de entrada e saída de cada dispositivo conforme a topologia. O método processarComando interpretava comandos de controle para atualizar o estado dos dispositivos (e.g., ligar/desligar uma chave, ajustar um regulador) e propagava as alterações de tensão. A função exibirStatusDispositivos produzia uma saída ordenada do estado de todos os dispositivos.

2.2.3. Extensibilidade

A arquitetura favorecia a extensibilidade. Novas categorias de dispositivos podiam ser adicionadas facilmente estendendo DispositivoControle ou ComponenteEletrico. Novos comandos podiam ser incorporados ao método processarComando.

2.2.4. Diagrama de Classes

Um diagrama de classes detalhado representava a hierarquia de classes e suas interações, destacando a aplicação dos princípios de herança e polimorfismo.

2.3. Exercício 6: Sistema de Gerenciamento de Circuitos Complexos

2.3.1. Design de Classes

  • DispositivoControle e subclasses (ChaveControle, ReguladorEscalonado, ReguladorContinuo): Como no exercício 5, mas com refinamentos.
  • ComponenteEletrico e subclasses (LampadaIncandescente, LampadaFluorescente, Ventilador, VentiladorDePiso): Definem a funcionalidade específica de cada componente, incluindo a obtenção de seu estado.
  • PontoConexao: Representa um ponto de união no circuito.
  • CircuitoSerie e CircuitoParalelo: Classes para modelar as estruturas de circuitos elétricos, permitindo a representação de topologias complexas.
  • GerenciadorComponentes: Centraliza a gestão de todos os componentes e suas conexões, utilizando TreeMap para ordenação. Inclui métodos para processar conexões, aplicar controles, inicializar dispositivos e exibir status.

2.3.2. Análise da Implementação de Funcionalidades

O método processarConexao agora lidava com a definição de circuitos em série e paralelo. O método processarCircuitoPrincipal identificava o circuito principal para iniciar os cálculos de tensão. A inicialização dos dispositivos (inicializarDispositivos) envolvia um percurso recursivo pelo circuito (percorrerCircuito), calculando e ajustando as tensões de entrada e saída de cada dispositivo com base na sua posição no circuito. Comandos de controle eram processados por processarComando, que acionava a re-inicialização do circuito através de aplicarControles. O método exibirStatusDispositivos fornecia o estado atualizado de todos os componentes.

2.3.3. Características do Design

  • Design Orientado a Objetos: Herança e polimorfismo foram amplamente utilizados para abstrair e generalizar o comportamento de dispositivos e controladores.
  • Gestão Flexível de Circuitos: A capacidade de modelar circuitos complexos, incluindo série e paralelo, era um ponto forte.
  • Uso de Coleções Eficientes: TreeMap garantia a ordenação e acesso rápido aos dispositivos.

2.3.4. Diagrama de Classes

O diagrama de classes para este exercício ilustrava a estrutura complexa dos circuitos, as hierarquias de herança e a interação entre as classes, refletindo a modelagem mais avançada.

  1. Desafios e Lições Aprendidas

3.1. Exercício 4: Questões de Múltipla Escolha com Pontuação Parcial

3.1.1. Problema

A dificuldade residia em pontuar corretamente questões de múltipla escolha onde uma resposta parcialmente correta deveria receber metade da pontuação, e em distinguir este estado para feedback.

3.1.2. Reescrita do Exemplo de Código (Lógica de Pontuação)

O método a seguir ilustra a lógica revisada para calcular a pontuação de uma questão, especialmente tratando a pontuação parcial para múltiplos itens.

public class QuestaoM_Escolha extends Questao {
   // ... construtor e outros métodos ...

   @Override
   public int avaliarResposta(String respostaAluno, int pontuacaoMaxima) {
       Set<String> alternativasCorretas = new HashSet<>(Arrays.asList(this.getRespostasCorretas().split(" ")));
       Set<String> alternativasDoAluno = new HashSet<>(Arrays.asList(respostaAluno.split(" ")));

       if (alternativasCorretas.equals(alternativasDoAluno)) {
           return pontuacaoMaxima; // Resposta totalmente correta
       } else if (alternativasCorretas.containsAll(alternativasDoAluno) && !alternativasDoAluno.isEmpty()) {
           // Se o aluno escolheu um subconjunto das corretas, mas não todas, e não está vazia.
           // Isso cobre o "parcialmente correto" quando o aluno não escolheu alternativas erradas
           // mas faltou escolher algumas corretas.
           if (alternativasDoAluno.size() < alternativasCorretas.size()) {
               return pontuacaoMaxima / 2;
           }
       } else if (!Collections.disjoint(alternativasCorretas, alternativasDoAluno) && !alternativasDoAluno.equals(alternativasCorretas)) {
           // Se há alguma interseção, mas não é totalmente correta, e também não é um subconjunto limpo.
           // Isso pode pegar casos onde o aluno escolheu algumas corretas e algumas erradas.
           // Depende da regra específica de "parcialmente correto".
           // Para simplificar, vamos focar no subconjunto para 50%.
       }
       return 0; // Incorreta
   }

   @Override
   public String obterStatusCorrecao(String respostaAluno) {
       Set<String> alternativasCorretas = new HashSet<>(Arrays.asList(this.getRespostasCorretas().split(" ")));
       Set<String> alternativasDoAluno = new HashSet<>(Arrays.asList(respostaAluno.split(" ")));

       if (alternativasCorretas.equals(alternativasDoAluno)) {
           return "totalmente correta";
       } else if (alternativasCorretas.containsAll(alternativasDoAluno) && alternativasDoAluno.size() < alternativasCorretas.size()) {
           return "parcialmente correta";
       }
       return "incorreta";
   }
}

Nesta abordagem, a classe QuestaoM_Escolha (substituindo a lógica de #Z: no exemplo original) sobrepõe métodos da classe base Questao para tratar a avaliação específica. O método avaliarResposta primeiro verifica a correção total. Se não for totalmente correta, ele verifica se as alternativas do aluno formam um subconjunto das alternativas corretas (mas não todas elas), aplicando a pontuação parcial. O método obterStatusCorrecao fornece um feedback descritivo sobre o status da resposta.

3.2. Exercício 5: Falha na Identificação de Erros Apesar dos Testes Passarem

3.2.1. Problema

Todos os casos de teste fornecidos foram aprovados, mas havia uma intuição de que o código poderia ter problemas subjacentes, possivelmente relacionados à propagação de tensão ou à atualização de estados.

3.2.2. Reescrita do Exemplo de Código (Inicialização e Controle de Dispositivos)

A seguir, uma reestruturação da lógica de inicialização e processamento de comandos, com foco em uma propagação de tensão mais explícita e um melhor tratamento de estado.

public class SistemaGerenciamentoComponentes {
   private Map<Integer, ChaveControle> chaves = new TreeMap<>();
   // ... outros mapas para outros tipos de dispositivos

   public void configurarConexoesIniciais(List<String[]> listaConexoes) {
       // ... Lógica para construir o grafo de conexões ou uma lista ordenada
       // Aqui simulamos um processamento linear para demonstração
       double tensaoFonte = 220.0; // VCC
       for (String[] conexao : listaConexoes) {
           String pinoOrigem = conexao[0];
           String pinoDestino = conexao[1];

           if ("VCC".equals(pinoOrigem)) {
               if (pinoDestino.startsWith("K")) {
                   int numDisp = Integer.parseInt(pinoDestino.substring(1, pinoDestino.indexOf('-')));
                   chaves.computeIfAbsent(numDisp, k -> new ChaveControle());
                   chaves.get(numDisp).definirTensaoEntrada(tensaoFonte);
                   chaves.get(numDisp).atualizarTensaoSaida(); // Recalcula saída com base no estado inicial
                   // ... Lógica para conectar a saída desta chave a outros dispositivos
               }
               // ... Lógica similar para F (ReguladorEscalonado) e L (ReguladorContinuo)
           }
           // ... Mais lógica para processar conexões entre dispositivos
       }
   }

   public void executarComandoControle(String comandoLinha) {
       if (comandoLinha.startsWith("#K")) {
           int numeroChave = Integer.parseInt(comandoLinha.substring(2));
           ChaveControle chaveAlvo = chaves.get(numeroChave);
           if (chaveAlvo != null) {
               chaveAlvo.alternarEstado();
               chaveAlvo.atualizarTensaoSaida(); // Importante: recalcular a saída da chave
               // Após a mudança de estado e tensão de saída da chave,
               // é necessário propagar essa mudança para todos os dispositivos a jusante.
               // Isso exigiria um mecanismo de notificação ou uma re-inicialização parcial do circuito.
               System.out.println("Chave K" + numeroChave + " estado alterado. Tensão de saída: " + chaveAlvo.getTensaoSaida());
           }
       }
       // ... Lógica para outros tipos de controle (F, L)
   }
}

Na versão reescrita, SistemaGerenciamentoComponentes.configurarConexoesIniciais processa as conexões para configurar a tensão de entrada e garantir que a tensão de saída de cada controlador seja calculada desde o início. O método executarComandoControle, ao receber um comando, localiza o controlador, altera seu estado (e.g., alterna uma chave) e recalcula sua tensão de saída imediatamente. O ponto crucial é que qualquer alteração de um controlador exigiria uma subsequente propagação de tensão para todos os componentes conectados a ele, o que poderia ser implementado através de um mecanismo de "dirty bit" e re-avaliação do circuito ou um padrão Observer.

3.3. Exercício 6: Problemas na Lógica de Atualização de Tensão em Circuitos Complexos

3.3.1. Problema

A lógica de atualização de tensão era inconsistente, especialmente em circuitos paralelos, onde a tensão de entrada para cada ramo deveria ser a mesma, e em como as tensões de saída dos controladores se propagavam para os dispositivos subsequentes.

3.3.2. Reescrita do Exemplo de Código (Traversia do Circuito e Atualização de Tensão)

Uma abordagem mais robusta para percorrer e calcular tensões em circuitos pode ser implementada da seguinte forma:

public class GerenciadorDeCircuitos {
   private Map<String, CircuitoSerie> circuitosEmSerie = new HashMap<>();
   private Map<String, CircuitoParalelo> circuitosEmParalelo = new HashMap<>();
   // ... mapas para dispositivos de controle e componentes elétricos

   /**
    * Percorre o circuito recursivamente para calcular e definir as tensões.
    * @param idCircuito ID do circuito (ex: T1, M1)
    * @param tensaoEntradaAtual Tensão de entrada no ponto atual do circuito.
    */
   public void calcularTensoesNoCircuito(String idCircuito, double tensaoEntradaAtual) {
       if (idCircuito.startsWith("T")) { // Circuito em Série
           CircuitoSerie cSerie = circuitosEmSerie.get(idCircuito);
           if (cSerie == null) return;

           double tensaoParaProximo = tensaoEntradaAtual;
           for (PontoConexao ponto : cSerie.getPontosDeConexao()) {
               String idDispositivo = ponto.getIdDispositivo();
               String tipoDispositivo = obterTipoDispositivo(idDispositivo);
               int numeroDispositivo = obterNumeroDispositivo(idDispositivo);

               // Ignora pontos especiais (VCC, GND, IN, OUT)
               if (idDispositivo.equals("VCC") || idDispositivo.equals("GND") ||
                   idDispositivo.equals("IN") || idDispositivo.equals("OUT")) {
                   continue;
               }

               if (tipoDispositivo == null) {
                   System.err.println("Dispositivo desconhecido: " + idDispositivo);
                   continue;
               }

               // Lógica para dispositivos de controle
               if (tipoDispositivo.equals("K") || tipoDispositivo.equals("F") || tipoDispositivo.equals("L")) {
                   DispositivoControle controlador = obterControlador(tipoDispositivo, numeroDispositivo);
                   if (controlador != null) {
                       controlador.definirTensaoEntrada(tensaoParaProximo);
                       controlador.atualizarTensaoSaida(); // Atualiza a saída com base no estado atual
                       tensaoParaProximo = controlador.getTensaoSaida(); // A tensão para o próximo ponto é a saída do controlador
                   }
               } else { // Lógica para componentes elétricos
                   ComponenteEletrico componente = obterComponente(tipoDispositivo, numeroDispositivo);
                   if (componente != null) {
                       // Componentes consomem tensão, sua "entrada" é a tensão que chega a eles
                       componente.definirDiferencaTensao(tensaoParaProximo);
                       // Em circuitos em série, componentes geralmente subtraem tensão.
                       // Para simplificar a propagação, assumimos que eles são terminais no "ramo" atual
                       // ou que sua queda de tensão é calculada separadamente se necessário para o próximo.
                       // Para este exemplo, a tensão do próximo é o que 'restou' após um controlador.
                       // Se fosse um resistor, tensaoParaProximo = tensaoParaProximo - componente.getQuedaTensao();
                   }
               }

               // Se for um ponto de entrada para um circuito paralelo aninhado
               if (ponto.getNomePino().equals("IN") && idDispositivo.startsWith("M")) { // M indica circuito paralelo
                   CircuitoParalelo cParaleloAninhado = circuitosEmParalelo.get(idDispositivo);
                   if (cParaleloAninhado != null) {
                       // Todos os ramos paralelos recebem a mesma tensão de entrada
                       for (CircuitoSerie subCircuito : cParaleloAninhado.getCircuitosEmSerie()) {
                           calcularTensoesNoCircuito(subCircuito.getId(), tensaoParaProximo);
                       }
                   }
               }
           }
       } else if (idCircuito.startsWith("M")) { // Circuito Paralelo
           CircuitoParalelo cParalelo = circuitosEmParalelo.get(idCircuito);
           if (cParalelo == null) return;

           // Cada ramo em um circuito paralelo recebe a mesma tensão de entrada
           for (CircuitoSerie ramo : cParalelo.getCircuitosEmSerie()) {
               calcularTensoesNoCircuito(ramo.getId(), tensaoEntradaAtual);
           }
       }
   }

   // Métodos auxiliares (obterTipoDispositivo, obterNumeroDispositivo, obterControlador, obterComponente)
   // seriam implementados para buscar as instâncias corretas dos dispositivos.
}

Na lógica reescrita de GerenciadorDeCircuitos.calcularTensoesNoCircuito, a função é recursiva e recebe a tensaoEntradaAtual para cada segmento ou ramo do circuito. Para circuitos em série, a tensaoParaProximo é atualizada sequencialmente: controladores recebem a tensão de entrada e sua tensão de saída se torna a entrada para o próximo. Componentes elétricos consomem tensão, e sua diferença de tensão é definida. Para circuitos paralelos, todos os ramos recebem a mesma tensaoEntradaAtual, garantindo a consistência. A estrutura da lógica foi ligeiramente modificada para ser mais explícita sobre como a tensão é passada adiante.

  1. Sugestões para Melhorias Futuras

4.1. Melhorias para o Sistema de Gestão de Provas

  • Validação de Entrada e Tratamento de Erros: Aprimorar a validação rigorosa de todos os dados de entrada (IDs, formatos, valores) e implementar um tratamento de exceções mais robusto com mensagens claras.
  • Estrutura do Código e Legibilidade: Refatorar o método principal em funções menores e mais focadas, usando nomes de variáveis e métodos mais descritivos para aumentar a clareza e manutenção.
  • Práticas Recomendadas para Coleções: Avaliar o uso de ConcurrentHashMap para cenários multithread (se aplicável) e LinkedHashMap para preservar a ordem de inserção onde for necessário.

4.2. Melhorias para o Sistema de Gerenciamento de Dispositivos Eletrônicos

4.2.1. Design Orientado a Objetos

  • Polimorfismo Aprimorado: Introduzir um método abstrato atualizarEstado ou reavaliarSaida na classe Dispositivo para que cada subclasse possa implementar sua lógica de atualização, reduzindo repetições no gerenciador.
  • Interfaces/Classes Abstratas para Controladores: Definir uma interface comum (e.g., IControlador) ou uma classe abstrata para todos os dispositivos de controle, padronizando a forma como suas tensões são atualizadas.

4.2.2. Padrões de Design

  • Padrão Observer: Implementar o padrão Observer para notificar automaticamente os dispositivos conectados a um controlador sempre que o estado ou a tensão de saída do controlador for alterado, evitando a necessidade de re-inicializações completas.
  • Padrão Comando: Utilizar o padrão Comando para encapsular as ações de controle (e.g., "ligar chave K1", "ajustar regulador F2 para a 2ª marcha") em objetos, permitindo a fácil extensão de novos comandos e a implementação de recursos como desfazer/refazer.

4.3. Melhorias para o Sistema de Gerenciamento de Circuitos Complexos

  • Tratamento Consistente de Tensão em Circuitos Paralelos: Assegurar que todos os ramos de um circuito paralelo recebam consistentemente a mesma tensão de entrada, revisando a lógica de propagação de tensão recursiva.
  • Refinamento da Lógica de Propagação de Tensão: Ajustar o método calcularTensoesNoCircuito para garantir que a tensão de saída de cada dispositivo seja corretamente calculada e passada como entrada para o próximo componente na sequência, considerando as quedas de tensão.
  • Atualização de Estado Após Controle: Confirmar que o método aplicarControles reinicializa ou atualiza de forma eficiente o estado de todos os dispositivos afetados por uma mudança de controle, e que o status final dos dispositivos (e.g., velocidade do ventilador) reflete corretamente essas mudanças.
  1. Conclusão Geral

Os exercícios do quarto ao sexto ciclo representaram uma jornada de aprendizado significativo em design e implementação de sistemas de software. O quinto exercício, em particular, foi uma demonstração da aplicação eficaz de princípios de Programação Orientada a Objetos, com um sistema de gerenciamento de dispositivos elétricos que empregou modularidade, herança e polimorfismo para lidar com diferentes tipos de componentes e seus controladores. A consideração de padrões como Fábrica e Estratégia, embora não explicitamente formalizados, esteve presente na forma como os dispositivos eram criados e seus comportamentos específicos eram definidos.

O sexto exercício aprofundou-se na complexidade, introduzindo o gerenciamento de circuitos elétricos complexos, incluindo configurações em série e paralelo. Embora o sistema tenha atendido aos requisitos básicos, desafios na propagação precisa de tensão e na atualização de estados de dispositivos em cenários complexos foram identificados. A estrutura de código exibiu elementos de padrões como Fábrica (na criação de dispositivos com base no tipo) e princípios de modularidade, preparando o terreno para uma aplicação mais formal de padrões de design, como Observer ou Command, para aumentar a manutenibilidade e a extensibilidade em futuras iterações.

Tags: Programação Orientada a Objetos Estruturas de Dados Circuitos Elétricos java design patterns

Publicado em 7-8 01:10