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

ElementoConvençãoExemplo
ClasseSubstantivo, PascalCaseNotaFiscalService
MétodoVerbo de ação, PascalCaseCalcularDesconto()
VariávelcamelCase, descritivatotalComImpostos
BooleanoPrefixo is, has, podeisAtivo, possuiDesconto
InterfacePrefixo I + substantivoIPedidoRepository
ConstantePascalCaseMaximoTentativas

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.