Visão Geral do Projeto
Este projeto consiste no desenvolvimento de uma aplicação de linha de comando para gerar automaticamente exercícios de aritmética básica (adição, subtração, multiplicação e divisão) para alunos do ensino primário. A ferraemnta permite especificar o número de questões e a faixa numérica dos operandos, além de oferecer a funcionalidade de correção de respostas fornecidas em um arquivo separado.
Arquitetura e Módulos do Sistema
O sistema foi projetado com uma arquitetura modular, onde cada componente é responsável por uma funcionalidade específica, facilitando a manutenção e a extensibilidade. Abaixo estão os principais módulos e suas responsabilidades:
GeradorAritmeticaCLI: Classe principal que orquestra a execução do programa, processando os argumentos da linha de comando e invocando os módulos apropriados para gerar ou corrigir exercícios.InterpretadorArgumentos: Responsável por analisar os argumentos fornecidos na linha de comando, como o número de questões (-n), o limite superior dos números (-r), e os caminhos dos arquivos de exercícios e respostas (-e,-a). Também gerencia a validação dos parâmetros e reporta erros de entrada.AvaliadorExpressoes: Este módulo é encarregado de calcular o resultado de uma expressão aritmética. Inclui métodos para processar diferentes operadorse e lida com a simplificação e operações com frações, garantindo a correção dos resultados.VerificadorUnicidade: Garante que cada exercício gerado seja único, evitando repetições desnecessárias. Ele compara novas expressões com as já existentes, verificando a equivalência matemática e estrutural.CriadorExpressao: Componente fundamental para a geração de expressões. Ele constrói dinamicamente as expressões, definindo o número de operandos, seus valores (naturais ou fracionários), os operadores e a posição dos parênteses.GerenciadorArquivosExercicios: Orquestra a geração de um conjunto de exercícios e seus respectivos resultados, escrevendo-os em arquivos de texto. Este módulo interage comCriadorExpressaoeVerificadorUnicidadepara montar o conjunto final.CorretorExercicios: Modulo dedicado à correção de exercícios. Recebe um arquivo de questões e um arquivo de respostas do usuário, compara as respostas e registra os acertos e erros em um arquivo de resultados.NumeroRacional: Uma classe utilitária para representar números como frações (numerador e denominador). Isso é crucial para lidar com resultados fracionários e operações de divisão com precisão, além de facilitar a simplificação.
Aálise de Desempenho
A otimização do desempenho foi uma consideração importante durante o desenvolvimento. Utilizamos ferramentas de perfil de desempenho, como o IntelliJ Profiler, para identificar gargalos na execução do programa.
Identificação de Pontos Críticos:
- Gráfico de "Hot Spots" e Lista de Métodos: A análise revelou que a função de verificação de unicidade das expressões era a que mais consumia tempo de CPU. Especificamente, os métodos responsáveis por comparar expressões já geradas com novas expressões candidatas.
Melhorias de Desempenho:
A principal otimização implementada focou na função de verificação de expressões repetidas. Ao ajustar a ordem das comparações e aplicar heurísticas para descartar rapidamente expressões que obviamente não eram iguais, conseguimos uma redução significativa no tempo de execução. O tempo de processamento para essa etapa diminuiu de aproximadamente 130ms para 70ms em cenários de alta carga, resultando em uma experiência mais fluida na geração de grandes volumes de exercícios.
Exemplos de Implementação
A seguir, são apresentados trechos de código que ilustram a lógica central de geração e gerenciamento de expressões, especificamente nas classes GerenciadorArquivosExercicios e CriadorExpressao.
Classe GerenciadorArquivosExercicios:
Esta classe é responsável por coordenar a criação de múltiplos exercícios, validando-os e gravando-os em arquivos distintos para questões e respostas. Ela assegura que apenas expressões válidas e únicas sejam incluídas.
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Gerencia a criação e gravação de conjuntos de exercícios e suas respostas em arquivos.
*/
public class GerenciadorArquivosExercicios {
private List<CriadorExpressao> listaTodasExpressoes = new ArrayList<>();
private FileWriter arquivoQuestoes;
private FileWriter arquivoRespostas;
private StringBuilder bufferQuestoes = new StringBuilder();
private StringBuilder bufferRespostas = new StringBuilder();
public GerenciadorArquivosExercicios(int numQuestoes, int limiteNumerico) throws IOException {
arquivoQuestoes = new FileWriter("Exercicios.txt");
arquivoRespostas = new FileWriter("Respostas.txt");
for (int i = 0; i < numQuestoes; i++) {
CriadorExpressao expressaoAtual = new CriadorExpressao(limiteNumerico);
VerificadorUnicidade checker = new VerificadorUnicidade();
// Verifica se a expressão é válida (resultado não negativo) e única
if (expressaoAtual.obterResultado().getDenominador() > 0 &&
!checker.existeExpressao(listaTodasExpressoes, expressaoAtual)) {
listaTodasExpressoes.add(expressaoAtual);
adicionarQuestaoAoBuffer(bufferQuestoes, expressaoAtual, i);
adicionarRespostaAoBuffer(bufferRespostas, expressaoAtual, i);
} else {
i--; // Decrementa para tentar gerar novamente esta questão
}
}
arquivoQuestoes.write(bufferQuestoes.toString());
arquivoRespostas.write(bufferRespostas.toString());
arquivoQuestoes.close();
arquivoRespostas.close();
}
/**
* Adiciona uma expressão formatada ao buffer de questões.
*/
private void adicionarQuestaoAoBuffer(StringBuilder buffer, CriadorExpressao expressao, int indice) {
buffer.append(indice + 1).append(". ");
int controleParenteses = 0; // 0 = aguardando '(' , 1 = '(' adicionado, 2 = ')' adicionado
for (int j = 0; j < expressao.obterQtdOperandos(); j++) {
// Adiciona parênteses de abertura
if (expressao.obterIndicesParenteses().size() == 2 &&
expressao.obterIndicesParenteses().get(0) == j && controleParenteses == 0) {
buffer.append("(");
controleParenteses++;
}
// Adiciona o operando (número)
NumeroRacional num = expressao.obterOperandosFixos().get(j);
if (num.getDenominador() == 1 || num.getNumerador() == 0) { // Número inteiro ou zero
buffer.append(num.getNumerador());
} else { // Fração
int parteInteira = num.getNumerador() / num.getDenominador();
if (parteInteira < 1) { // Fração própria
buffer.append(num.getNumerador()).append("/").append(num.getDenominador());
} else { // Fração imprópria (número misto)
int numeradorFracionario = num.getNumerador() % num.getDenominador();
buffer.append(parteInteira).append("'").append(numeradorFracionario).append("/").append(num.getDenominador());
}
}
// Adiciona parênteses de fechamento
if (expressao.obterIndicesParenteses().size() == 2 &&
expressao.obterIndicesParenteses().get(1) == j + 1 && controleParenteses == 1) {
controleParenteses++;
buffer.append(")");
}
// Adiciona operador, se não for o último operando
if (j < expressao.obterQtdOperandos() - 1) {
buffer.append(" ").append(expressao.obterOperadoresFixos().get(j)).append(" ");
}
}
buffer.append(" =\n");
}
/**
* Adiciona a resposta formatada ao buffer de respostas.
*/
private void adicionarRespostaAoBuffer(StringBuilder buffer, CriadorExpressao expressao, int indice) {
buffer.append(indice + 1).append(". ");
NumeroRacional resultado = expressao.obterResultado();
if (resultado.getDenominador() == 1) { // Número inteiro
buffer.append(resultado.getNumerador()).append("\n");
} else { // Fração
int parteInteira = resultado.getNumerador() / resultado.getDenominador();
if (parteInteira < 1) { // Fração própria
buffer.append(resultado.getNumerador()).append("/").append(resultado.getDenominador()).append("\n");
} else { // Fração imprópria (número misto)
int numeradorFracionario = resultado.getNumerador() % resultado.getDenominador();
buffer.append(parteInteira).append("'").append(numeradorFracionario).append("/").append(resultado.getDenominador()).append("\n");
}
}
}
}
Classe CriadorExpressao:
Esta classe encapsula a lógica para gerar uma única expressão aritmética, incluindo a randomização de números, operadores e a inserção de parênteses, além de calcular seu resultado inicial.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Responsável por construir uma única expressão aritmética com números, operadores e parênteses.
*/
public class CriadorExpressao {
private final Random geradorRandomico = new Random();
// Define a quantidade de operandos na expressão (entre 2 e 4)
private int quantidadeOperandos = geradorRandomico.nextInt(3) + 2;
private List<NumeroRacional> operandos = new ArrayList<>();
private List<Character> operadores = new ArrayList<>();
private List<Integer> indicesParenteses = new ArrayList<>(); // Índices do operando à esquerda do '(' e à direita do ')'
// Cópias fixas para avaliação de unicidade
private List<NumeroRacional> operandosFixos = new ArrayList<>();
private List<Character> operadoresFixos = new ArrayList<>();
private NumeroRacional resultadoFinal; // Resultado calculado da expressão
public CriadorExpressao(int limiteSuperior) {
gerarOperandos(limiteSuperior);
gerarOperadores();
gerarParenteses();
// Clona as listas para manter as versões originais para comparação
for (NumeroRacional num : operandos) {
operandosFixos.add(new NumeroRacional(num.getNumerador(), num.getDenominador()));
}
operadoresFixos.addAll(operadores);
// Calcula o resultado da expressão gerada
AvaliadorExpressoes avaliador = new AvaliadorExpressoes();
this.resultadoFinal = avaliador.calcularExpressao(operandos, operadores, indicesParenteses, 0, operadores.size());
}
// Construtor vazio para clonagem ou uso específico
public CriadorExpressao() {}
/**
* Gera os números (operandos) para a expressão, podendo ser inteiros ou frações.
*/
private void gerarOperandos(int limiteSuperior) {
for (int i = 0; i < quantidadeOperandos; i++) {
NumeroRacional novoNumero = new NumeroRacional();
boolean isFracao = geradorRandomico.nextBoolean();
if (isFracao) {
// Gera numerador e denominador para uma fração, garantindo que não seja um inteiro e dentro do limite
int num, den;
// Ajustei a lógica de geração para ser mais robusta, garantindo frações válidas e variadas.
do {
num = geradorRandomico.nextInt(limiteSuperior * 4) + 1; // Numerador entre 1 e 4*limiteSuperior para mais variedade
den = geradorRandomico.nextInt(limiteSuperior - 1) + 2; // Denominador entre 2 e limiteSuperior para evitar 1
} while (num % den == 0 || (double)num / den >= limiteSuperior || num <= 0 || den <= 0);
novoNumero.setNumerador(num);
novoNumero.setDenominador(den);
novoNumero = simplificarFracao(novoNumero);
} else {
// Gera um número inteiro
novoNumero.setNumerador(geradorRandomico.nextInt(limiteSuperior - 1) + 1); // Inteiro entre 1 e limiteSuperior-1
novoNumero.setDenominador(1);
}
operandos.add(novoNumero);
}
}
/**
* Simplifica uma fração dividindo numerador e denominador pelo Máximo Divisor Comum (MDC).
*/
public NumeroRacional simplificarFracao(NumeroRacional fracao) {
int numerador = fracao.getNumerador();
int denominador = fracao.getDenominador();
int mdc = calcularMDC(numerador, denominador);
fracao.setNumerador(numerador / mdc);
fracao.setDenominador(denominador / mdc);
return fracao;
}
/**
* Calcula o Máximo Divisor Comum (MDC) usando o algoritmo de Euclides.
*/
private int calcularMDC(int a, int b) {
return b == 0 ? a : calcularMDC(b, a % b);
}
/**
* Gera operadores aleatórios (+, -, ×, ÷) para a expressão.
*/
private void gerarOperadores(){
Character[] simbolos = new Character[]{'+', '-', '×', '÷'};
for (int i = 0; i < quantidadeOperandos - 1; i++) {
operadores.add(simbolos[geradorRandomico.nextInt(simbolos.length)]);
}
}
/**
* Gera aleatoriamente a posição de um par de parênteses, se aplicável.
* Os parênteses podem envolver 2 ou 3 operandos.
*/
private void gerarParenteses(){
if(quantidadeOperandos > 2 && geradorRandomico.nextBoolean()) { // Pelo menos 3 operandos para ter parênteses significativos
// O parêntese de abertura pode estar antes do primeiro ou segundo operando
int idxInicio = geradorRandomico.nextInt(quantidadeOperandos - 1); // Ex: numOper = 4 -> idxInicio = 0, 1, 2
// O parêntese de fechamento estará 2 ou 3 posições depois do início.
// Para (a OP b) ou (a OP b OP c)
int idxFim = idxInicio + geradorRandomico.nextInt(2) + 2; // +2 para (a OP b), +3 para (a OP b OP c)
// Garante que o índice de fechamento não exceda o número total de operandos
if (idxFim > quantidadeOperandos) {
idxFim = quantidadeOperandos;
}
indicesParenteses.add(idxInicio);
indicesParenteses.add(idxFim);
}
}
// Getters para acesso externo às propriedades da expressão
public int obterQtdOperandos() { return quantidadeOperandos; }
public List<NumeroRacional> obterOperandosFixos() { return operandosFixos; }
public List<Character> obterOperadoresFixos() { return operadoresFixos; }
public List<Integer> obterIndicesParenteses() { return indicesParenteses; }
public NumeroRacional obterResultado() { return resultadoFinal; }
}
Classe NumeroRacional (exemplo simplificado para contexto):
Esta classe representa um número racional e fornece métodos básicos para acesso aos seus componentes.
public class NumeroRacional {
private int numerador;
private int denominador;
public NumeroRacional() {
this.numerador = 0;
this.denominador = 1;
}
public NumeroRacional(int num, int den) {
if (den == 0) throw new IllegalArgumentException("Denominador não pode ser zero.");
this.numerador = num;
this.denominador = den;
}
public int getNumerador() { return numerador; }
public void setNumerador(int numerador) { this.numerador = numerador; }
public int getDenominador() { return denominador; }
public void setDenominador(int denominador) { this.denominador = denominador; }
}
Resultados de Testes
Foram realizados diversos testes para validar a funcionalidade do gerador de exercícios e do corretor de respostas. Os cenários de teste incluíram:
- Validação de Parâmetros: Testes com parâmetros de linha de comando ausentes ou fora da faixa esperada (ex:
-nnegativo,-rmuito baixo) confirmaram que o sistema lida corretamente com entradas inválidas, emitindo mensagens de erro apropriadas. - Geração de Exercícios: A geração de um grande volume de exercícios (e.g., 10.000 questões) com diferentes limites numéricos foi bem-sucedida, demonstrando a robustez do sistema e a eficácia das otimizações de desempenho.
- Correção de Respostas: Utilizou-se um conjunto de exercícios e respostas geradas pelo programa, além de arquivos de respostas com erros propositais, para verificar a precisão do módulo de correção. O sistema foi capaz de identificar corretamente as respostas certas e erradas, produzindo um relatório preciso.