Princípios de Código Limpo em Java
Princípios de código limpo em Java: métodos pequenos, nomes significativos, responsabilidade única e mutação mínima.
Código limpo é aquele que a próxima pessoa — muitas vezes você mesmo, seis meses depois — consegue ler, confiar e modificar sem medo. O compilador aceita quase tudo; código limpo é sobre o outro público, os humanos que o mantêm. Nenhum dos princípios abaixo é uma invenção específica do Java, mas o Java oferece ferramentas concretas — métodos pequenos, campos final, records, exceções, tipos significativos — para aplicá-los. Este capítulo percorre os que trazem resultados todos os dias.
Nomes que revelam intenção
Um bom nome responde por que o valor existe, não apenas qual é o tipo. Se um nome precisa de um comentário para ser explicado, o nome está errado. Evite letras isoladas (exceto em laços curtos), evite abreviações que só você entende e prefira um nome descritivo mais longo a um nome curto e enigmático — sua IDE completa automaticamente de qualquer forma.
// Unclear: what is d? what unit? what is the magic 86400000?
int d = (t2 - t1) / 86400000;
// Clear: the names and a constant carry the meaning
long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
long elapsedDays = (endMillis - startMillis) / MILLIS_PER_DAY;Booleanos ficam melhores como perguntas ou estados: isActive, hasNext, shouldRetry. Métodos que fazem algo recebem nomes de verbos (calculateTotal); métodos que retornam algo recebem nomes de substantivos ou getters (total, getTotal). Para as convenções da linguagem por trás dessas escolhas, veja convenções de nomenclatura Java e boas práticas de nomenclatura Java.
Métodos pequenos com responsabilidade única
Um método deve fazer uma coisa em um único nível de abstração. Quando você se pega adicionando um comentário como // now validate the input, esse bloco geralmente quer se tornar seu próprio método bem nomeado. Métodos curtos são mais fáceis de nomear, testar e reutilizar — e o local de chamada lê como uma frase.
// Before: one method juggling validation, calculation, and formatting
String receipt(Order order) {
if (order == null || order.items().isEmpty())
throw new IllegalArgumentException("empty order");
int total = 0;
for (var i : order.items()) total += i.price() * i.qty();
return "Total: $" + total / 100 + "." + (total % 100);
}
// After: each step is a named method; receipt() now reads top-down
String receipt(Order order) {
requireNonEmpty(order);
int total = totalCents(order);
return formatCents(total);
}Cláusulas de guarda mantêm os métodos planos. Trate os casos extremos e retorne cedo em vez de envolver o caminho feliz em blocos if cada vez mais profundos.
Prefira imutabilidade e mutação mínima
Estado mutável compartilhado é a raiz da maioria dos bugs de concorrência e de muita confusão comum. Padronize com campos e variáveis locais final; só relaxe para mutável quando tiver um motivo. Os records do Java tornam objetos de valor imutáveis uma linha só, gerando um construtor canônico, equals, hashCode e toString.
// An immutable value object with validation in the compact constructor
record Money(long cents, String currency) {
Money {
if (cents < 0) throw new IllegalArgumentException("cents must be >= 0");
}
Money plus(Money other) { return new Money(cents + other.cents, currency); }
}Observe que plus retorna um novo Money em vez de mutar this. Objetos imutáveis são seguros para compartilhar entre threads, seguros para usar como chaves de mapa e impossíveis de corromper após a construção. Para se aprofundar, veja Java records, classes imutáveis e boas práticas de imutabilidade; a palavra-chave final cobre como bloquear campos e variáveis.
Falhe rápido e use exceções, não códigos de erro
Valide argumentos na fronteira e lance imediatamente quando algo estiver errado, para que a falha apareça perto de sua causa em vez de três camadas abaixo como uma confusa NullPointerException. Use exceções para sinalizar erros; não retorne null ou valores sentinela mágicos que todo chamador deve se lembrar de verificar.
| Padrão | Evitar | Preferir |
|---|---|---|
| Valor ausente | return null; | Optional<T> ou lançar exceção |
| Argumento inválido | return -1; | throw new IllegalArgumentException(...) |
| Estado impossível | padrão silencioso | throw new IllegalStateException(...) |
| Limpeza de recursos | finally manual | try-with-resources |
static User findUser(String id) {
Objects.requireNonNull(id, "id must not be null"); // fail fast
return repository.lookup(id)
.orElseThrow(() -> new NoSuchElementException("no user: " + id));
}Objects.requireNonNull transforma um NPE vago downstream em uma mensagem precisa no ponto de entrada. Para modelar "pode estar ausente" sem null, leia Java Optional; para projetar tipos de erro, veja boas práticas de exceções e exceções personalizadas.
DRY, mas sem over-abstrair
DRY (Don't Repeat Yourself) significa que uma única peça de conhecimento vive em um lugar. Quando a mesma constante ou cálculo aparece duas vezes, extraia-o. Mas resista ao erro oposto: dois trechos que apenas parecem iguais hoje podem divergir amanhã. Código duplicado é mais barato de corrigir do que a abstração errada. Extraia quando o significado é compartilhado, não apenas a sintaxe.
// Knowledge duplicated: the threshold lives in two places
if (order.total() >= 5000) freeShip = true; // here
if (cart.total() >= 5000) showBadge = true; // and here
// One source of truth
static final int FREE_SHIPPING_THRESHOLD_CENTS = 5000;
boolean qualifiesForFreeShipping(int totalCents) {
return totalCents >= FREE_SHIPPING_THRESHOLD_CENTS;
}Um exemplo prático: um carrinho de compras limpo
Este programa reúne os princípios em um arquivo pequeno e executável: um record LineItem imutável que se valida, métodos de propósito único com nomes que revelam intenção, constantes nomeadas em vez de números mágicos, uma cláusula de guarda e igualdade baseada em valor. Nenhum comentário é necessário para explicar o que faz — os nomes fazem isso.
O que tirar da execução:
- A saída lê exatamente como o domínio que modela —
2 x Notebook @ $12.50 = $25.00— porque todo método e campo é nomeado conforme seu propósito. Você não precisou de um único comentário para acompanhar o cálculo do carrinho; nomes que revelam intenção fizeram a documentação. - O subtotal é
$30.97e o frete é$5.99, não gratuito: a cláusula de guarda emshippingCentscomparou o subtotal com a constante nomeadaFREE_SHIPPING_THRESHOLD_CENTS(5000), e 3097 está abaixo disso. O número mágico vive em exatamente um lugar, então a regra é impossível de ficar inconsistente. value equality: trueprova que orecordnos deuequalspor valor gratuitamente — dois itensPenconstruídos separadamente são iguais porque seus dados são iguais. Escrever esse parequals/hashCodemanualmente é boilerplate que você não precisa mais manter.rejected bad item: quantity must be >= 0mostra o fail-fast em ação: o construtor compacto validou o argumento e lançouIllegalArgumentExceptionno momento da construção, para que uma quantidade-1nunca entre no sistema como dado inválido silencioso.- Cada auxiliar —
subtotalCents,shippingCents,formatCents— faz uma coisa, entãomainlê de cima para baixo como uma história. Métodos pequenos de responsabilidade única são o que tornam o programa inteiro legível em vez de uma parede de lógica aninhada.