Voltar ao blog
Clean CodeBest PracticesSoftware EngineeringTypeScript

Clean Code na prática: as 12 regras pelas quais a nossa equipa se rege

Doze regras que exigimos em code review e CI: funções com um só propósito, erros tipados, testes de comportamento, sem código morto, padrões automatizados.

P
Davi Nunes
March 18, 202612 min de leitura

Todos os programadores já leram o "Clean Code" de Robert Martin. Poucas equipas aplicam de facto os seus princípios de forma consistente. É no fosso entre saber como é o código limpo e entregá-lo sob pressão de prazos que a maioria das bases de código se degrada.

Na Privum, destilámos anos de experiência em produção em 12 regras que aplicamos em todos os projetos — desde ferramentas internas a plataformas voltadas para o cliente que tratam milhões de pedidos. Não são diretrizes aspiracionais; são práticas concretas, sustentadas por revisões de código, linters e gates de CI.

1. As funções fazem uma só coisa

Uma função deve fazer uma coisa, fazê-la bem e fazer apenas isso. Se precisas da palavra "e" para descrever o que uma função faz, deviam ser duas funções.

Mau: validateAndSaveUser(data) — isto valida E guarda. Se a validação falhar, tenta guardar na mesma? Se a gravação falhar, reverte o estado da validação? O acoplamento cria ambiguidade.

Bom: validateUser(data) → saveUser(validatedUser) — cada função tem uma única responsabilidade, um único motivo para falhar e uma única coisa para testar.

A regra prática: se uma função tem mais de 20 linhas, provavelmente está a fazer mais do que uma coisa. Extrai a segunda.

2. Dá nomes às coisas pelo que fazem, não pelo que são

Os nomes de variáveis e funções devem revelar a intenção. Quem lê deve perceber o que o código faz sem ler a implementação.

Mau: const d = new Date() — o que é "d"? Uma data, mas qual? Porque é que precisamos dela?

Bom: const subscriptionExpiresAt = new Date(user.trialEnd) — agora qualquer pessoa que leia isto sabe exatamente o que representa e porque existe.

Para variáveis booleanas, usa prefixos que se leiam como perguntas: isActive, hasPermission, canDelete, shouldRetry. Código que se lê como inglês é código que não precisa de comentários.

3. Falha depressa, falha ruidosamente

Não engulas erros em silêncio. Não devolvas null quando algo corre mal. Não registes um erro no log e continues como se nada fosse.

Mau: Um try/catch que apanha tudo e devolve um array vazio — quem chama não faz ideia de que algo falhou, e depurar torna-se um pesadelo.

Bom: Valida as entradas na fronteira (handlers de API, submissões de formulários), lança erros tipados quando os invariantes são violados e deixa o erro propagar-se até um handler que saiba como responder (devolver um 400, mostrar um toast, tentar de novo).

Os erros silenciosos são os bugs mais caros. Não deitam a tua aplicação abaixo — corrompem os teus dados lentamente, ao longo de semanas, até alguém reparar.

4. Escreve testes de comportamento, não de implementação

Os testes devem verificar o que o código faz, não como o faz. Se refatorares o funcionamento interno e os teus testes partirem, os teus testes estão a testar a coisa errada.

Mau: Verificar que um método privado específico foi chamado com argumentos específicos — este teste está acoplado a detalhes de implementação e vai partir em qualquer refactor.

Bom: Dada uma entrada, verifica o resultado ou o efeito secundário. "Quando um utilizador submete um formulário válido, deve ver uma mensagem de sucesso e os dados devem ficar persistidos." Este teste sobrevive à refatoração porque testa comportamento.

As métricas de cobertura são úteis, mas enganadoras. 80% de cobertura com bons testes de comportamento é melhor do que 100% de cobertura com testes de implementação frágeis.

5. Mantém as dependências nas extremidades

A lógica de negócio não deve importar clientes HTTP, drivers de base de dados nem código específico de um framework. Empurra o I/O para as extremidades e mantém o núcleo puro.

Isto não é um conselho académico — tem consequências práticas: - A lógica de negócio pura é trivialmente testável (sem necessidade de mocks) - Mudar de PostgreSQL para MongoDB altera o adaptador, não o domínio - As atualizações de framework não se propagam em cascata por toda a tua base de código

O padrão: Controladores/Handlers → Casos de uso/Serviços → Lógica de domínio. As dependências fluem para dentro. O domínio nunca importa nada da infraestrutura.

6. Prefere composição a herança

A herança cria um acoplamento forte. Quando herdas de uma classe base, herdas todo o seu contrato — incluindo partes de que não precisas e bugs que não escreveste.

A composição dá-te flexibilidade: combina peças pequenas e focadas em vez de construíres hierarquias profundas. Em TypeScript/JavaScript, isto significa preferir funções e objetos simples a hierarquias de classes.

