image

Bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image
Jonathan Costa
Jonathan Costa21/10/2025 15:38
Compartilhe

Boas práticas em Java: imutabilidade, SOLID e testes que realmente protegem seu código

  • #Java

Como aplicar as boas práticas de imutabilidade, SOLID e testes em Java

Este artigo direto ao ponto mostra como aplicar imutabilidade, princípios SOLID e testes eficazes para escrever código Java mais legível, seguro e fácil de evoluir. Inclui exemplos práticos com record, Streams, Optional, JUnit 5 e AssertJ, além de um checklist final para revisão técnica.

1) Por que falar de boas práticas em “Fundamentos de Java”?

Fundamentos não são só sintaxe. São hábitos que reduzem bug, melhoram a legibilidade e aceleram a evolução do sistema. Três pilares práticos que elevam o nível do seu Java:

  • Imutabilidade: evita estados inesperados e facilita testes.
  • SOLID: organiza responsabilidades e reduz acoplamento.
  • Testes eficientes: validam regra de negócio e previnem regressões.

2) Imutabilidade: previsibilidade que paga dividendos

Quando aplicar: DTOs, Value Objects (moeda, CPF/CNPJ, e-mail), configurações, respostas de API.

2.1 Exemplo com record (Java 16+)

public record Dinheiro(long centavos, String moeda) {

  public Dinheiro {
      if (centavos < 0) throw new IllegalArgumentException("Valor negativo");
      if (moeda == null || moeda.isBlank()) throw new IllegalArgumentException("Moeda inválida");
  }

  public Dinheiro somar(Dinheiro outro) {
      if (!this.moeda.equals(outro.moeda)) {
          throw new IllegalArgumentException("Moedas diferentes");
      }
      return new Dinheiro(this.centavos + outro.centavos, this.moeda);
  }
}

2.2 Se você está no Java 8

  • Campos private final, sem setters.
  • Validação no construtor.
  • Implemente equals/hashCode/toString.
  • Ao expor coleções, use cópias imutáveis:
public List<Item> itens() {
  return Collections.unmodifiableList(this.itens);
}

Benefícios práticos: menos bugs de concorrência, objetos fáceis de testar, APIs mais previsíveis.

3) SOLID sem dogma (com exemplos mínimos e úteis)

S — Single Responsibility (uma razão para mudar)

Separar persistência, regra de negócio e integração reduz acoplamento.

class FaturaService {
  private final FaturaRepository repo;
  private final PoliticaCobranca politica;

  public FaturaService(FaturaRepository repo, PoliticaCobranca politica) {
      this.repo = repo;
      this.politica = politica;
  }

  public void processar(Fatura f) {
      politica.aplicar(f);
      repo.salvar(f);
  }
}

O — Open/Closed (aberto para extensão, fechado para modificação)

Novas regras sem tocar no núcleo.

interface RegraDesconto { BigDecimal aplicar(BigDecimal valor); }

class DescontoBlackFriday implements RegraDesconto { /* ... */ }
class DescontoAniversario implements RegraDesconto { /* ... */ }

class CalculadoraDescontos {
  private final List<RegraDesconto> regras;

  public CalculadoraDescontos(List<RegraDesconto> regras) {
      this.regras = regras;
  }

  public BigDecimal calcular(BigDecimal valor) {
      for (var r : regras) valor = r.aplicar(valor);
      return valor;
  }
}

L — Liskov Substitution (herdar sem quebrar contrato)

Se a subclasse precisa “desligar” comportamentos da superclasse, prefira composição.

I — Interface Segregation (interfaces pequenas)

Evite interfaces com métodos que clientes não usam. Crie interfaces coesas.

D — Dependency Inversion (dependa de abstrações)

Injete interfaces, não implementações. Isso reduz acoplamento e viabiliza testes.

4) Coleções, Streams e Optional sem armadilhas

4.1 Streams: expressividade com clareza

  • Pipelines curtos e puros (sem efeitos colaterais).
  • collect(toList()) > construir manualmente com forEach.
  • Evite parallelStream() sem medir ganho real.
List<Pedido> topAprovados = pedidos.stream()
  .filter(Pedido::aprovado)
  .sorted(Comparator.comparing(Pedido::data).reversed())
  .limit(10)
  .toList();

groupingBy e partitioningBy:

Map<Status, List<Pedido>> porStatus = pedidos.stream()
  .collect(Collectors.groupingBy(Pedido::status));

Map<Boolean, List<Pedido>> atrasados = pedidos.stream()
  .collect(Collectors.partitioningBy(Pedido::estaAtrasado));

4.2 Optional: sem null surpresa

  • Use como retorno; evite em campos.
  • Prefira orElse, orElseGet, orElseThrow a get().
Cliente cliente = repo.buscarPorId(id)
  .orElseThrow(() -> new NaoEncontradoException("Cliente " + id));

