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.
- 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.
- 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 deDispositivoControle, implementando um interruptor simples com estados ligado/desligado.ReguladorEscalonado: Deriva deDispositivoControle, para controle de velocidade em passos (0 a 3).ReguladorContinuo: Deriva deDispositivoControle, 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 deComponenteEletrico, 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
DispositivoControlee subclasses (ChaveControle,ReguladorEscalonado,ReguladorContinuo): Como no exercício 5, mas com refinamentos.ComponenteEletricoe 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.CircuitoSerieeCircuitoParalelo: 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, utilizandoTreeMappara 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:
TreeMapgarantia 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.
- 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.
- 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
ConcurrentHashMappara cenários multithread (se aplicável) eLinkedHashMappara 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
atualizarEstadooureavaliarSaidana classeDispositivopara 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
calcularTensoesNoCircuitopara 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
aplicarControlesreinicializa 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.
- 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.