Esta postagem é um exercício. Aqui, vou desenvolver um código para o cálculo de aprovação de alunos em uma disciplina. Para isso, vou adotar uma premissa muito rígida: resolver exclusivamente o problema que está sendo apresentado da maneira mais direta possível. Não vou usar conceitos de OOP, abstrações, boas práticas, ou recursos mais avançados da linguagem Java. O negócio vai ser mover o CARD para DONE.
Atenção! A leitura deste texto faz muito mais sentido se você ler também a parte 2, com o mesmo problema, mas implementando usando OOP e SOLID. Provas, Recuperação, Final… não OOP & não SOLID # Parte 1. Mais ainda, seria legal se você se permitisse comparar a solução proposta aqui, com a solução final da outra postagem.
Primeiro problema
O sistema deve receber três notas de um aluno de três avaliações aplicadas durante um semestre letivo em uma disciplina de curso superior. O sistema deve informar se o aluno está aprovado ou reprovado, considerando esse conjunto de notas. As notas são valores numéricos que podem ir de 0 até 10. O critério para aprovar é ter média simples maior ou igual a 7.0.
A implementação aqui é fácil. É só tirar a média e colocar um if.
public class GradingCalculator {
public boolean isApproved(
double exam1,
double exam2,
double exam3
) {
if ((exam1 < 0.0 || exam1 > 10.0)
|| (exam2 < 0.0 || exam2 > 10.0)
|| (exam3 < 0.0 || exam3 > 10.0)) {
throw new IllegalArgumentException("Wrong exam values");
}
if ((exam1 + exam2 + exam3) / 3.0 < 7.0) {
return false;
}
return true;
}
}
Primeiro acréscimo
O sistema agora deve considerar a nota da reposição. A reposição é uma prova extra que o aluno faz caso sua média das três primeiras provas não seja suficiente para aprovação. Nesse caso, a prova da reposição deve substituir a menor nota entre as três avaliações. Se a nota da reposição for ainda menor, ela não deve ser considerada.
Boa idea. Dar a chance pro aluno refazer uma das provas é justo. Nem todo mundo está bem todo tempo e a ideia de avaliação contínua me fazer muito sentido. Considerar a nota da reposição complicou um pouco, mas eu conheço uma técnica de implementação que vai ferrar o próximo programador a dar manutenção nisso.
public class GradingCalculator {
public boolean isApproved(
double exam1,
double exam2,
double exam3,
double replacement
) {
if ((exam1 < 0.0 || exam1 > 10.0)
|| (exam2 < 0.0 || exam2 > 10.0)
|| (exam3 < 0.0 || exam3 > 10.0)
|| (replacement < 0.0 || replacement > 10.0)) {
throw new IllegalArgumentException("Wrong exam values");
}
if ((exam1 + exam2 + exam3) / 3.0 < 7.0) {
double sum = exam1 + exam2 + exam3
- Math.min(exam1, Math.min(exam2, exam3))
+ replacement;
if (sum / 3.0 < 7.0) {
return false;
}
}
return true;
}
}
Um pouco mais de complexidade
O sistema deve permitir que cada avaliação, incluindo a reposição, seja composta por um conjunto de atividades. A nota de avaliação é a média simples das notas das atividades, com a nota das atividades também indo de 0 até 10.
O negócio aqui é mover a tarefa para DONE. O Go Horse canta! Sem firula. Se cada avaliação é composta por várias atividades, nada melhor que um array para cada. Tira a média de tudo e vida que segue. Que merda é essa com Math.min que colocaram aqui? Próxima!
public class GradingCalculator {
public boolean isApproved(
double exam1[],
double exam2[],
double exam3[],
double replacement[]
) {
validadeValues(exam1);
validadeValues(exam2);
validadeValues(exam3);
validadeValues(replacement);
double exam1Avg = calculateAverage(exam1);
double exam2Avg = calculateAverage(exam2);
double exam3Avg = calculateAverage(exam3);
double replacementAvg = calculateAverage(replacement);
if ((exam1Avg + exam2Avg + exam3Avg) / 3.0 < 7.0) {
double sum = exam1Avg + exam2Avg + exam3Avg
- Math.min(exam1Avg, Math.min(exam2Avg, exam3Avg))
+ replacementAvg;
if (sum / 3.0 < 7.0) {
return false;
}
}
return true;
}
private double calculateAverage(double values[]) {
double sum = 0.0;
for (double x : values) {
sum += x;
}
return sum / values.length;
}
private void validadeValues(double values[]) {
for (double x : values) {
if (x < 0.0 || x > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
}
}
}
Dava para ter pensando nisso antes?
As atividades realizadas dentro de uma avaliação podem ter um peso variável na composição da nota da avaliação. O peso de uma atividade é um valor que pode variar de 1 a 10. A nota da avaliação deve ser a média ponderada das notas de todas as atividades.
Qual é a maneira mais rápida de fazer isso? Tem um monte de opção, mas nenhuma delas envolve criar uma classe para representar uma tarefa e seu peso. O negócio é mandar dois arrays cada avaliação, um com as notas da satividades, e outro com os pesos. A pessoa que fez isso recebeu a tarefa numa segunda-feira de manhã e terminou antes do meio dia, mas levou o assunto até sexta-feira, passando status que faltava só testar. Ambos líder técnico e gestor de tecnologia da equipe pensaram consigo que a tarefa parecia estar levando mais tempo que o necessário.
public class GradingCalculator {
public boolean isApproved(
double exam1[],
double exam1Weights[],
double exam2[],
double exam2Weights[],
double exam3[],
double exam3Weights[],
double replacement[],
double replacementWeights[]
) {
validadeValues(exam1);
validadeValues(exam1Weights);
validadeValues(exam2);
validadeValues(exam2Weights);
validadeValues(exam3);
validadeValues(exam3Weights);
validadeValues(replacement);
validadeValues(replacementWeights);
double exam1Avg = calculateAverage(exam1, exam1Weights);
double exam2Avg = calculateAverage(exam2, exam2Weights);
double exam3Avg = calculateAverage(exam3, exam3Weights);
double replacementAvg = calculateAverage(replacement, replacementWeights);
if ((exam1Avg + exam2Avg + exam3Avg) / 3.0 < 7.0) {
double sum = exam1Avg + exam2Avg + exam3Avg
- Math.min(exam1Avg, Math.min(exam2Avg, exam3Avg))
+ replacementAvg;
if (sum / 3.0 < 7.0) {
return false;
}
}
return true;
}
private double calculateAverage(
double values[],
double wights[]
) {
double sum = 0.0;
double sum2 = 0.0;
for (int i = 0; i < values.length; i++) {
sum += values[i] * wights[i];
sum2 += wights[i];
}
return sum / sum2;
}
private void validadeValues(double values[]) {
for (double x : values) {
if (x < 0.0 || x > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
}
}
}
A última chance
Caso o aluno não seja aprovado após a reposição, ele pode fazer uma prova final, com nota variando de 0 a 10. A prova final é única, não sendo composta por múltiplas atividades. Suponhamos que N seja a média das três primeiras avaliações (possivelmente considerando também a prova de reposição), e vamos supor que F seja a nota da prova final, o critério para aprovação neste caso é a média simples entre N e F, cujo valor deve ser maior ou igual a 6.0.
Recebi essa tarefa desdém, porque tenho certeza que do ponto de vista pedagógico é muito difícil alguém ser aprovado em uma avaliação com todo conteúdo do semestre, mesmo depois de ter feito quatro provas e obter notas ruins. Com meus botões, penso que esta funcionalidade vale mais para o professor descobrir aqueles alunos que, por alguma razão, não conseguiram média, mas estão se dedicando de alguma forma. Talvez o professor ajude de alguma forma, afinal de contas, em muitos casos, é prerrogativa dele a forma de avaliação.
public class GradingCalculator {
public boolean isApproved(
double exam1[],
double exam1Weights[],
double exam2[],
double exam2Weights[],
double exam3[],
double exam3Weights[],
double replacement[],
double replacementWeights[],
double finalExam
) {
validadeValues(exam1);
validadeValues(exam1Weights);
validadeValues(exam2);
validadeValues(exam2Weights);
validadeValues(exam3);
validadeValues(exam3Weights);
validadeValues(replacement);
validadeValues(replacementWeights);
if (finalExam < 0.0 || finalExam > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
double exam1Avg = calculateAverage(exam1, exam1Weights);
double exam2Avg = calculateAverage(exam2, exam2Weights);
double exam3Avg = calculateAverage(exam3, exam3Weights);
double replacementAvg = calculateAverage(replacement, replacementWeights);
if ((exam1Avg + exam2Avg + exam3Avg) / 3.0 < 7.0) {
double sum = exam1Avg + exam2Avg + exam3Avg
- Math.min(exam1Avg, Math.min(exam2Avg, exam3Avg))
+ replacementAvg;
if (sum / 3.0 < 7.0) {
if (((sum / 3.0) + finalExam) / 2.0 < 6.0) {
return false;
}
}
}
return true;
}
private double calculateAverage(
double values[],
double wights[]
) {
double sum = 0.0;
double sum2 = 0.0;
for (int i = 0; i < values.length; i++) {
sum += values[i] * wights[i];
sum2 += wights[i];
}
return sum / sum2;
}
private void validadeValues(double values[]) {
for (double x : values) {
if (x < 0.0 || x > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
}
}
}
Isso é bom para relatórios
O sistema deve ser incrementado para dizer em quais condições o aluno foi aprovado, ou reprovado, isto é, se foi aprovado por média sem reposição, se foi aprovado com prova de reposição, se foi aprovado com a prova final, ou foi reprovado.
Desde a primeira linha de código muitos meses atrás, a pessoa que começou esse sistema deveria ter pensando nessa situação. Era melhor usar números como resultado, mas quem usou o boolean deve ter pensado no cenário da época, já que função deveria descobrir apenas se o aluno será aprovado ou reprovado.
public class GradingCalculator {
public int isApproved(
double exam1[],
double exam1Weights[],
double exam2[],
double exam2Weights[],
double exam3[],
double exam3Weights[],
double replacement[],
double replacementWeights[],
double finalExam
) {
validadeValues(exam1);
validadeValues(exam1Weights);
validadeValues(exam2);
validadeValues(exam2Weights);
validadeValues(exam3);
validadeValues(exam3Weights);
validadeValues(replacement);
validadeValues(replacementWeights);
if (finalExam < 0.0 || finalExam > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
double exam1Avg = calculateAverage(exam1, exam1Weights);
double exam2Avg = calculateAverage(exam2, exam2Weights);
double exam3Avg = calculateAverage(exam3, exam3Weights);
double replacementAvg = calculateAverage(replacement, replacementWeights);
if ((exam1Avg + exam2Avg + exam3Avg) / 3.0 < 7.0) {
double sum = exam1Avg + exam2Avg + exam3Avg
- Math.min(exam1Avg, Math.min(exam2Avg, exam3Avg))
+ replacementAvg;
if (sum / 3.0 < 7.0) {
if (((sum / 3.0) + finalExam) / 2.0 < 6.0) {
return 4;
} else {
return 2;
}
} else {
return 1;
}
}
return 0;
}
private double calculateAverage(
double values[],
double wights[]
) {
double sum = 0.0;
double sum2 = 0.0;
for (int i = 0; i < values.length; i++) {
sum += values[i] * wights[i];
sum2 += wights[i];
}
return sum / sum2;
}
private void validadeValues(double values[]) {
for (double x : values) {
if (x < 0.0 || x > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
}
}
}
Não é só nota, é envolvimento
O sistema deve considerar a frequência do aluno. Um aluno não será reprovado por faltas quando tiver uma frequência igual ou superior a 75% das aulas. A reprovação por faltas deve acontecer mesmo se o aluno obtiver média para aprovação. Nestes casos, o sistema deve indicar também se a reprovação foi por falta, no caso dele alcançar média para aprovação, ou por falta e média, nos caso dele não conseguir média para aprovação, e também não apresentar a frequência necessária.
Implementei essa muito descontente. Não é justo que o aluno seja reprovado mesmo tirando nota 10 em todas as avaliações. E se o aluno trabalha e estuda, se ele tem família e filhos?
public class GradingCalculator {
public int isApproved(
double exam1[],
double exam1Weights[],
double exam2[],
double exam2Weights[],
double exam3[],
double exam3Weights[],
double replacement[],
double replacementWeights[],
double finalExam,
int totalClasses,
int frequency
) {
validadeValues(exam1);
validadeValues(exam1Weights);
validadeValues(exam2);
validadeValues(exam2Weights);
validadeValues(exam3);
validadeValues(exam3Weights);
validadeValues(replacement);
validadeValues(replacementWeights);
if (finalExam < 0.0 || finalExam > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
if (totalClasses <= 0) {
throw new IllegalArgumentException("Wrong value");
}
if (frequency < 0 || frequency > totalClasses) {
throw new IllegalArgumentException("Wrong value");
}
double exam1Avg = calculateAverage(exam1, exam1Weights);
double exam2Avg = calculateAverage(exam2, exam2Weights);
double exam3Avg = calculateAverage(exam3, exam3Weights);
double replacementAvg = calculateAverage(replacement, replacementWeights);
boolean frequencyApproved = ((double) frequency / totalClasses) >= 0.75;
if ((exam1Avg + exam2Avg + exam3Avg) / 3.0 < 7.0) {
double sum = exam1Avg + exam2Avg + exam3Avg
- Math.min(exam1Avg, Math.min(exam2Avg, exam3Avg))
+ replacementAvg;
if (sum / 3.0 < 7.0) {
if (((sum / 3.0) + finalExam) / 2.0 < 6.0) {
return frequencyApproved ? 4 : 6;
} else {
return frequencyApproved ? 2 : 5;
}
} else {
return frequencyApproved ? 1 : 5;
}
}
return frequencyApproved ? 0 : 5;
}
private double calculateAverage(
double values[],
double wights[]
) {
double sum = 0.0;
double sum2 = 0.0;
for (int i = 0; i < values.length; i++) {
sum += values[i] * wights[i];
sum2 += wights[i];
}
return sum / sum2;
}
private void validadeValues(double values[]) {
for (double x : values) {
if (x < 0.0 || x > 10.0) {
throw new IllegalArgumentException("Wrong exam values");
}
}
}
}
Conclusão
Sem dúvida, a solução final atende a todos os requisitos apresentados. A questão é: qual foi o custo dessa escolha? O sistema foi construído considerando um curso superior com três avaliações, uma prova de reposição e uma prova final. Mas e se amanhã ele precisar atender a um modelo diferente, como o ensino básico, com avaliações bimestrais, segunda chamada, recuperação paralela e regras distintas de frequência? A cada nova necessidade, vamos ampliar ainda mais a complexidade do mesmo método? Responder tudo isso é impossível, sobretudo quando estamos codificando para resolver um problema em específico, adicionar uma funcionadade bem definida. Mas seguir boas práticas garante que essas alterações sejam mais fáceis de fazer, e também permitem que a manutenção do próprio sistema seja mais simples.
A função central concentra toda a lógica e retorna um número inteiro. O valor zero representa aprovação. Isso é evidente para quem escreveu o código ou acompanhou sua evolução. Porém, para alguém que não conhece esse contexto, o número zero não comunica absolutamente nada. Ele é apenas um número. O significado está implícito, escondido na implementação. Um valor isolado não carrega intenção, não expressa domínio e não revela regra de negócio.
Esse problema se agrava quando o sistema evolui. Em determinado momento, o retorno deixa de ser um boolean e passa a ser um int. Todas as partes que dependiam apenas de uma informação simples, aprovado ou reprovado, agora precisam interpretar códigos numéricos. O que antes era um simples if (aprovado) passa a ser uma verificação como if (resultado == 0 || resultado == 1) ou if (resultado != 3). Componentes que não deveriam conhecer detalhes internos da regra passam a depender deles. O conhecimento da regra se espalha pelo sistema de forma frágil e implícita.
Nada disso é artificial ou exagerado. Pelo contrário, esse tipo de solução é comum no mercado. Pressão por prazo, foco exclusivo na entrega imediata e ausência de modelagem explícita levam a decisões pragmáticas que resolvem o problema do momento. O sistema funciona hoje. O requisito é entregue. O CARD é movido para DONE. No entanto, cada atalho gera um custo acumulado que será pago no futuro.
O método responsável pelo cálculo começou simples: três notas e uma média. Com o tempo, foram sendo adicionadas novas regras, substituição da menor nota, prova final, controle de frequência, médias ponderadas, códigos de retorno. Todas essas responsabilidades foram incorporadas ao mesmo ponto do sistema. O resultado é um método extenso, repleto de condicionais aninhadas e fluxos de decisão difíceis de acompanhar. Ler o código exige esforço. Alterá-lo exige cautela. Entendê-lo exige contexto prévio.
O problema central não é apenas técnico; é semântico. O retorno inteiro representa estados importantes do negócio, mas não comunica significado por si só. O valor 0 só significa “aprovado por média” porque alguém sabe disso. Fora desse conhecimento tácito, ele é apenas um número mágico. O mesmo vale para os demais códigos. As regras não estão modeladas de forma explícita; estão escondidas em decisões condicionais. Isso cria acoplamento implícito e dependência de memória humana.
Além disso, a capacidade de evolução fica comprometida. Se surgirem novas regras, como média mínima por avaliação, pesos diferentes por período, dependência automática, critérios diferenciados por curso, a tendência será continuar expandindo o mesmo método. Mais parâmetros. Mais condicionais. Mais ramificações. O código cresce até se tornar rígido e arriscado de modificar. Cada alteração aumenta a chance de falha.
A reflexão principal é que elegância não é um luxo acadêmico. Não se trata de aplicar padrões por formalidade ou seguir princípios por dogma. Trata-se de estruturar o sistema de forma que ele possa evoluir sem colapsar. É possível evoluir incrementalmente e, ainda assim, organizar responsabilidades, explicitar regras de negócio e comunicar intenção com clareza.
Este exercício demonstra que é possível construir uma solução direta, pragmática e funcional, mesmo que estruturalmente frágil. Ela atende aos requisitos e resolve o problema atual. A pergunta relevante não é se funciona, porque funciona, sim. A pergunta é outra: você gostaria de manter esse código pelos próximos cinco anos?