5) Exceções e logs que ajudam a resolver o problema

  • Checked: chamador pode se recuperar (I/O, rede).
  • Unchecked: erro de programação/validação (ex.: IllegalArgumentException).
  • Não engula exceções. Logue contexto e decida (propagar, retry, fallback).
public class SaldoInsuficienteException extends RuntimeException {
  public SaldoInsuficienteException(String conta, BigDecimal tentativa, BigDecimal saldo) {
      super("Saldo insuficiente na conta " + conta +
            " (tentativa=" + tentativa + ", saldo=" + saldo + ")");
  }
}

try {
  pagamentoService.executar(pedido);
} catch (GatewayIndisponivelException e) {
  log.warn("Gateway indisponível no pagamento do pedido {}. Tentando fallback.", pedido.getId(), e);
  // fallback...
}

Boas práticas de log: níveis corretos (info/warn/error), sem dados sensíveis, mensagens acionáveis.

6) Testes que realmente protegem (JUnit 5 + AssertJ)

Pirâmide saudável: muitos testes de unidade, menos de integração, poucos E2E.

6.1 Exemplo de teste de unidade (regra de negócio)

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

class DinheiroTest {

  @Test
  void soma_mesma_moeda() {
      Dinheiro a = new Dinheiro(100, "BRL");
      Dinheiro b = new Dinheiro(50, "BRL");

      Dinheiro r = a.somar(b);

      assertThat(r.centavos()).isEqualTo(150);
      assertThat(r.moeda()).isEqualTo("BRL");
  }

  @Test
  void soma_moedas_diferentes_dispara_erro() {
      var a = new Dinheiro(100, "BRL");
      var b = new Dinheiro(50, "USD");

      assertThatThrownBy(() -> a.somar(b))
          .isInstanceOf(IllegalArgumentException.class)
          .hasMessageContaining("Moedas diferentes");
  }
}

6.2 Dicas de produtividade

  • Nomes descritivos: deve_calcular_desconto_progressivo.
  • Test Doubles (mocks/fakes) só quando há dependências externas.
  • Cobertura é métrica, não objetivo: foque ramos críticos do domínio.
  • Corrigiu bug? Escreva um teste que o reproduza.

7) Qualidade contínua no dia a dia

  • Formatação e lint (Spotless/Checkstyle) no build.
  • Qualidade em PR (Sonar, regras simples e claras).
  • Métricas úteis: complexidade ciclomática, duplicação, acoplamento.
  • Exceções às regras só com justificativa explícita (comentário técnico).

8) Erros comuns (e como evitar)

  1. Objetos mutáveis “escapando”: expondo listas/arrays mutáveis → retorne cópias imutáveis.
  2. Streams gigantes: quebre em métodos nomeados para manter legibilidade.
  3. Optional mal usado: chamar get() sem checar presença → use orElseThrow.
  4. parallelStream() por “achismo”: meça; cuidado com thread pools disputadas.
  5. Exceções genéricas: mensagens fracas e sem contexto atrapalham diagnóstico.
  6. Testes frágeis: acoplados a detalhes de implementação, nomes ou ordenações não determinísticas.

9) Checklist de revisão (cole no fim do seu PR)

  • Classes com uma responsabilidade clara
  • Dependências via interfaces + injeção
  • Objetos de valor imutáveis; coleções expostas de forma segura
  • Exceções informativas (com contexto) e logs nos níveis corretos
  • Streams curtas, puras e legíveis (sem efeitos colaterais)
  • Optional apenas como retorno; sem null oculto
  • Testes de unidade cobrindo regras críticas e cenários de erro
  • Ferramentas de qualidade no pipeline (formatador, linter, análise estática)
  • Nomes de classes/métodos/variáveis expressivos; comentários só onde agregam

Conclusão

Boas práticas não são burocracia: são atalhos para código confiável. Comece pequeno hoje:

  1. torne um Value Object imutável;
  2. extraia uma regra para uma interface (SOLID);
  3. escreva um teste que impediria um bug real que você já viu.

Repita esse ciclo nos próximos PRs. Seu futuro “eu” — e seu time — vão agradecer.

Compartilhe
Recomendados para você
PcD Tech Bradesco - Java & QA Developer
Riachuelo - Primeiros Passos com Java
GFT Start #7 - Java
Comentários (1)
DIO Community
DIO Community - 21/10/2025 16:16

Fantástico, Jonathan! Seu artigo é um guia prático de altíssimo nível, indo direto ao que realmente importa: hábitos que transformam sintaxe em software de qualidade. A forma como você usa record, Optional, Streams e AssertJ para ilustrar os conceitos é cirúrgica e imediatamente aplicável para qualquer desenvolvedor Java, do iniciante ao sênior.

Você tem toda razão: Fundamentos não são só sintaxe; são os hábitos que reduzem bugs.

Qual você diria que é o maior desafio para um desenvolvedor ao gerenciar o grafo de dependências em uma aplicação Spring Boot de grande porte, em termos de tempo de inicialização e de configurações que quebram em tempo de execução (como dependências circulares), mesmo com o DIP sendo aplicado corretamente?