Se deres por ti a criar uma hierarquia de classes com mais de dois níveis, para e refatora. A complexidade não compensa a abstração.

7. Trata os erros ao nível certo

Nem todas as funções devem tratar todos os erros. Os erros devem ser tratados ao nível que tem contexto suficiente para tomar a decisão certa.

Uma função de consulta à base de dados não deve decidir que código de estado HTTP devolver — não sabe que está a ser chamada a partir de um handler HTTP. Deve lançar um erro tipado que o handler consiga interpretar.

Organiza o tratamento de erros por camadas: - Camada de domínio: lança erros específicos do domínio (UserNotFound, InsufficientBalance) - Camada de aplicação: apanha os erros de domínio e decide a estratégia de resposta - Camada de infraestrutura: apanha os erros de transporte, implementa novas tentativas e circuit breakers

8. Torna os estados ilegais irrepresentáveis

Usa o teu sistema de tipos para prevenir bugs em tempo de compilação. Se um utilizador pode estar "ativo" ou "suspenso", não uses um booleano — usa um tipo união ou um enum. Se uma encomenda tem de ter pelo menos um artigo, não uses um array — usa um tipo que garanta que não está vazio.

Exemplo em TypeScript: em vez de { status: string }, usa { status: 'active' | 'suspended' | 'deleted' }. Agora o compilador apanha gralhas e estados inválidos antes do runtime.

Quantos mais invariantes codificares nos tipos, menos verificações em tempo de execução precisas e menos bugs chegam a produção.

9. As revisões de código não são opcionais

Cada linha de código que chega a produção deve ser revista por pelo menos outro engenheiro. Sem exceções — nem para "correções rápidas", nem para "são só alterações de configuração", nem para o tech lead.

As revisões de código apanham bugs, partilham conhecimento, fazem cumprir os padrões e criam responsabilidade partilhada pelo código. São a prática de qualidade mais eficaz a seguir aos testes automatizados.

Mantém as revisões pequenas (menos de 400 linhas). Revê em menos de 24 horas. Comenta padrões, não preferências. Faz perguntas em vez de exigências.

10. Apaga o código morto

O código morto não é gratuito. Confunde os novos membros da equipa, aumenta a carga cognitiva e, de vez em quando, é reativado por acidente. Se o código não é chamado, apaga-o. O Git lembra-se.

Isto inclui: - Blocos de código comentados - Imports e variáveis não utilizados - Feature flags que estão permanentemente ligadas ou desligadas há meses - Endpoints de API que nada chama

Corre uma ferramenta de análise de código morto trimestralmente. Vais ficar surpreendido com o quanto se acumula.

11. Faz logging com propósito

Registar tudo é tão inútil como não registar nada. Bons logs respondem a: "o que aconteceu, quando, a quem e qual era o contexto?"

Estrutura os teus logs em JSON com campos consistentes: timestamp, level, service, requestId, userId, action, duration, error. Isto torna-os pesquisáveis e agregáveis.

Usa corretamente os níveis de log: - ERROR: algo falhou e precisa de atenção - WARN: aconteceu algo inesperado, mas foi tratado - INFO: eventos de negócio relevantes (um utilizador registou-se, um pagamento foi processado) - DEBUG: detalhes técnicos úteis durante o desenvolvimento (tempos de consulta, cache hits)

Em produção, corre ao nível INFO. Ativa o DEBUG apenas quando estiveres a investigar problemas específicos.

12. Automatiza os teus padrões

As regras que dependem da disciplina humana para serem cumpridas vão ser violadas sob pressão. Codifica os teus padrões nas ferramentas:

  • Linting: ESLint, Prettier ou Biome para formatação e estilo
  • Verificação de tipos: TypeScript em modo strict, sem any
  • Pre-commit hooks: Husky + lint-staged para apanhar problemas antes de chegarem ao CI
  • Gates de CI: testes, verificação de tipos e lint têm de passar antes do merge
  • Análise de dependências: Renovate ou Dependabot para atualizações automatizadas

Se uma regra é suficientemente importante para ser imposta, é suficientemente importante para ser automatizada. Os revisores humanos devem focar-se na lógica, na arquitetura e na clareza — não na indentação nem na ordem dos imports.

Conclusão

Código limpo não é um luxo — é uma decisão económica. Cada hora investida em qualidade de código poupa dez horas em depuração, onboarding e manutenção. As equipas que aplicam estas práticas entregam mais depressa (não mais devagar), porque passam menos tempo a lutar contra a sua própria base de código e mais tempo a construir funcionalidades.

A melhor altura para adotar estas práticas é no início de um projeto. A segunda melhor altura é agora. Escolhe três regras desta lista, acrescenta-as à definition of done da tua equipa e aplica-as no teu próximo sprint. A melhoria é incremental — mas acumula-se.

Clean Code: 12 regras que seguimos à risca | Privum Cloud