Complexidade ciclomática: como medir e reduzir a complexidade do código
Entenda a métrica de McCabe, como contar os caminhos lógicos de uma função e aplique técnicas práticas para reduzir complexidade e facilitar os testes.

Toda função tem um "custo de entendimento": quanto mais caminhos lógicos ela contém, mais difícil é raciocinar sobre ela, testá-la e mantê-la. A complexidade ciclomática é uma métrica criada justamente para colocar um número nesse custo. Neste artigo você vai aprender a calculá-la, entender o que ela diz sobre seu código e dominar técnicas concretas para reduzi-la.
O que é complexidade ciclomática?
A complexidade ciclomática é uma métrica de software proposta por Thomas McCabe em 1976 que mede o número de caminhos linearmente independentes através do código de um programa (McCabe, 1976). Em termos práticos, ela conta quantas decisões (desvios de fluxo) existem em uma função.
A intuição é direta: um código sem nenhum if, laço ou desvio tem um único caminho de execução do início ao fim — complexidade 1. Cada ponto de decisão (if, while, for, case, operador lógico) adiciona um novo caminho possível, aumentando o número.
Esse número importa por três razões:
Vale uma distinção que confunde muita gente: complexidade ciclomática não é o mesmo que linhas de código. Uma função de 200 linhas que apenas atribui campos sequencialmente tem complexidade 1 — ela é longa, mas tem um único caminho. Já uma função de 15 linhas cheia de if aninhados pode ter complexidade 10. A métrica mede ramificação de fluxo, não tamanho.
Como calcular a complexidade ciclomática
Existem algumas formas equivalentes de calcular a métrica. A mais prática no dia a dia é a contagem de pontos de decisão.
Método prático: contar decisões + 1
Comece com 1 (o caminho base) e some 1 para cada uma destas construções:
Repare em duas sutilezas. Primeiro, um else puro não soma — ele não introduz uma nova condição, apenas captura o caminho restante do if que já foi contado. Segundo, operadores lógicos contam porque criam curto-circuito: em a && b, a expressão b só é avaliada se a for verdadeiro, o que é, na prática, um desvio de fluxo escondido.
Veja um exemplo:
function classificarUsuario(usuario) {
if (usuario.idade < 18) { // +1
return 'menor'
} else if (usuario.idade < 60) { // +1
if (usuario.assinante && usuario.ativo) { // +1 (if) +1 (&&)
return 'assinante-ativo'
}
return 'adulto'
} else {
return 'idoso'
}
}Contagem: base 1 + 4 decisões = complexidade ciclomática 5. Isso significa que precisamos de pelo menos 5 casos de teste para cobrir todos os caminhos independentes dessa função.
Método formal: grafo de fluxo de controle
A definição original de McCabe usa a teoria de grafos. Se você representar o programa como um grafo de fluxo de controle com E arestas (transições) e N nós (blocos de código), a complexidade V(G) é:
V(G) = E - N + 2Ponde P é o número de componentes conexos (geralmente 1 para uma única função). Os dois métodos chegam ao mesmo valor; a contagem de decisões é apenas um atalho prático do cálculo formal.
Para conectar os dois mundos, pense em cada if como um nó que se divide em duas arestas (verdadeiro e falso) que depois se reencontram. Cada divisão dessas adiciona exatamente +1 ao resultado de E - N + 2, que é precisamente o que a contagem de decisões captura. Por isso os dois métodos sempre coincidem — um é a teoria, o outro é o atalho.
Interpretando os valores
Embora não haja um limiar universal, faixas amplamente usadas servem como referência:
| Complexidade | Interpretação | |---|---| | 1–4 | Baixa — fácil de testar e manter | | 5–7 | Moderada — atenção, mas aceitável | | 8–10 | Alta — considere refatorar | | 11+ | Muito alta — risco elevado, refatore |
Esses números são guias, não leis. Um switch com muitos casos simples e paralelos pode ter complexidade alta sem ser difícil de entender. Use a métrica como um sinalizador que pede uma segunda olhada, não como um veredicto automático.
McCabe propôs originalmente 10 como um limiar de atenção, mas ele mesmo admitia exceções para casos como grandes switch. O valor exato importa menos do que a consistência: escolha um limiar para o seu time e aplique-o de forma previsível.
Por que reduzir a complexidade importa
Funções complexas violam um princípio fundamental do desenvolvimento de software: manter as coisas simples. O princípio KISS (Keep It Simple, Stupid) defende exatamente isso — a solução mais simples que resolve o problema costuma ser a melhor. Complexidade ciclomática alta quase sempre significa que estamos resolvendo o problema de forma mais complicada do que o necessário.
Robert C. Martin argumenta que funções devem ser pequenas e fazer uma única coisa (Martin, 2008). Funções que fazem uma coisa só raramente acumulam muitos desvios. A complexidade ciclomática, nesse sentido, é um termômetro objetivo da aderência a esse princípio de Clean Code.
Há também um custo cognitivo concreto. A pesquisa sobre carga cognitiva sugere que a mente humana lida mal com muitos estados simultâneos. Cada ramo adicional em uma função é mais um estado que o leitor precisa manter na cabeça para entender o todo. Reduzir a complexidade não é purismo estético: é diminuir a chance de que você — ou quem mantém o código depois — esqueça de um caso e introduza um bug.
Técnicas para reduzir a complexidade ciclomática
A boa notícia é que reduzir a métrica é, na prática, aplicar técnicas conhecidas de refatoração. Vamos às principais.
1. Extrair método/função
Quebre uma função grande em funções menores, cada uma com poucos caminhos. A complexidade total do sistema não desaparece, mas se distribui em unidades menores e independentemente testáveis.
// Antes: tudo em uma função
function processarPedido(pedido) {
if (pedido.itens.length === 0) return 'vazio'
if (!pedido.pagamento) return 'sem-pagamento'
if (pedido.pagamento.tipo === 'cartao' && !pedido.pagamento.aprovado) {
return 'recusado'
}
// ...mais regras
}
// Depois: responsabilidades extraídas
function processarPedido(pedido) {
if (!pedidoTemItens(pedido)) return 'vazio'
if (!pedidoTemPagamento(pedido)) return 'sem-pagamento'
if (!pagamentoAprovado(pedido.pagamento)) return 'recusado'
// ...
}Um detalhe importante: extrair funções reduz a complexidade de cada função individual, mas a soma das complexidades pode até subir um pouco. Isso é desejável. O que torna o código difícil não é a complexidade total do sistema, e sim a concentração dela em um único lugar que você precisa entender de uma vez. Distribuir é a meta.
2. Cláusulas de guarda (early return)
Em vez de aninhar if dentro de if, retorne cedo nos casos inválidos. Isso achata a estrutura e elimina níveis de aninhamento, reduzindo tanto a complexidade percebida quanto a contagem real.
# Antes: aninhamento profundo
def desconto(usuario):
if usuario.ativo:
if usuario.assinante:
if usuario.anos >= 2:
return 0.3
return 0.1
return 0.0
# Depois: cláusulas de guarda
def desconto(usuario):
if not usuario.ativo:
return 0.0
if not usuario.assinante:
return 0.0
if usuario.anos >= 2:
return 0.3
return 0.13. Substituir condicionais por tabelas ou mapas
Cadeias longas de if/else ou switch que mapeiam um valor para outro podem virar uma estrutura de dados.
// Antes
function rotulo(status) {
if (status === 'novo') return 'Novo'
if (status === 'pago') return 'Pago'
if (status === 'enviado') return 'Enviado'
return 'Desconhecido'
}
// Depois: complexidade cai para 1
const ROTULOS = { novo: 'Novo', pago: 'Pago', enviado: 'Enviado' }
function rotulo(status) {
return ROTULOS[status] ?? 'Desconhecido'
}Essa técnica brilha quando o mapeamento é puramente de dados. Quando cada ramo executa lógica diferente (não apenas devolve um valor), o mapa pode guardar funções em vez de valores — um padrão de despacho que mantém a complexidade de cada função baixa.
4. Polimorfismo no lugar de condicionais por tipo
Quando você tem condicionais que escolhem comportamento com base no "tipo" de algo, substitua por polimorfismo. Cada tipo passa a ter sua própria implementação, e o if que decidia entre eles desaparece.
# Antes: um if que cresce a cada novo tipo
def calcular_frete(pedido):
if pedido.tipo == 'normal':
return pedido.peso * 2
elif pedido.tipo == 'expresso':
return pedido.peso * 5
elif pedido.tipo == 'internacional':
return pedido.peso * 10 + 50
# cada novo tipo exige editar esta função
# Depois: cada tipo encapsula seu cálculo
class FreteNormal:
def calcular(self, peso): return peso * 2
class FreteExpresso:
def calcular(self, peso): return peso * 5
# a escolha vira um lookup; a função de cálculo tem complexidade 1Esse padrão também melhora a extensibilidade: adicionar um novo tipo significa criar uma nova classe, sem tocar no código existente — o princípio aberto/fechado em ação.
5. Decompor expressões booleanas complexas
Condições com muitos && e || aumentam a complexidade e dificultam a leitura. Extraia-as para variáveis ou funções com nomes que explicam a intenção.
# Antes
if usuario.idade >= 18 and usuario.verificado and not usuario.bloqueado:
...
# Depois
pode_comprar = usuario.idade >= 18 and usuario.verificado and not usuario.bloqueado
if pode_comprar:
...Curiosamente, extrair a expressão para uma variável reduz a complexidade do if, mas a complexidade não some — ela se desloca. Se você extrair para uma função pode_comprar(usuario), a contagem dos operadores lógicos passa a viver lá dentro. O ganho real é a legibilidade: o nome documenta a intenção e o teste pode focar a regra isoladamente.
Complexidade ciclomática e testes
Como a métrica indica o número de caminhos independentes, ela tem uma relação direta com a quantidade de testes automatizados necessários para uma cobertura completa. Uma função com complexidade 8 precisa de ao menos 8 casos de teste para exercitar todos os caminhos.
Isso traz uma consequência prática importante: ao reduzir a complexidade, você também reduz o número de testes necessários e torna cada teste mais focado. Funções simples são funções fáceis de testar — e essa é uma das maiores recompensas de manter a métrica baixa.
Há uma sutileza sobre cobertura que vale conhecer. Cobrir 100% das linhas não é o mesmo que cobrir 100% dos caminhos. Uma função com dois if independentes tem complexidade 3, mas combinar as condições gera 4 cenários distintos de entrada. A complexidade ciclomática garante o número de caminhos linearmente independentes — uma base sólida de testes —, mas casos de borda combinatórios ainda merecem atenção extra.
Medindo na prática com ferramentas
Você não precisa contar à mão. A maioria das linguagens tem ferramentas que calculam a complexidade ciclomática automaticamente:
Um exemplo de configuração no ESLint que falha o build acima do limiar:
{
"rules": {
"complexity": ["error", 10]
}
}Integre essas ferramentas ao seu pipeline para que funções acima de um limiar sejam sinalizadas automaticamente. Isso transforma a métrica em parte da rotina de code review: em vez de discutir subjetivamente se uma função "está complicada demais", o time tem um dado objetivo para embasar a conversa e sugerir a refatoração.
Erros comuns ao trabalhar com a métrica
Complexidade ciclomática vs. complexidade cognitiva
Uma evolução importante da métrica original vale ser conhecida. A complexidade ciclomática trata todos os pontos de decisão como iguais: um if plano e um if aninhado em quatro níveis somam o mesmo +1 cada. Mas, para um leitor humano, aninhamento profundo é desproporcionalmente mais difícil. A complexidade cognitiva, popularizada pelo SonarQube, corrige isso penalizando o aninhamento: quanto mais fundo um desvio, maior o peso que ele recebe.
Compare estas duas funções, ambas com complexidade ciclomática semelhante:
# Plana — complexidade cognitiva baixa
def a(x):
if x < 0: return "neg"
if x == 0: return "zero"
return "pos"
# Aninhada — mesma quantidade de decisões, mas cognitivamente pior
def b(x, y):
if x > 0:
if y > 0:
if x > y:
return "caso raro"A função b é visivelmente mais difícil de seguir. Usar as duas métricas em conjunto dá um retrato mais fiel: a ciclomática para estimar testes, a cognitiva para estimar esforço de leitura.
Cuidados ao usar a métrica
A complexidade ciclomática mede caminhos lógicos, não legibilidade real. Ela ignora nomes ruins, abstrações erradas e acoplamento. Por isso:
Perguntas frequentes
Qual é um bom valor de complexidade ciclomática? Como referência, manter funções abaixo de 10 é uma meta saudável para a maioria dos times. Acima disso, vale revisar. Mas o número ideal depende do contexto e da linguagem.
Complexidade ciclomática conta para a classe inteira ou por função? A métrica é definida por unidade de fluxo — tipicamente por função ou método. Ferramentas costumam reportar por função e, às vezes, somar por arquivo ou classe como indicador agregado.
O else aumenta a complexidade? Não por si só. O ponto de decisão é o if (que já contou +1). O else apenas captura o caminho alternativo já existente. Já um else if conta, porque introduz uma nova condição.
Reduzir complexidade sempre melhora o código? Quase sempre, mas não cegamente. Se a redução vem de extrair abstrações com bons nomes, sim. Se vem de fragmentar lógica coesa em peças desconexas, pode piorar. O julgamento humano continua indispensável.
Conclusão
A complexidade ciclomática transforma uma sensação vaga — "essa função está complicada" — em um número acionável. Calcular é simples: conte os pontos de decisão e some 1. Reduzir é aplicar técnicas clássicas de refatoração: extrair funções, usar cláusulas de guarda, substituir condicionais por dados ou polimorfismo e decompor expressões booleanas. Mantida baixa, a métrica leva a um código mais simples, mais fácil de testar e mais barato de manter. Use ferramentas para medi-la continuamente e integrá-la ao seu pipeline, mas lembre-se de que ela é um sinalizador inteligente, não um substituto para o julgamento humano.