Een Laravel controller met een method van 120 regels code is werkende code, maar niemand durft er nog aanpassingen in te maken. Dat is het moment waarop je je afvraagt hoe je objectief kunt bepalen of code te ingewikkeld is geworden. Het antwoord zit in cyclomatic complexity, een metric die de vertakkingsstructuur van code vertaalt naar één enkel getal.
Cyclomatic complexity telt het aantal lineair onafhankelijke paden door je code. Simpel gezegd: elk if-statement, elseif, while, for, switch case en catch blok verhoogt de complexity met 1. Een lege method heeft complexity 1, omdat er altijd minimaal één pad door de code loopt. Deze metric werd in 1976 ontwikkeld door Thomas McCabe en is sindsdien een van de meest gebruikte codekwaliteitsmetrics.
De praktische toepassing van complexity metrics
Mijn dagelijkse workflow bevat nu altijd een complexity check voordat ik code naar production push. Ik gebruik hiervoor PHPMD (PHP Mess Detector), dat standaard waarschuwt bij methods met complexity hoger dan 10. Deze drempelwaarde is niet willekeurig gekozen: onderzoek toont aan dat methods met complexity boven de 10 significant meer bugs bevatten en moeilijker te testen zijn.
composer require --dev phpmd/phpmd
./vendor/bin/phpmd app text codesize,unusedcode,naming,design,controversial,cleancode
Dit commando scant mijn hele app directory en rapporteert verschillende metrics, waaronder cyclomatic complexity. Wat ik altijd doe na het runnen van deze check is de output filteren op complexity warnings. Methods die constant opduiken in deze rapporten krijgen prioriteit voor refactoring.
Een voorbeeld van hoge complexity vond ik recent in een payment processing method. De originele code had complexity 18 door nested if-statements voor verschillende payment providers, foutafhandeling en validatie. Het resultaat was een method die niemand durfde aan te raken, ondanks dat er regelmatig bugs in opdoken.
public function processPayment($paymentData)
{
if ($paymentData['provider'] === 'stripe') {
if ($paymentData['amount'] > 0) {
if ($this->validateStripeData($paymentData)) {
try {
$charge = $this->stripeClient->charges->create([
'amount' => $paymentData['amount'],
'currency' => $paymentData['currency'],
'source' => $paymentData['token'],
]);
if ($charge->status === 'succeeded') {
$this->logSuccess($charge);
return ['status' => 'success', 'id' => $charge->id];
} else {
$this->logFailure($charge);
return ['status' => 'failed', 'error' => 'Charge failed'];
}
} catch (Exception $e) {
$this->logException($e);
return ['status' => 'error', 'message' => $e->getMessage()];
}
} else {
return ['status' => 'error', 'message' => 'Invalid Stripe data'];
}
} else {
return ['status' => 'error', 'message' => 'Invalid amount'];
}
} elseif ($paymentData['provider'] === 'paypal') {
// Vergelijkbare geneste logica voor PayPal...
}
// En nog meer providers...
}
Deze method heeft een cyclomatic complexity van 18, wat betekent dat je 18 verschillende test scenarios nodig hebt om alle paden te dekken. Dat is praktisch onhaalbaar en een duidelijk signaal dat refactoring nodig is.
Strategieën voor complexity reductie
Mijn favoriete aanpak voor het verlagen van cyclomatic complexity is het extract method patroon. Door verantwoordelijkheden op te splitsen breng je niet alleen de complexity omlaag, maar maak je de code ook veel leesbaarder. Bij de payment method hierboven extraheerde ik eerst de provider-specifieke logica naar aparte methods.
public function processPayment($paymentData)
{
$validator = $this->getPaymentValidator($paymentData['provider']);
if (!$validator->validate($paymentData)) {
return $this->errorResponse('Invalid payment data');
}
$processor = $this->getPaymentProcessor($paymentData['provider']);
try {
return $processor->process($paymentData);
} catch (Exception $e) {
$this->logException($e);
return $this->errorResponse($e->getMessage());
}
}
private function processStripePayment($paymentData)
{
$charge = $this->stripeClient->charges->create([
'amount' => $paymentData['amount'],
'currency' => $paymentData['currency'],
'source' => $paymentData['token'],
]);
return $charge->status === 'succeeded'
? $this->successResponse($charge->id)
: $this->failureResponse('Charge failed');
}
Door deze refactoring daalde de complexity van de hoofdmethod van 18 naar 4. Elke provider-specifieke method heeft nu een complexity van maximaal 3. Het belangrijkste voordeel is dat ik nu elke method afzonderlijk kan testen en debuggen.
Guard clauses zijn een andere krachtige techniek die ik regelmatig toepas. In plaats van geneste if-statements gebruik je vroege returns om ongeldige condities af te handelen. Dit patroon reduceert niet alleen complexity, maar maakt de happy path van je code veel duidelijker.
public function updateUserProfile($userId, $profileData)
{
$user = User::find($userId);
if (!$user) {
return $this->errorResponse('User not found');
}
if (!$this->hasPermission($user, 'update_profile')) {
return $this->errorResponse('Insufficient permissions');
}
if (!$this->validateProfileData($profileData)) {
return $this->errorResponse('Invalid profile data');
}
// Happy path - alleen de core business logic
$user->update($profileData);
return $this->successResponse($user->fresh());
}
Deze aanpak voorkomt de "arrow anti-pattern" waar je code steeds verder naar rechts indenteert door geneste conditionals. De cyclomatic complexity blijft laag omdat je geen geneste branches hebt, alleen opeenvolgende vroege exits.
Complexity monitoring in development workflow
Integratie van complexity checks in je CI/CD pipeline voorkomt dat complexe code überhaupt in je codebase terechtkomt. Ik gebruik GitHub Actions om bij elke pull request automatisch een complexity rapport te genereren. Dit geeft reviewers objectieve data om refactoring suggesties op te baseren.
name: Code Quality Check
on: [pull_request]
jobs:
complexity-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run PHPMD complexity check
run: ./vendor/bin/phpmd app text codesize --reportfile complexity-report.txt
- name: Comment PR with complexity report
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('complexity-report.txt', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Complexity Report\n\`\`\`\n${report}\n\`\`\``
});
Deze configuratie post automatisch een comment op pull requests met alle complexity violations. Ontwikkelaars zien direct welke methods te complex zijn geworden en kunnen deze refactoren voordat de code gemerged wordt.
Wat ik ook doe is het instellen van complexity trends tracking. Door historical data bij te houden zie je of de overall codekwaliteit van je project verbetert of verslechtert over tijd. Tools zoals SonarQube kunnen dit automatisch voor je bijhouden, maar ik gebruik een simpelere aanpak met een JSON bestand dat complexity metrics per release opslaat.
// In mijn deployment script
$complexityReport = shell_exec('./vendor/bin/phpmd app json codesize');
$data = json_decode($complexityReport, true);
$metrics = [
'date' => date('Y-m-d'),
'version' => $this->getGitVersion(),
'average_complexity' => $this->calculateAverageComplexity($data),
'high_complexity_methods' => $this->countHighComplexityMethods($data),
'total_methods' => count($data['violations'] ?? [])
];
file_put_contents('complexity-history.json',
json_encode(array_merge($this->loadComplexityHistory(), [$metrics]), JSON_PRETTY_PRINT)
);
Deze data helpt me om refactoring prioriteiten te stellen. Als de gemiddelde complexity stijgt, weet ik dat ik tijd moet investeren in code cleanup. Methods die consistent hoge complexity scores hebben worden prioriteit voor refactoring in de volgende sprint.
Cyclomatic complexity is niet de enige metric die telt, maar het geeft wel een solide basis voor objectieve discussies over codekwaliteit. Sinds ik het dagelijks gebruik, merk ik dat code reviews productiever zijn geworden en dat er minder bugs in production komen. Het mooiste vind ik nog steeds dat nieuwe teamleden nu een concrete metric hebben om te leren wat "goede code" betekent, in plaats van dat het een vaag concept blijft.