Código que funciona qualquer um escreve. Código que outros conseguem entender, manter e evoluir é o que separa um dev junior de um profissional sênior. Clean Code não é estética — é produtividade, menos bugs e menos retrabalho.
Nomes que comunicam intenção
O nome de uma variável, método ou classe deve responder três perguntas: o que faz, por que existe e como é usado. Se precisa de um comentário para explicar, o nome está ruim.
// ❌ Ruim — o que é "d"? O que são "x" e "y"?
int d;
var x = GetList();
foreach (var y in x)
{
if (y.S == 1)
Process(y);
}
// ✅ Bom — o código se auto-explica
int diasAteVencimento;
var pedidosPendentes = _pedidoRepository.ObterPendentes();
foreach (var pedido in pedidosPendentes)
{
if (pedido.Status == StatusPedido.AguardandoPagamento)
_pagamentoService.ProcessarCobranca(pedido);
}
Regras práticas para nomes
| Elemento | Convenção | Exemplo |
|---|---|---|
| Classe | Substantivo, PascalCase | NotaFiscalService |
| Método | Verbo de ação, PascalCase | CalcularDesconto() |
| Variável | camelCase, descritiva | totalComImpostos |
| Booleano | Prefixo is, has, pode | isAtivo, possuiDesconto |
| Interface | Prefixo I + substantivo | IPedidoRepository |
| Constante | PascalCase | MaximoTentativas |
Métodos pequenos que fazem uma coisa só
Se um método tem mais de 20 linhas, provavelmente faz coisas demais. Métodos longos são difíceis de testar, difíceis de reutilizar e difíceis de entender.
// ❌ Método que faz tudo — validar, calcular, salvar, notificar
public async Task CriarPedido(PedidoRequest request)
{
if (string.IsNullOrEmpty(request.ClienteId))
throw new ArgumentException("Cliente obrigatório");
if (request.Itens == null || !request.Itens.Any())
throw new ArgumentException("Pedido sem itens");
var total = 0m;
foreach (var item in request.Itens)
{
var produto = await _produtoRepo.GetById(item.ProdutoId);
total += produto.Preco * item.Quantidade;
}
var desconto = total > 500 ? total * 0.1m : 0m;
total -= desconto;
var pedido = new Pedido
{
ClienteId = request.ClienteId,
Itens = request.Itens,
Total = total,
Desconto = desconto,
CriadoEm = DateTime.UtcNow
};
await _pedidoRepo.Inserir(pedido);
await _emailService.Enviar(request.ClienteId, "Pedido criado", $"Total: {total:C}");
await _estoqueService.Reservar(request.Itens);
}
// ✅ Cada método tem uma responsabilidade clara
public async Task CriarPedido(PedidoRequest request)
{
Validar(request);
var total = await CalcularTotal(request.Itens);
var desconto = CalcularDesconto(total);
var pedido = MontarPedido(request, total, desconto);
await _pedidoRepo.Inserir(pedido);
await NotificarClienteEReservarEstoque(pedido, request);
}
private void Validar(PedidoRequest request)
{
if (string.IsNullOrEmpty(request.ClienteId))
throw new ArgumentException("Cliente obrigatório");
if (request.Itens is null || !request.Itens.Any())
throw new ArgumentException("Pedido sem itens");
}
private async Task<decimal> CalcularTotal(List<ItemRequest> itens)
{
var total = 0m;
foreach (var item in itens)
{
var produto = await _produtoRepo.GetById(item.ProdutoId);
total += produto.Preco * item.Quantidade;
}
return total;
}
private decimal CalcularDesconto(decimal total)
=> total > 500 ? total * 0.1m : 0m;
private Pedido MontarPedido(PedidoRequest request, decimal total, decimal desconto) => new()
{
ClienteId = request.ClienteId,
Itens = request.Itens,
Total = total - desconto,
Desconto = desconto,
CriadoEm = DateTime.UtcNow
};
Agora cada método é testável isoladamente e o fluxo principal se lê como uma história.
Guard Clauses: elimine nesting desnecessário
Código com múltiplos if aninhados é difícil de acompanhar. Use guard clauses para retornar cedo e manter o caminho principal sem indentação.
// ❌ Pirâmide da morte
public decimal CalcularFrete(Pedido pedido)
{
if (pedido != null)
{
if (pedido.Itens.Any())
{
if (pedido.Endereco != null)
{
if (pedido.Endereco.Estado == "SP")
return 0m;
else
return pedido.PesoTotal * 2.5m;
}
else
{
throw new InvalidOperationException("Endereço obrigatório");
}
}
}
return 0m;
}
// ✅ Guard clauses — limpo e linear
public decimal CalcularFrete(Pedido pedido)
{
if (pedido is null || !pedido.Itens.Any())
return 0m;
if (pedido.Endereco is null)
throw new InvalidOperationException("Endereço obrigatório");
if (pedido.Endereco.Estado == "SP")
return 0m;
return pedido.PesoTotal * 2.5m;
}
Evite comentários óbvios
Comentários devem explicar o porquê, nunca o o quê. Se o código precisa de comentário para ser entendido, o problema é o código — não a falta de comentário.
// ❌ Comentário que repete o código
// Incrementa o contador
contador++;
// Verifica se o usuário é admin
if (usuario.Role == "Admin")
// Retorna a lista de produtos
return produtos;
// ✅ Comentário que agrega valor real
// Margem de segurança exigida pela LGPD: dados devem ser
// anonimizados 30 dias antes do prazo legal de retenção
var prazoAnonimizacao = prazoLegal.AddDays(-30);
// Circuit breaker: após 5 falhas, pausa chamadas por 60s
// para evitar cascata de timeout no gateway de pagamento
services.AddCircuitBreaker(failures: 5, pauseSeconds: 60);
SOLID na prática (sem enrolação)
Single Responsibility — uma classe, um motivo para mudar
// ❌ Classe que faz tudo
public class PedidoService
{
public void Criar(Pedido p) { /* salva no banco */ }
public void EnviarEmail(Pedido p) { /* envia notificação */ }
public decimal CalcularImposto(Pedido p) { /* calcula ICMS */ }
public byte[] GerarPdf(Pedido p) { /* gera nota fiscal */ }
}
// ✅ Cada classe com sua responsabilidade
public class PedidoService { /* apenas orquestra o fluxo */ }
public class NotificacaoService { /* apenas envia emails/SMS */ }
public class ImpostoCalculator { /* apenas calcula tributos */ }
public class NotaFiscalGenerator { /* apenas gera PDF */ }
Dependency Inversion — dependa de abstrações
// ❌ Acoplado a implementação concreta
public class RelatorioService
{
private readonly SqlRelatorioRepository _repo = new();
public List<Relatorio> Gerar() => _repo.BuscarTodos();
}
// ✅ Depende da abstração — testável e substituível
public class RelatorioService
{
private readonly IRelatorioRepository _repo;
public RelatorioService(IRelatorioRepository repo)
{
_repo = repo;
}
public List<Relatorio> Gerar() => _repo.BuscarTodos();
}
Agora você pode testar com um mock, trocar SQL por MongoDB, ou adicionar cache — sem tocar no RelatorioService.
Use recursos modernos do C#
O C# evoluiu muito. Aproveite as features das versões recentes para escrever código mais conciso sem perder clareza.
Pattern Matching
// ❌ Cadeia de if-else
public string ClassificarCliente(Cliente c)
{
if (c.TotalCompras > 10000)
return "VIP";
else if (c.TotalCompras > 5000)
return "Gold";
else if (c.TotalCompras > 1000)
return "Silver";
else
return "Standard";
}
// ✅ Switch expression
public string ClassificarCliente(Cliente c) => c.TotalCompras switch
{
> 10000 => "VIP",
> 5000 => "Gold",
> 1000 => "Silver",
_ => "Standard"
};
Records para Value Objects
// ❌ Classe verbosa para dados imutáveis
public class Endereco
{
public string Rua { get; }
public string Cidade { get; }
public string Estado { get; }
public Endereco(string rua, string cidade, string estado)
{
Rua = rua;
Cidade = cidade;
Estado = estado;
}
// Ainda precisa de Equals, GetHashCode, ToString...
}
// ✅ Record — imutável, com Equals e ToString de graça
public record Endereco(string Rua, string Cidade, string Estado);
Null handling expressivo
// ❌ Verificações verbosas
string cidade;
if (cliente != null && cliente.Endereco != null)
cidade = cliente.Endereco.Cidade;
else
cidade = "Não informada";
// ✅ Null-conditional + null-coalescing
var cidade = cliente?.Endereco?.Cidade ?? "Não informada";
Checklist rápido de Clean Code
Antes de abrir um PR, passe por esta lista:
- Os nomes explicam a intenção sem precisar de comentário?
- Os métodos têm no máximo 20 linhas?
- Cada classe tem uma única responsabilidade?
- Removeu código comentado e variáveis não utilizadas?
- Tem guard clauses em vez de ifs aninhados?
- As dependências são injetadas (não instanciadas com
new)? - Os comentários explicam o porquê, não o o quê?
- Os testes cobrem o comportamento, não a implementação?
Conclusão
Clean Code não é sobre seguir regras cegamente — é sobre empatia com o próximo dev que vai ler seu código (que muitas vezes é você mesmo, 3 meses depois). Em C#, a linguagem oferece ferramentas poderosas para escrever código expressivo: records, pattern matching, null-coalescing, LINQ. Use-as a seu favor, mantenha métodos curtos, nomes claros e responsabilidades separadas. O resultado é um código que não precisa de manual de instruções.