"Código de qualidade não é escrito, é lapidado; bons hábitos não são aprendidos, são forjados em meio a tropeços."
Ao longo dos anos desenvolvendo software, acumulei uma boa quantidade de experiências e cometi diversos erros. Refletindo sobre isso, percebo que a maioria dos problemas não estava relacionada à falha na implementação de funcionalidades, mas sim em aspectos como robustez e manutenibilidade do código, áreas que na época recebia pouca atenção.
Este artigo é uma compilação dessas experiências. Não me atrevo a chamá-lo de metodologia, mas sim de um registro organizado de anos de prática, na esperança de auxiliar outros desenvolvedores que trilharam caminhos semelhantes, permitindo que evitem alguns obstáculos e acelerem seu crescimento profissional.
O Que Este Artigo Não É
- Não é um tutorial de Padrões de Projeto: Não abordarei em detalhes a implementação das 23 State of the Art Patterns.
- Não é um manual de codificação: Não entrarei em debates sobre convenções de nomenclatura como camelCase ou snake_case.
- Não é um guia de uso de frameworks: Não ensinarei como utilizar Spring, MyBatis, etc.
- É sim: Uma coleção de experiências reais, reflexões sobre hábitos de codificação e insights de práticas de engenharia.
Para Quem Se Destina
- Desenvolvedores Java com 1 a 5 anos de experiência.
- Profissionais que buscam aprimorar a qualidade do código.
- Desenvolvedores que frequentemente enfrentam problemas e desejam evitá-los.
- Aqueles que aspiram a transitar de "saber codificar" para "codificar bem".
Como Ler Este Artigo
- Leitura Completa: Leia do início ao fim para obter uma visão holística.
- Leitura Seletiva: Navegue diretamente para os capítulos de seu interesse.
- Consulta Rápida: Utilize-o como referência ao deparar-se com desafios específicos.
Capítulo 1: Pensamento de Design — A "Via" por Trás do Código
"Um bom design minimiza o custo de mudanças."
1.1 O Que Define um Bom Design?
Muitos desenvolvedores questionam: "Que tipo de design é considerado bom?". Minha perspectiva é: um bom design é aquele que minimiza o custo de modificação quando o código precisa se adaptar a mudanças.
Não existe uma solução universalmente perfeita. O design é uma arte de equilíbrio, buscando a harmonia entre as demandas atuais e as potenciais evoluções futuras.
Critérios para um Bom Design:
| Critério | Significado | Oposto |
|---|---|---|
| Manutenibilidade | Pequeno escopo de impacto ao corrigir bugs ou adicionar funcionalidades. | Alterar um ponto causa problemas em múltiplos locais. |
| Extensibilidade | Adicionar novas funcionalidades sem modificar o código existente. | Cada nova funcionalidade exige grandes refatorações. |
| Reutilização | Código escrito pode ser utilizado em outros contextos. | Necessidade de reescrever código repetidamente. |
| Legibilidade | Código de fácil compreensão, sem necessidade de adivinhação. | Código enigmático, compreensível apenas pelo autor. |
Design Excessivo vs. Design Insuficiente:
Design Insuficiente ←————————————————————→ Design Excessivo
↓ ↓
Código caótico, difícil de manter Código complexo, abstração exagerada
↓ ↓
Alto custo de refatoração Baixa eficiência de desenvolvimento
O Segredo para Encontrar o Equilíbrio:
- Princípio YAGNI (You Ain't Gonna Need It): Não crie funcionalidades ou abstrações para necessidades futuras incertas.
- Regra dos Três: Repita uma tarefa três vezes antes de considerar a abstração.
- Design Evolutivo: Comece com o código funcionando e, gradualmente, otimize-o.
Hábito a Cultivar: Antes de escrever código, reflita sobre como o design atual se comportaria diante de futuras mudanças.
1.2 Princípios SOLID: A Pedra Angular do Design Orientado a Objetos
Os princípios SOLID são os cinco pilares do design orientado a objetos, fundamentais para um bom design. Em vez de focar na implementação detalhada, o objetivo aqui é elucidar o "porquê" de sua adesão.
Princípio da Responsabilidade Única (SRP - Single Responsibility Principle)
Ideia Central: Uma classe deve ter um único motivo para mudar, ou seja, uma única responsabilidade.
Por Que Seguir:
- Responsabilidades mais singulares resultam em classes mais simples.
- Modificar uma responsabilidade não afeta outras.
- Facilita a compreensão e os testes.
Sinal de Alerta: Se você começar a descrever uma classe usando "e" (ex: "Esta classe gerencia usuários e registra logs"), é um indicativo para considerar a separação.
// ❌ Viola o SRP: Uma classe com múltiplas responsabilidades
public class GerenciadorUsuario {
public Usuario buscarUsuario(Long id) { ... }
public void salvarUsuario(Usuario usuario) { ... }
public void enviarEmail(String destinatario, String conteudo) { ... }
public void registrarLog(String operacao) { ... }
}
// ✅ Cumpre o SRP: Cada classe foca em uma única tarefa
public class ServicoUsuario {
public Usuario buscarUsuario(Long id) { ... }
public void salvarUsuario(Usuario usuario) { ... }
}
public class ServicoEmail {
public void enviarEmail(String destinatario, String conteudo) { ... }
}
public class ServicoLog {
public void registrarLog(String operacao) { ... }
}
Princípio da Aberta/Fechada (OCP - Open/Closed Principle)
Ideia Central: Entidades de software (classes, módulos, funções) devem estar abertas para extensão, mas fechadas para modificação. Novas funcionalidades devem ser implementadas através da extensão, não da alteração do código existente.
Por Que Seguir:
- Modificar código existente introduz risco de bugs.
- Extender com novo código é mais seguro, pois não afeta funcionalidades já estabelecidas.
- O sistema se torna mais estável e fácil de manter.
Sinal de Alerta: Pergunte-se "Preciso modificar código existente para adicionar esta nova funcionalidade?". Se a resposta for sim, considere refatorar.
// ❌ Viola o OCP: Adicionar cada nova forma de pagamento exige modificação
public class ServicoPagamento {
public void pagar(String tipo, BigDecimal valor) {
if ("ALIPAY".equals(tipo)) {
// Lógica de pagamento Alipay
} else if ("WECHAT".equals(tipo)) {
// Lógica de pagamento WeChat
}
// Cada nova forma de pagamento requer um novo 'else if'
}
}
// ✅ Cumpre o OCP: Novas funcionalidades são adicionadas via extensão
public interface Pagamento {
void processar(BigDecimal valor);
}
public class PagamentoAlipay implements Pagamento {
public void processar(BigDecimal valor) { ... }
}
public class PagamentoWechat implements Pagamento {
public void processar(BigDecimal valor) { ... }
}
// Para adicionar uma nova forma de pagamento, basta criar uma nova classe implementando Pagamento.
Princípio da Substituição de Liskov (LSP - Liskov Substitution Principle)
Ideia Central: Objetos de uma superclasse devem ser substituíveis por objetos de suas subclasses sem alterar a corretude do programa.
Por Que Seguir:
- Garante a correção da hierarquia de herança.
- Permite que o polimorfismo funcione como esperado.
- Torna o código mais previsível.
Sinal de Alerta: Uma subclasse viola o "contrato" da superclasse? Por exemplo, se um método da superclasse não lança exceções, mas uma subclasse o faz.
// ❌ Viola o LSP: A subclasse altera o comportamento esperado da superclasse
public class Retangulo {
protected int largura;
protected int altura;
public void setLargura(int largura) { this.largura = largura; }
public void setAltura(int altura) { this.altura = altura; }
public int getArea() { return largura * altura; }
}
public class Quadrado extends Retangulo {
@Override
public void setLargura(int largura) {
this.largura = largura;
this.altura = largura; // Altera o comportamento herdado
}
@Override
public void setAltura(int altura) {
this.largura = altura;
this.altura = altura; // Altera o comportamento herdado
}
}
// Se um objeto do tipo Retangulo na verdade for um Quadrado, o comportamento pode ser inesperado.
Princípio da Segregação de Interfaces (ISP - Interface Segregation Principle)
Ideia Central: Clientes não devem ser forçados a depender de interfaces que não utilizam. Interfaces devem ser pequenas e focadas.
Por Que Seguir:
- Reduz dependências desnecessárias.
- Torna as interfaces mais claras e fáceis de entender.
- Minimiza o impacto de mudanças em uma interface.
Sinal de Alerta: Uma classe implementadora é forçada a implementar métodos vazios? Isso sugere que a interface está muito grande.
// ❌ Viola o ISP: Interface muito grande, força implementações desnecessárias
public interface Animal {
void comer();
void voar();
void nadar();
}
// Um peixe não voa, mas é forçado a implementar voar()
public class Peixe implements Animal {
public void comer() { ... }
public void voar() { throw new UnsupportedOperationException("Peixes não voam"); }
public void nadar() { ... }
}
// ✅ Cumpre o ISP: Interfaces pequenas e especializadas
public interface Comestivel {
void comer();
}
public interface Voador {
void voar();
}
public interface Nadador {
void nadar();
}
public class Peixe implements Comestivel, Nadador {
public void comer() { ... }
public void nadar() { ... }
}
Princípio da Inversão de Dependência (DIP - Dependency Inversion Principle)
Ideia Central: Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Abstrações não devem depender de detalhes; detalhes devem depender de abstrações.
Por Que Seguir:
- Desacopla módulos de alto e baixo nível.
- Facilita a substituição de implementações de baixo nível.
- Simplifica os testes (permitindo o uso de mocks).
Sinal de Alerta: Um módulo de alto nível instancia diretamente uma classe concreta de baixo nível? Isso viola o DIP.
// ❌ Viola o DIP: Alto nível depende diretamente do baixo nível
public class ServicoUsuario {
private MySQLRepositorioUsuario repositorio = new MySQLRepositorioUsuario(); // Instancia direta
public Usuario buscarUsuario(Long id) {
return repositorio.buscarPorId(id);
}
}
// ✅ Cumpre o DIP: Depende de uma abstração (interface)
public interface RepositorioUsuario {
Usuario buscarPorId(Long id);
}
public class ServicoUsuario {
private RepositorioUsuario repositorio;
// Injeção de dependência via construtor
public ServicoUsuario(RepositorioUsuario repositorio) {
this.repositorio = repositorio;
}
public Usuario buscarUsuario(Long id) {
return repositorio.buscarPorId(id);
}
}
// Agora é fácil substituir por outras implementações: PostgreSQL, MongoDB, etc.
Hábito a Cultivar: Ao projetar classes, revise-as utilizando os princípios SOLID.
1.3 Sabedoria do Design: DRY, KISS, YAGNI
Além dos princípios SOLID, existem três princípios práticos importantes que guiam a sabedoria no design.
DRY (Don't Repeat Yourself)
Ideia Central: Elimine duplicação. Cada peça de conhecimento em um sistema deve ter uma representação única, clara e autoritativa.
Por Que Seguir:
- Código duplicado implica em múltiplas modificações.
- Aumenta a chance de esquecer de atualizar um ponto, gerando bugs.
- Eleva o custo de manutenção.
Limites da Duplicação: Quando a duplicação pode ser aceitável?
- Quando dois trechos de código parecem similares, mas representam conceitos de negócio distintos, não devem ser forçados a uma unificação.
- Abstração excessiva pode ser pior que duplicação.
- Regra dos Três: Repita três vezes antes de pensar em abstrair.
// ❌ Código duplicado
public void validarUsuario(Usuario usuario) {
if (usuario.getNome() == null || usuario.getNome().isEmpty()) {
throw new IllegalArgumentException("Nome não pode ser vazio.");
}
}
public void validarPedido(Pedido pedido) {
if (pedido.getNumero() == null || pedido.getNumero().isEmpty()) {
throw new IllegalArgumentException("Número do pedido não pode ser vazio.");
}
}
// ✅ Extrair método comum
public void validarNaoVazio(String valor, String nomeCampo) {
if (valor == null || valor.isEmpty()) {
throw new IllegalArgumentException(nomeCampo + " não pode ser vazio.");
}
}
KISS (Keep It Simple, Stupid)
Ideia Central: Mantenha a simplicidade. A simplicidade é elegante.
Por Que Seguir:
- Código simples é mais fácil de entender.
- Código simples é mais fácil de manter.
- Código simples tende a ter menos bugs.
Limites da Simplicidade: Simplicidade não significa superficialidade.
- Simplicidade é fazer o "suficiente", não necessariamente o "mínimo".
- Às vezes, uma certa complexidade é necessária para garantir a correção.
- O foco é em eliminar a "complexidade desnecessária".
// ❌ Implementação excessivamente complexa
public boolean ehPar(int numero) {
return numero % 2 == 0 ? true : false;
}
// ✅ Simples e direto
public boolean ehPar(int numero) {
return numero % 2 == 0;
}
YAGNI (You Aren't Gonna Need It)
Ideia Central: Você não vai precisar disso. Evite o design excessivo para necessidades futuras incertas.
Por Que Seguir:
- Design excessivo adiciona complexidade desnecessária.
- Desperdiça tempo de desenvolvimento.
- A funcionalidade pode nunca ser utilizada.
Limites do Design Excessivo: Preparar pontos de extensão não é necessariamente design excessivo.
- Preparar pontos de extensão pode ter baixo custo (ex: usar interfaces).
- Design excessivo tem alto custo (ex: implementar um framwork complexo).
- O ponto chave é o equilíbrio entre "custo" e "probabilidade de uso".
Hábito a Cultivar: Antes de codificar, pergunte-se: Isso é repetitivo? É simples o suficiente? É realmente necessário agora?
1.4 Alta Coesão e Baixo Acoplamento
Coesão: Refere-se ao grau de inter-relação entre os elementos dentro de um módulo. Alta coesão significa que o módulo é focado em uma única tarefa.
Acoplamento: Refere-se ao grau de dependência entre os módulos. Baixo acoplamento significa que os módulos são independentes.
Como Identificar: Quantos lugares você precisa modificar ao alterar um único ponto? Quanto mais lugares, maior o acoplamento.
Benefícios da Alta Coesão e Baixo Acoplamento:
- Modificações em um módulo não afetam outros.
- Módulos podem ser testados independentemente.
- Permite a reutilização de módulos individuais.
- O sistema se torna mais fácil de entender e manter.
Estratégias para Alcançar Alta Coesão e Baixo Acoplamento:
- Responsabilidade Única: Cada classe deve ter uma única tarefa.
- Injeção de Dependência: Injetar dependências via construtor ou setters.
- Programação Orientada a Interfaces: Depender de abstrações em vez de implementações concretas.
- Lei de Dérmetris: Um objeto só deve falar com seus "amigos" diretos.
Hábito a Cultivar: Ao projetar módulos, busque alta coesão e baixo acoplamento.
Capítulo 2: Hábitos de Codificação — Escrevendo Código Robusto
"Código é escrito para ser lido por humanos, e executado por máquinas."
2.1 Programação Defensiva: Não Confie em Nenhuma Entrada
O cerne da programação defensiva é: não confiar em nenhuma entrada externa e proteger todos os pontos onde erros podem ocorrer.
Validação de Parâmetros de Entrada
Por Que Validar:
- Entradas externas não são confiáveis; o chamador pode fornecer dados inválidos.
- Detectar erros precocemente, na camada de fronteira.
- Evitar que dados incorretos contaminem a lógica interna.
O Que Validar:
- Verificação de nulos.
- Verificação de intervalo (valores numéricos, comprimentos).
- Verificação de formato (datas, e-mails, telefones).
- Verificação de regras de negócio (status, permissões).
Onde Validar:
- Camada de Fronteira: No início do método.
- Confiança Interna: Em métodos internos, os parâmetros podem ser considerados válidos.
// ❌ Sem validação de parâmetros
public void criarUsuario(String nome, int idade) {
// Uso direto, risco de problemas
Usuario usuario = new Usuario(nome, idade);
repositorio.salvar(usuario);
}
// ✅ Validação de parâmetros
public void criarUsuario(String nome, int idade) {
if (nome == null || nome.trim().isEmpty()) {
throw new IllegalArgumentException("Nome não pode ser vazio.");
}
if (idade < 0 || idade > 150) {
throw new IllegalArgumentException("Idade deve estar entre 0 e 150.");
}
Usuario usuario = new Usuario(nome, idade);
repositorio.salvar(usuario);
}
Proteção Contra NullPointerExceptions (NPE)
Por Que NPEs são Difíceis de Depurar:
- Mensagens de erro de NPEs nem sempre são claras, dificultando a localização.
- Podem ocorrer em qualquer lugar.
- Ocasionalmente ocorrem em produção, sendo difíceis de reproduzir.
Hábito a Cultivar: Verifique a nulidade antes de acessar atributos.
// ❌ Chamada encadeada perigosa
String cidade = usuario.getEndereco().getCidade();
// ✅ Forma segura
String cidade = null;
if (usuario != null && usuario.getEndereco() != null) {
cidade = usuario.getEndereco().getCidade();
}
// ✅ Usando Optional (Java 8+)
String cidade = Optional.ofNullable(usuario)
.map(Usuario::getEndereco)
.map(Endereco::getCidade)
.orElse(null);
Verificação de Limites
Cenários Comuns de Excesso de Limite:
- Acesso a arrays:
array[indice], ondeindicepode estar fora dos limites. - Acesso a coleções:
lista.get(indice), ondeindicepode estar fora dos limites. - Extração de strings:
str.substring(inicio, fim), onde os limites podem ser inválidos.
Hábito a Cultivar: Verifique o tamanho antes de acessar.
// ❌ Possível IndexOutOfBoundsException
String nome = lista.get(1).getNome();
// ✅ Verifique antes de acessar
if (lista != null && lista.size() > 1) {
String nome = lista.get(1).getNome();
}
Hábito a Cultivar: Mantenha uma postura de desconfiança em relação a todas as entradas externas e cultive o hábito de validação.
2.2 Gerenciamento de Recursos: Use e Libere
O princípio central do gerenciamento de recursos é: liberar recursos assim que o uso for concluído para evitar vazamentos.
Recursos de IO
Por Que Fechar:
- Recursos de IO são finitos (handles de arquivo, conexões de banco de dados, etc.).
- Não fechar leva a vazamentos de recursos.
- Vazamentos em larga escala podem levar à falha do sistema.
Hábito a Cultivar: Prefira try-with-resources.
// ❌ Forma tradicional, risco de esquecer de fechar
FileInputStream fis = null;
try {
fis = new FileInputStream("arquivo.txt");
// Usar o recurso
} catch (IOException e) {
log.error("Erro de IO", e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
log.error("Erro ao fechar fis", e);
}
}
}
// ✅ try-with-resources (Java 7+)
try (FileInputStream fis = new FileInputStream("arquivo.txt")) {
// Usar o recurso
} catch (IOException e) {
log.error("Erro de IO", e);
}
// Fecha automaticamente, sem necessidade de tratamento manual.
Recursos de Thread
Por Que Usar Thread Pools:
- Criar e destruir threads tem um custo.
- Thread pools permitem a reutilização de threads.
- Permitem o controle da concorrência.
Necessidade de Isolamento de Thread Pools:
- Utilize thread pools diferentes para lógicas de negócio distintas.
- Evite que uma operação lenta impacte todo o serviço.
- Proporciona melhor controle e monitoramento de recursos.
// ❌ Todas as operações usam o mesmo pool
ExecutorService executorGeral = Executors.newFixedThreadPool(10);
// ✅ Pools diferentes para operações distintas
ExecutorService executorPedidos = new ThreadPoolExecutor(...);
ExecutorService executorPagamentos = new ThreadPoolExecutor(...);
ExecutorService executorNotificacoes = new ThreadPoolExecutor(...);
Hábito a Cultivar: Feche os recursos após o uso. Dê preferência a try-with-resources.
2.3 Consciência de Concorrência: Pense em Multithreading
A ideia central é: após escrever o código, reflita sobre como ele se comportaria em um ambiente multithread.
Segurança de Threads
Quando Considerar Concorrência:
- Variáveis compartilhadas: múltiplos threads acessando a mesma variável.
- Operações "leia-e-modifique": consulta e modificação não são atômicas.
- Dependência de estado: operações que dependem de um estado específico.
Problemas Comuns de Segurança de Threads:
- Condições de corrida (Race Conditions): múltiplos threads modificando dados simultaneamente.
- Problemas de visibilidade: um thread modifica um dado, mas outros não enxergam a alteração.
- Problemas de ordenação: a ordem de execução do código pode ser reordenada pelo processador/compilador.
Hábito a Cultivar: Seja cauteloso com variáveis compartilhadas.
// ❌ Não é thread-safe
private Map<string string=""> cache = new HashMap<>();
// ✅ Thread-safe
private Map<string string=""> cache = new ConcurrentHashMap<>();
</string></string>
Armadilhas de Atomicidade
Condição de Corrida em "Leia e Modifique":
// ❌ Operação não atômica, sujeito a condições de corrida
if (disponivel(idBilhete)) {
// Outro thread pode ter usado o bilhete neste ponto
adicionarDinheiro(valor);
removerBilhetePorId(idBilhete);
}
// ✅ Utilizando atomicidade do banco de dados
if (removerBilheteDisponivelPorId(idBilhete) == 1) {
adicionarDinheiro(valor);
}
Hábito a Cultivar: Após escrever o código, pense em como ele se comportaria sob concorrência. Existem problemas de thread-safety?
2.4 Tratamento de Exceções: Lide com o Inesperado com Elegância
O princípio é: trate situações inesperadas com elegância, em vez de deixar o programa travar.
Não Engula Exceções
Antipadrão:
// ❌ Engolindo a exceção, problemas não são detectados
try {
fazerAlgo();
} catch (Exception e) {
// Bloco vazio ou apenas uma mensagem genérica
}
// ❌ Informação de log insuficiente
try {
fazerAlgo();
} catch (Exception e) {
log.error("Ocorreu um erro.");
}
Padrão:
// ✅ Logando informações cruciais
try {
fazerAlgo();
} catch (Exception e) {
log.error("Falha na operação, userId={}, orderId={}", userId, orderId, e);
}
Hábito a Cultivar: Os logs devem conter informações essenciais:
- Contexto da operação (quem, o quê, quais parâmetros).
- Stack trace completo da exceção.
Fronteiras da Exceção
Onde Capturar, Onde Lançar:
- Captura na Fronteira: Capture exceções na entrada do método, registre logs e retorne uma resposta amigável.
- Lançamento Interno: Métodos internos devem lançar exceções para que o chamador decida como lidar.
Exceções Verificadas vs. Não Verificadas:
- Verificadas (Checked): O compilador exige tratamento (
try-catchouthrows). Adequadas para condições recuperáveis. - Não Verificadas (Unchecked): O tratamento é opcional. Adequadas para erros de programação.
// ✅ Tratamento de exceção adequado
public Usuario buscarUsuario(Long id) {
try {
return repositorio.buscarPorId(id);
} catch (DataAccessException e) {
log.error("Falha ao buscar usuário, id={}", id, e);
throw new ExcecaoDeNegocio("Falha ao buscar usuário.", e);
}
}
Degradação Graciosa (Graceful Degradation)
O Que Fazer em Caso de Falha de API Externa:
- Valor Padrão: Retorne um valor padrão ou vazio.
- Cache como Fallback: Utilize dados de cache.
- Circuit Breaker: Falhe rapidamente e retorne um resultado degradado.
// ✅ Degradação graciosa
public Usuario buscarUsuarioRemoto(Long id) {
try {
return servicoRemoto.buscarUsuario(id);
} catch (Exception e) {
log.warn("Falha na chamada remota, usando cache como fallback, id={}", id);
return servicoCache.buscarUsuario(id);
}
}
Hábito a Cultivar: O tratamento de exceções deve ser claro nos logs, com fronteiras bem definidas e degradação graciosa.
Capítulo 3: Ética de Engenharia — Refinamento Além do Código
"O verdadeiro campo de batalha está além do código."
3.1 Design de Interfaces: Espírito de Contrato
Interfaces são contratos entre módulos. Um bom design de interface torna o sistema mais estável e fácil de usar.
Compatibilidade (Backward Compatibility)
Por Que Compatibilidade é Crucial:
- Existem consumidores do seu código em produção; alterar interfaces pode impactá-los.
- Manter a compatibilidade retroativa é um princípio fundamental de design de API.
- Quebrar a compatibilidade pode levar à falha dos sistemas consumidores.
Como Manter Compatibilidade:
- Adicionar, Não Modificar: Adicione parâmetros ou métodos, mas não altere os existentes.
- Valores Padrão: Novos parâmetros devem ter valores padrão.
- Marcação de Depreciação: Use
@Deprecatedpara métodos que serão removidos.
// ❌ Modificação direta da interface, quebra de compatibilidade
// Interface antiga
void criarUsuario(String nome, int idade);
// Modificada para:
void criarUsuario(String nome, int idade, String email); // Consumidores falharão
// ✅ Adição de nova interface, mantendo compatibilidade
// Interface antiga permanece
@Deprecated
void criarUsuario(String nome, int idade);
// Nova interface
void criarUsuario(String nome, int idade, String email);
Idempotência
O Que é Idempotência: Executar uma operação múltiplas vezes produz o mesmo resultado que executá-la uma única vez. Operações de consulta são naturalmente idempotentes.
Por Que Idempotência é Importante:
- Redes são inerentemente não confiáveis; retentativas podem ocorrer.
- Usuários podem clicar repetidamente em botões.
- Mensagens podem ser processadas múltiplas vezes em sistemas distribuídos.
Soluções Comuns:
- Índices únicos: Garantia no nível do banco de dados.
- Mecanismo de token: Previne submissões duplicadas.
- Otimismo Lock: Controle de versão.
- Máquina de Estados: Transições de estado unidirecionais.
// ✅ Garantindo idempotência com índice único
public void criarPedido(Pedido pedido) {
try {
repositorio.salvar(pedido);
} catch (DuplicateKeyException e) {
log.warn("Pedido já existe, orderId={}", pedido.getNumero());
// Tratamento idempotente, sem lançar exceção
}
}
Hábito a Cultivar: Ao projetar interfaces, pergunte-se: "O que acontece se esta interface for chamada duas vezes?".
3.2 Interação com Banco de Dados: Respeite os Dados
Dados são o núcleo do sistema. Opere sobre o banco de dados com cautela.
Validação de SQL
Hábito a Cultivar: Execute seu SQL antes de colocá-lo em produção.
- Execute o SQL em um cliente de banco de dados para verificar a sintaxe.
- Use
EXPLAINpara analisar o plano de execução e garantir o uso de índices. - Verifique o volume de dados escaneado para evitar varreduras de tabela completa.
-- Verificando o plano de execução
EXPLAIN SELECT * FROM usuario WHERE usuario_id = 10086;
-- Campos importantes a observar:
-- type: Tipo de acesso (ALL indica varredura completa)
-- key: Índice utilizado
-- rows: Número de linhas escaneadas
Operações em Lote
Por Que Evitar Consultas em Loop:
- Cada consulta tem um overhead de rede.
- Conexões de banco de dados são recursos limitados.
- N consultas individuais são significativamente mais lentas que 1 consulta em lote.
// ❌ Consultas em loop
for (Long id : ids) {
Usuario usuario = repositorio.buscarPorId(id);
// Processar
}
// ✅ Consulta em lote
List<usuario> usuarios = repositorio.buscarPorIds(ids);
Map<long usuario=""> mapaUsuarios = usuarios.stream()
.collect(Collectors.toMap(Usuario::getId, Function.identity()));
</long></usuario>
Latência de Replicação (Master-Slave Lag
O Que é Latência de Replicação:
- Operações de escrita ocorrem no Master.
- Operações de leitura ocorrem nos Slaves.
- A sincronização do Master para o Slave tem um atraso.
Quando Ocorrem Problemas:
- Leitura imediatamente após uma escrita.
- Inserção bem-sucedida, mas a consulta subsequente não retorna o dado.
Soluções:
- Forçar Leitura do Master: Em operações críticas, direcione as leituras para o Master.
- Double Delete com Delay: Delete do cache, atualize o DB, delete do cache novamente após um tempo. (Para consistência eventual).
- Aceitar Latência: Para operações não críticas, aceite o atraso temporário.
Hábito a Cultivar: Após escrever operações de banco de dados, considere a possibilidade de latência de replicação.
3.3 Uso de Cache: Arte de Trocar Espaço por Tempo
O cache é crucial para o desempenho, mas seu uso incorreto pode introduzir problemas.
Consistência do Cache
Atualizar DB Primeiro ou Deletar Cache Primeiro?
- Opção 1: Atualizar DB, depois deletar cache. Problema: Uma leitura pode pegar o cache antigo entre a atualização do DB e a deleção do cache.
- Opção 2: Deletar cache, depois atualizar DB. Problema: Uma lietura pode pegar o cache deletado e popular com o dado antigo entre a deleção e a atualização do DB.
- Opção 3: Double Delete (Deleção Dupla com Atraso). Deleta cache, atualiza DB, espera um tempo e deleta cache novamente. Visa a consistência eventual.
Aceitação da Consistência Eventual:
- A maioria das aplicações pode tolerar inconsistências breves.
- O importante é que a consistência seja alcançada "eventualmente".
- Se consistência forte é requisito, evite cache.
Os Três Grandes Problemas do Cache
Cache Penetration (Cache Through):
- Problema: Consultas por dados inexistentes. O cache nunca será preenchido.
- Solução: Filtros de Bloom, cache de valores nulos.
Cache Breakdown (Cache Knock-out):
- Problema: Uma chave popular expira. Grande volume de requisições atinge o banco de dados.
- Solução: Locks de exclusão mútua (mutex), cache com expiração infinita + atualização assíncrona.
Cache Avalanche (Cache Avalanche):
- Problema: Muitas chaves expiram simultaneamente. Grande volume de requisições atinge o banco de dados.
- Solução: Adicionar um valor aleatório ao tempo de expiração, cache multinível.
┌─────────────────────────────────────────────────────┐
│ Cache Penetration: Consulta por dados inexistentes │
│ Solução: Filtro de Bloom / Cache de nulos │
├─────────────────────────────────────────────────────┤
│ Cache Breakdown: Chave popular expira │
│ Solução: Mutex / Expiração infinita + update async │
├─────────────────────────────────────────────────────┤
│ Cache Avalanche: Múltiplas chaves expiram juntas │
│ Solução: Tempo de expiração aleatório / Cache multi-nível│
└─────────────────────────────────────────────────────┘
Hábito a Cultivar: Ao usar cache, considere problemas de consistência e os "Três Grandes Problemas".
3.4 Integração com Terceiros: Gerenciamento de Dependências Externas
Ao chamar APIs de terceiros, antecipe e gerencie possíveis falhas.
Controle de Timeout
Por Que Definir Timeout:
- APIs de terceiros podem ser lentas.
- Sem timeout, sua aplicação pode ficar bloqueada indefinidamente.
- Timeout protege sua aplicação contra lentidão externa.
connectTimeout vs. readTimeout:
connectTimeout: Tempo para estabelecer a conexão.readTimeout: Tempo para receber a resposta após a conexão ser estabelecida.
// ✅ Definindo timeouts razoáveis
HttpClient cliente = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5)) // 5 segundos para conectar
.readTimeout(Duration.ofSeconds(10)) // 10 segundos para ler a resposta
.build();
Estratégia de Retentativa (Retry)
Quando Retentar:
- Timeouts de rede.
- Erros de servidor (status 5xx).
- Rate limiting (status 429).
Quando NÃO Retentar:
- Erros de cliente (status 4xx).
- Erros de negócio.
- Operações não idempotentes.
Backoff Exponencial:
- 1ª retentativa: esperar 1s.
- 2ª retentativa: esperar 2s.
- 3ª retentativa: esperar 4s.
- ... e assim por diante.
// ✅ Retentativa com backoff exponencial
public <T> T retryWithBackoff(Callable<T> callable, int maxRetries) throws Exception {
int tentativas = 0;
while (true) {
try {
return callable.call();
} catch (Exception e) {
if (++tentativas > maxRetries) {
throw e; // Lança a exceção após o número máximo de tentativas
}
// Calcula o tempo de espera exponencialmente
long tempoEspera = (long) Math.pow(2, tentativas) * 1000;
try {
Thread.sleep(tempoEspera);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // Restaura o status de interrupção
throw e; // Lança a exceção original
}
}
}
}
Circuit Breaker e Degradação
O Que é Circuit Breaker:
- Quando a taxa de falha atinge um limite, a chamada falha rapidamente.
- Evita chamadas contínuas a um serviço indisponível.
- Permite que o serviço tenha tempo para se recuperar.
Quando Acionar o Circuit Breaker:
- Taxa de erros excede um limiar (ex: 50%).
- Volume de requisições excede um limiar.
- Tempo de resposta excede um limiar.
Estratégias de Degradação:
- Retornar valor padrão.
- Retornar dados do cache.
- Retornar uma mensagem de erro amigável.
Hábito a Cultivar: Ao chamar APIs externas, implemente timeouts, retentativas e circuit breakers.
Capítulo 4: Ética de Colaboração — Código para Pessoas
"Seu código é o manual de instruções para a próxima pessoa."
4.1 Nomenclatura: A Primeira Impressão do Código
Um bom nome é o melhor comentário. O ideal é que o nome revele a intenção.
Critérios para Bons Nomes:
- Sugestivo: O nome deve indicar claramente o propósito.
- Sem Ambiguidade: Evitar interpretações errôneas.
- Consistente: Usar a mesma convenção para conceitos similares.
Convenções de Nomenclatura:
- Nomes de Classe: Substantivos, CamelCase (
ServicoUsuario). - Nomes de Métodos: Verbos ou frases verbo-objeto, camelCase (
buscarUsuario,criarPedido). - Constantes: Tudo em maiúsculas, com underscores (
MAX_TENTATIVAS). - Variáveis: camelCase (
nomeUsuario).
Nomes a Evitar:
temp,data,info: Muito genéricos, não indicam o conteúdo.a,b,c: Sem significado.processar,manipular: Vagos, não especificam a ação.
// ❌ Nomes ruins
public void processar(Object dados) { ... }
// ✅ Nomes bons
public void criarPedido(RequisicaoPedido requisicao) { ... }
Verbos Úteis para Nomes de Métodos:
get: Obtercreate: Criarupdate: Atualizardelete: Deletarfind: Encontrarcheck: Verificarvalidate: Validarcalculate: Calcularconvert: Converterformat: Formatar
Hábito a Cultivar: Invista alguns segundos extras na nomeação para tornar o código mais legível.
4.2 Comentários: Clareza nas Explicações
Comentários complementam o código; bons comentários auxiliam na compreensão.
Limites dos Comentários:
- O Que Comentar: Lógica de negócio complexa, razões para tratamentos específicos, workarounds para bugs conhecidos.
- O Que NÃO Comentar: Código óbvio, comentários desatualizados, comentários redundantes.
Bons Comentários: Explicam o "Porquê", Não o "O Quê".
// ❌ Comentário redundante
// Obtém o usuário
public Usuario getUsuario(Long id) { ... }
// ✅ Explicando o "porquê"
// Necessário ler diretamente do Master devido à latência de replicação pós-inserção.
public Usuario getUsuarioDoMaster(Long id) { ... }
Comentários Ruins: Pior que a Ausência.
// ❌ Comentário desatualizado
// Enviar e-mail
public void enviarNotificacao(Usuario usuario) {
// Na verdade, envia SMS
servicoSMS.enviar(usuario.getTelefone(), "...");
}
Lógica de Negócio Complexa: Comentários São Essenciais
// ✅ Lógica complexa requer comentários
/**
* Calcula o valor total do pedido.
*
* Regras:
* 1. Desconto de R$10 para pedidos acima de R$100.
* 2. Desconto de R$30 para pedidos acima de R$200.
* 3. Clientes VIP recebem 5% de desconto adicional.
* 4. Cupons podem ser combinados.
*/
public BigDecimal calcularValorPedido(Pedido pedido, Usuario usuario, Cupom cupom) {
// ... implementação ...
}
Hábito a Cultivar: Comente lógicas de negócio complexas, explicando o "porquê", não apenas o "o quê".
4.3 Code Review (Revisão de Código)
Code Review é mais que encontrar bugs; é uma ferramenta vital de colaboração.
Valor do Code Review:
- Identificação de bugs e potenciais problemas.
- Compartilhamento de conhecimento; aprendizado mútuo.
- Uniformização do estilo e das convenções do código.
- Melhora geral da qualidade do código.
Como Fazer um Bom Code Review:
- Foco: Correção lógica, tratamento de limites, tratamento de exceções, performance.
- Atitude: Crítica construtiva, focada no código, não na pessoa.
- Timing: Revise prontamente para não atrasar o processo.
Como Receber um Code Review:
- Mantenha a mente aberta; não encare como ataque pessoal.
- Considere seriamente cada sugestão.
- Discuta pontos de dúvida, mas evite discussões infrutíferas.
Checklist de Code Review:
- Lógica correta?
- Casos de borda tratados?
- Exceções tratadas adequadamente?
- Potenciais problemas de concorrência?
- Impacto na performence?
- Nomenclatura clara?
- Comentários suficientes e relevantes?
- Testes cobertos?
Hábito a Cultivar: Participe ativamente de Code Reviews, tanto revisando quanto sendo revisado.
4.4 Hábito de Documentação
Boa documentação facilita a manutenção e a passagem de conhecimento.
README: O Cartão de Visitas do Projeto
- Breve descrição do projeto.
- Instruções de início rápido.
- Configuração necessária.
- Guia de implantação.
CHANGELOG: Registro das Versões
- Detalhamento das mudanças em cada release.
- Novas funcionalidades.
- Bugs corrigidos.
- Alterações que quebram compatibilidade (breaking changes).
Documentos de Design: Para Soluções Complexas
- Contexto e problema.
- Design da solução.
- Escolhas tecnológicas.
- Avaliação de riscos.
Documentação de API: Swagger/YApi
- Endpoint da API.
- Parâmetros de requisição.
- Formato da resposta.
- Códigos de erro.
Hábito a Cultivar: Documente funcionalidades e soluções importantes para registrar o conhecimento.
Capítulo 5: Evolução Pessoal — De Hábitos a Virtudes
"Escrever código é uma jornada de aprimoramento."
5.1 Coragem e Oportunidade para Refatorar
O Que é Refatorar: Melhorar a estrutura interna do código sem alterar seu comportamento externo.
Quando Refatorar:
- Regra dos Três: Na primeira vez que escrever, apenas escreva. Na segunda vez que vir código similar, preste atenção. Na terceira vez, refatore.
- Odores de Código: Código duplicado, métodos longos, classes gigantes, listas de parâmetros extensas.
- Ao Adicionar Funcionalidade: Se o código atual dificulta a extensão, refatore antes de adicionar.
- Ao Corrigir Bugs: Se o código é difícil de entender, refatore antes de corrigir.
A Coragem de Refatorar:
- Não tenha medo de modificar código; ele é feito para ser alterado.
- Testes automatizados criam uma rede de segurança para refatorações.
- Refatore em pequenos passos; modifique pouco a pouco.
A Rede de Segurança da Refatoração:
- Testes unitários.
- Testes de integração.
- Revisões de código (Code Reviews).
Hábito a Cultivar: Ao identificar "odores de código", refatore prontamente, não espere os problemas se agravarem.
5.2 Gerenciamento de Débito Técnico
O Que é Débito Técnico: Sacrificar a qualidade do código por ganhos de curto prazo, resultando em custos futuros maiores.
Tipos de Débito:
- Intencional: Ciente do problema, mas opta por uma solução rápida para cumprir prazos.
- Não Intencional: O problema não é identificado no momento da escrita do código.
Como Gerenciar:
- Registrar: Mantenha um registro do débito técnico.
- Avaliar: Estime o impacto e o custo de quitação.
- Pagar: Quite o débito periodicamente.
Juros do Débito:
- Débito técnico é como juros altos; quanto mais se adia, mais caro fica.
- Quitar o débito no início é mais barato.
- O não pagamento pode tornar o sistema insustentável.
Hábito a Cultivar: Registre o débito técnico e pague-o regularmente.
5.3 Métodos de Aprendizagem
Leitura de Código Fonte:
- Entenda o "porquê" por trás das implementações.
- Aprenda designs e práticas de código de alta qualidade.
- Recomendado ler: JDK, Spring, MyBatis.
Blogs Técnicos:
- Escrever força a organizar o conhecimento.
- O processo de escrita de um blog consolida o aprendizado.
- Contribui para a construção de uma reputação técnica.
Contribuição para Projetos Open Source:
- Aprenda com projetos estabelecidos.
- Absorva as melhores práticas da comunidade.
- Contribua e receba feedback.
Revisão e Resumo:
- Tropeçar no mesmo erro é inaceitável.
- Após cada tropeço, resuma a causa e a solução.
- Crie seu próprio "manual de tropeços".
Hábito a Cultivar: Mantenha-se aprendendo e revise suas experiências periodicamente.
5.4 Longo Prazo para Engenheiros
Qualidade de Código é um Investimento:
- Em curto prazo, escrever código de alta qualidade pode parecer mais lento.
- No longo prazo, código de alta qualidade é mais fácil de manter e mais eficiente.
- Débito técnico é um empréstimo com juros altos; adiar o pagamento é custoso.
Profundidade vs. Amplitude Técnica:
- Busque profundidade primeiro, depois amplitude.
- Profundidade o torna um especialista; amplitude amplia sua visão.
- Busque ser um profissional em "T": especialista em uma área e com conhecimento em várias.
Escrever Código é uma Jornada de Aprimoramento:
- Código é a expressão do pensamento.
- Código de qualidade exige polimento contínuo.
- A jornada de aprimoramento não tem fim, apenas progresso constante.
Hábito a Cultivar: Adote uma mentalidade de longo prazo; a qualidade do código é um investimento, não um custo.
Conclusão
Resumo: O Núcleo da Ética de Engenharia
Recapitulando este artigo, a essência da ética de engenharia pode ser resumida em:
- Pensamento de Design: Um bom design minimiza o custo de mudanças.
- Hábitos de Codificação: Programação defensiva para um código robusto.
- Ética de Engenharia: O campo de batalha real está além do código.
- Ética de Colaboração: Escreva código para ser compreendido por outros.
- Evolução Pessoal: De hábitos a virtudes, com foco no longo prazo.
Sugestão: Três Hábitos para Começar Hoje
Se você me perguntasse por onde começar, eu sugeriria estes três hábitos:
- Pense em Multithreading após escrever o código: Cultive a consciência de concorrência.
- Verifique a nulidade antes de acessar atributos: Evite
NullPointerExceptions. - Comente lógicas complexas: Explique o "porquê".
Reflexão Final: Acima do Código, Estão as Pessoas
Código é escrito para ser lido por humanos, e executado por máquinas.
Acima do código, estão as pessoas. Seus colegas, seu futuro eu, aqueles que darão continuidade ao seu trabalho.
Escrever código de qualidade é um ato de respeito a si mesmo e aos outros.
Que possamos trilhar este caminho juntos.
"Código de qualidade não é escrito, é lapidado; bons hábitos não são aprendidos, são forjados em meio a tropeços."
Que cada tropeço se torne um degrau no seu crescimento.