Como medir e reduzir complexidade ciclomática em código legado
1. Entendendo a complexidade ciclomática e seu impacto em sistemas legados
A complexidade ciclomática, formalmente definida por Thomas McCabe em 1976, é uma métrica que quantifica o número de caminhos linearmente independentes em um código-fonte. Ela é calculada como: M = E - N + 2P, onde E é o número de arestas, N o número de nós no grafo de fluxo de controle e P o número de componentes conectados.
Código legado tende a acumular alta complexidade ciclomática por diversas razões:
- Acúmulo de correções pontuais sem refatoração
- Múltiplos desenvolvedores adicionando condicionais sem visão holística
- Pressão por entregas rápidas que priorizam funcionalidade sobre estrutura
- Documentação deficiente que dificulta entender o fluxo completo
O impacto prático é direto: funções com complexidade ciclomática acima de 10 têm probabilidade 3x maior de conter bugs. Acima de 20, a testabilidade cai drasticamente e o custo de manutenção cresce exponencialmente.
2. Ferramentas e técnicas para medição precisa
Ferramentas de análise estática são essenciais para quantificar essa métrica de forma consistente:
SonarQube (configuração típica):
sonar.exclusions=**/generated/**
sonar.java.file.suffixes=.java
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=squid:MethodCyclomaticComplexity
sonar.issue.ignore.multicriteria.e1.resourceKey=**/test/**
ESLint (para JavaScript/TypeScript):
{
"rules": {
"complexity": ["error", { "max": 10 }]
},
"overrides": [
{
"files": ["*.test.js"],
"rules": { "complexity": ["warn", { "max": 15 }] }
}
]
}
PMD (para Java):
<rule ref="category/java/design.xml/CyclomaticComplexity">
<properties>
<property name="classReportLevel" value="80"/>
<property name="methodReportLevel" value="10"/>
</properties>
</rule>
Interpretação prática dos valores:
- 1-5: Baixa complexidade (código simples, fácil de testar)
- 6-10: Moderada (aceitável, mas requer atenção)
- 11-20: Alta (risco elevado de bugs, refatoração recomendada)
- 21+: Muito alta (crítico, refatoração obrigatória)
3. Estratégias de refatoração para redução imediata
Extração de métodos é a técnica mais direta. Considere este código legado:
function processOrder(order) {
let total = 0;
let discount = 0;
let tax = 0;
if (order.items.length > 0) {
for (let item of order.items) {
if (item.quantity > 0) {
total += item.price * item.quantity;
if (item.category === 'electronics') {
discount += total * 0.05;
}
}
}
}
if (total > 1000) {
discount += total * 0.1;
}
if (order.customer.type === 'vip') {
discount += total * 0.15;
}
tax = total * 0.08;
return total - discount + tax;
}
Após extração e aplicação de cláusulas de guarda:
function processOrder(order) {
if (!order?.items?.length) return 0;
const total = calculateTotal(order.items);
const discount = calculateDiscount(order, total);
const tax = calculateTax(total);
return total - discount + tax;
}
function calculateTotal(items) {
return items
.filter(item => item.quantity > 0)
.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function calculateDiscount(order, total) {
let discount = 0;
if (total > 1000) discount += total * 0.1;
if (order.customer.type === 'vip') discount += total * 0.15;
return discount;
}
4. Lidando com estruturas de decisão complexas
Substituição de switch/case por polimorfismo:
Antes (complexidade ciclomática = 7):
function calculateShipping(type, weight) {
let cost;
switch (type) {
case 'standard':
cost = weight * 0.5;
if (weight > 10) cost += 5;
break;
case 'express':
cost = weight * 1.2;
if (weight > 5) cost += 10;
break;
case 'overnight':
cost = weight * 2.5;
if (weight > 2) cost += 15;
break;
default:
cost = weight * 0.3;
}
return cost;
}
Depois (complexidade ciclomática = 2 por estratégia):
const shippingStrategies = {
standard: { baseRate: 0.5, threshold: 10, surcharge: 5 },
express: { baseRate: 1.2, threshold: 5, surcharge: 10 },
overnight: { baseRate: 2.5, threshold: 2, surcharge: 15 },
};
function calculateShipping(type, weight) {
const strategy = shippingStrategies[type] || { baseRate: 0.3, threshold: Infinity, surcharge: 0 };
let cost = weight * strategy.baseRate;
if (weight > strategy.threshold) cost += strategy.surcharge;
return cost;
}
5. Abordagens para loops e iterações com alta complexidade
Loops com múltiplas responsabilidades são fontes comuns de alta complexidade:
Antes (complexidade ciclomática = 8):
function processTransactions(transactions) {
let validCount = 0;
let totalAmount = 0;
let fraudAlerts = [];
let summary = {};
for (let i = 0; i < transactions.length; i++) {
const t = transactions[i];
if (t.amount > 0 && t.status === 'completed') {
validCount++;
totalAmount += t.amount;
if (t.amount > 10000) {
fraudAlerts.push({ id: t.id, reason: 'high_value' });
}
const month = new Date(t.date).getMonth();
summary[month] = (summary[month] || 0) + t.amount;
}
}
return { validCount, totalAmount, fraudAlerts, summary };
}
Depois (complexidade ciclomática = 2 por função):
function processTransactions(transactions) {
const validTransactions = transactions.filter(t => t.amount > 0 && t.status === 'completed');
return {
validCount: validTransactions.length,
totalAmount: calculateTotalAmount(validTransactions),
fraudAlerts: detectFraud(validTransactions),
summary: buildMonthlySummary(validTransactions)
};
}
function calculateTotalAmount(transactions) {
return transactions.reduce((sum, t) => sum + t.amount, 0);
}
function detectFraud(transactions) {
return transactions
.filter(t => t.amount > 10000)
.map(t => ({ id: t.id, reason: 'high_value' }));
}
function buildMonthlySummary(transactions) {
return transactions.reduce((summary, t) => {
const month = new Date(t.date).getMonth();
summary[month] = (summary[month] || 0) + t.amount;
return summary;
}, {});
}
6. Estabelecendo limites e governança para evitar regressão
Para evitar que a complexidade retorne, estabeleça:
- Limites por camada:
- Controllers: max 5
- Services: max 10
- Repositories: max 8
-
Utilitários: max 3
-
Testes unitários como barreira de segurança:
describe('calculateTotal', () => {
it('deve retornar 0 para lista vazia', () => {
expect(calculateTotal([])).toBe(0);
});
it('deve calcular corretamente com itens válidos', () => {
const items = [{ price: 10, quantity: 2 }, { price: 5, quantity: 3 }];
expect(calculateTotal(items)).toBe(35);
});
it('deve ignorar itens com quantidade zero', () => {
const items = [{ price: 10, quantity: 0 }, { price: 5, quantity: 3 }];
expect(calculateTotal(items)).toBe(15);
});
});
- Pipeline CI/CD: Configure gates que bloqueiem merges se a complexidade média por função exceder 8.
7. Caso prático: refatoração incremental de um módulo legado
Módulo original (complexidade ciclomática total: 45):
function validateAndProcessPayment(payment, customer, rules) {
let isValid = true;
let errors = [];
let processedAmount = 0;
if (payment.amount > 0) {
if (payment.method === 'credit_card') {
if (payment.cardNumber && payment.cardNumber.length === 16) {
if (payment.expiryDate > new Date()) {
if (payment.cvv && payment.cvv.length === 3) {
processedAmount = payment.amount * 1.03;
} else { errors.push('Invalid CVV'); isValid = false; }
} else { errors.push('Card expired'); isValid = false; }
} else { errors.push('Invalid card number'); isValid = false; }
} else if (payment.method === 'paypal') {
if (payment.email && payment.email.includes('@')) {
processedAmount = payment.amount * 1.02;
} else { errors.push('Invalid email'); isValid = false; }
} else {
errors.push('Unsupported payment method');
isValid = false;
}
} else {
errors.push('Invalid amount');
isValid = false;
}
return { isValid, errors, processedAmount };
}
Após refatoração incremental (complexidade ciclomática total: 12):
function validateAndProcessPayment(payment, customer, rules) {
const errors = [];
if (!isValidAmount(payment.amount)) errors.push('Invalid amount');
if (!isValidPaymentMethod(payment.method)) errors.push('Unsupported payment method');
const methodErrors = validatePaymentMethod(payment);
errors.push(...methodErrors);
if (errors.length > 0) return { isValid: false, errors, processedAmount: 0 };
const fee = getTransactionFee(payment.method);
return {
isValid: true,
errors: [],
processedAmount: payment.amount * fee
};
}
function isValidAmount(amount) {
return amount > 0;
}
function isValidPaymentMethod(method) {
return ['credit_card', 'paypal'].includes(method);
}
function validatePaymentMethod(payment) {
const validators = {
credit_card: (p) => {
const errors = [];
if (!p.cardNumber || p.cardNumber.length !== 16) errors.push('Invalid card number');
if (!p.expiryDate || p.expiryDate <= new Date()) errors.push('Card expired');
if (!p.cvv || p.cvv.length !== 3) errors.push('Invalid CVV');
return errors;
},
paypal: (p) => {
if (!p.email || !p.email.includes('@')) return ['Invalid email'];
return [];
}
};
return validators[payment.method]?.(payment) || ['Unsupported payment method'];
}
function getTransactionFee(method) {
const fees = { credit_card: 1.03, paypal: 1.02 };
return fees[method] || 1;
}
Comparação de métricas:
- Complexidade ciclomática: 45 → 12 (redução de 73%)
- Linhas de código por função: 40 → 10 (média)
- Cobertura de testes possível: 20% → 95%
- Tempo estimado para entender o fluxo: 15 min → 2 min
A refatoração incremental, combinando extração de métodos, cláusulas de guarda e tabelas de decisão, transforma código de difícil manutenção em módulos coesos e testáveis. A chave é medir consistentemente, refatorar em pequenos passos e proteger cada mudança com testes unitários.
Referências
- McCabe Software - Cyclomatic Complexity — Documentação original da métrica de McCabe, com definições formais e histórico da pesquisa
- SonarQube Documentation - Cyclomatic Complexity — Guia oficial de configuração e interpretação da métrica no SonarQube
- ESLint Rules - Complexity — Documentação oficial da regra de complexidade ciclomática para JavaScript/TypeScript
- Refactoring Guru - Simplifying Conditional Expressions — Catálogo de técnicas de refatoração para reduzir complexidade condicional
- Martin Fowler - Refactoring: Improving the Design of Existing Code — Livro de referência com padrões de refatoração que reduzem complexidade ciclomática
- PMD - CyclomaticComplexity Rule — Documentação da regra de complexidade ciclomática para análise estática em Java
- Testing and Cyclomatic Complexity — Artigo técnico relacionando complexidade ciclomática com estratégias de teste unitário