Dette Technique : Comment l'Identifier et la Réduire Efficacement
La dette technique est un concept fondamental que tout développeur senior doit maîtriser. Comme une dette financière, elle s'accumule insidieusement et finit par paralyser les équipes si elle n'est pas gérée correctement.
Qu'est-ce que la Dette Technique ?
La dette technique représente le coût futur du travail supplémentaire causé par le choix d'une solution rapide maintenant plutôt qu'une meilleure approche qui prendrait plus de temps.
Les 4 Types de Dette Technique
1. Dette Intentionnelle et Prudente
// Exemple : Release urgente avec TODO explicite
class PaymentService
{
public function processPayment($amount)
{
// TODO: Implémenter la validation avancée après la release
// Ticket JIRA: PAY-123
if ($amount <= 0) {
throw new InvalidArgumentException('Invalid amount');
}
return $this->simplePaymentGateway->charge($amount);
}
}
2. Dette Intentionnelle et Imprudente
// Exemple : "On n'a pas le temps pour les tests"
class UserController
{
public function register(Request $request)
{
// Pas de validation, pas de tests
$user = new User();
$user->email = $request->get('email');
$user->save();
return new Response('OK');
}
}
3. Dette Non-Intentionnelle et Prudente
// Exemple : Évolution des bonnes pratiques
class LegacyService
{
// Code écrit avant PHP 8, maintenant on utiliserait les enums
const STATUS_PENDING = 'pending';
const STATUS_COMPLETED = 'completed';
const STATUS_FAILED = 'failed';
}
// Version moderne
enum PaymentStatus: string
{
case PENDING = 'pending';
case COMPLETED = 'completed';
case FAILED = 'failed';
}
4. Dette Non-Intentionnelle et Imprudente
// Exemple : Manque de compétences
class BadCodeExample
{
public function calculateTotal($items)
{
$total = 0;
foreach ($items as $item) {
if ($item['type'] == 'A') {
$total += $item['price'] * 1.1;
} elseif ($item['type'] == 'B') {
$total += $item['price'] * 1.2;
}
// ... 50 lignes de if/else
}
return $total;
}
}
Comment Identifier la Dette Technique
1. Métriques Automatisées
Avec PHPStan/Psalm
# Installation
composer require --dev phpstan/phpstan
# Configuration phpstan.neon
parameters:
level: 8
paths:
- src
reportUnmatchedIgnoredErrors: false
Métriques de Complexité
# Installation de PHPLOC
composer require --dev phploc/phploc
# Analyse
vendor/bin/phploc src/
# Métriques importantes :
# - Complexité cyclomatique moyenne
# - Lignes de code par méthode
# - Couplage entre classes
2. Indicateurs Humains
Temps de Développement
# Suivi dans votre outil de gestion
Story Points Estimation vs Reality:
- Feature A: Estimé 3j → Réalisé 8j
- Bug Fix: Estimé 2h → Réalisé 1j
- Refactor: Estimé 1j → Réalisé 3j
Fréquence des Bugs
-- Requête pour analyser les bugs récurrents
SELECT
component,
COUNT(*) as bug_count,
AVG(resolution_time) as avg_resolution_time
FROM bugs
WHERE created_date > DATE_SUB(NOW(), INTERVAL 3 MONTH)
GROUP BY component
ORDER BY bug_count DESC;
3. Code Smells Courants en Symfony
Long Parameter List
// ❌ Dette technique
public function createOrder(
$customerId, $productId, $quantity, $couponCode,
$shippingAddress, $billingAddress, $paymentMethod,
$deliveryDate, $specialInstructions, $giftWrap
) {
// ...
}
// âś… Solution avec DTO
class CreateOrderCommand
{
public function __construct(
public readonly int $customerId,
public readonly int $productId,
public readonly int $quantity,
public readonly ?string $couponCode = null,
public readonly Address $shippingAddress,
public readonly Address $billingAddress,
public readonly PaymentMethod $paymentMethod,
public readonly ?\DateTime $deliveryDate = null,
public readonly ?string $specialInstructions = null,
public readonly bool $giftWrap = false
) {}
}
God Object
// ❌ Classe qui fait tout
class UserManager
{
public function createUser() {}
public function sendEmail() {}
public function validatePayment() {}
public function generateReport() {}
public function uploadFile() {}
// ... 50 méthodes
}
// ✅ Séparation des responsabilités
class UserService
{
public function __construct(
private EmailService $emailService,
private PaymentValidator $paymentValidator,
private ReportGenerator $reportGenerator,
private FileUploader $fileUploader
) {}
}
Stratégies de Réduction de la Dette
1. La Règle du Boy Scout
// Avant modification
class OrderService
{
public function processOrder($data)
{
// Code legacy difficile Ă lire
$o = new Order();
$o->cid = $data['customer'];
$o->amt = $data['amount'];
if ($o->amt > 1000) $o->priority = 1;
// ...
}
}
// Après amélioration (même feature + nettoyage)
class OrderService
{
public function processOrder(array $orderData): Order
{
$order = new Order();
$order->setCustomerId($orderData['customer_id']);
$order->setAmount($orderData['amount']);
if ($order->getAmount() > self::HIGH_VALUE_THRESHOLD) {
$order->setPriority(Priority::HIGH);
}
return $order;
}
}
2. Refactoring Progressif avec Strangler Fig
// 1. Interface commune
interface PaymentProcessorInterface
{
public function processPayment(PaymentRequest $request): PaymentResult;
}
// 2. Wrapper du système legacy
class LegacyPaymentAdapter implements PaymentProcessorInterface
{
public function __construct(private OldPaymentSystem $legacySystem) {}
public function processPayment(PaymentRequest $request): PaymentResult
{
// Adaptation des données
$legacyRequest = $this->adaptRequest($request);
$legacyResponse = $this->legacySystem->pay($legacyRequest);
return $this->adaptResponse($legacyResponse);
}
}
// 3. Nouveau système
class ModernPaymentProcessor implements PaymentProcessorInterface
{
public function processPayment(PaymentRequest $request): PaymentResult
{
// Implémentation moderne
return $this->stripeGateway->charge($request);
}
}
// 4. Router qui bascule progressivement
class PaymentRouter
{
public function processPayment(PaymentRequest $request): PaymentResult
{
// Feature flag pour basculer progressivement
if ($this->featureFlag->isEnabled('new_payment_system', $request->getCustomerId())) {
return $this->modernProcessor->processPayment($request);
}
return $this->legacyAdapter->processPayment($request);
}
}
3. Tests de Caractérisation
// Avant refactoring, sécuriser le comportement existant
class LegacyCalculatorTest extends TestCase
{
/** @test */
public function it_preserves_existing_calculation_behavior()
{
$calculator = new LegacyCalculator();
// Capturer le comportement actuel (même s'il semble étrange)
$this->assertEquals(42.5, $calculator->calculate([10, 20, 12.5]));
$this->assertEquals(0, $calculator->calculate([]));
$this->assertEquals(-5, $calculator->calculate([-10, 5]));
// Ces tests garantissent qu'on ne casse rien
}
}
Mise en Place d'une Stratégie
1. Mesure et Suivi
// Service de métriques de dette technique
class TechnicalDebtMetrics
{
public function __construct(
private StatsdClient $statsd,
private CodeAnalyzer $analyzer
) {}
public function recordDailyMetrics(): void
{
$metrics = $this->analyzer->analyze();
$this->statsd->gauge('technical_debt.complexity.average', $metrics->getAverageComplexity());
$this->statsd->gauge('technical_debt.duplication.percentage', $metrics->getDuplicationRate());
$this->statsd->gauge('technical_debt.test_coverage.percentage', $metrics->getTestCoverage());
$this->statsd->gauge('technical_debt.phpstan.errors', $metrics->getStaticAnalysisErrors());
}
}
2. Priorisation avec la Matrice Impact/Effort
# Exemple de classification
High Impact / Low Effort:
- Fixer les warnings PHPStan niveau 6→8
- Ajouter les types de retour manquants
- Remplacer les magic numbers par des constantes
High Impact / High Effort:
- Migrer vers Symfony 7
- Refactorer le système de permissions
- Implémenter CQRS pour les commandes
Low Impact / Low Effort:
- Nettoyer les imports inutilisés
- Uniformiser le style de code
- Ajouter des commentaires PHPDoc
Low Impact / High Effort:
- Réécrire complètement le module legacy
- Changer d'ORM
- Migrer vers une autre architecture
3. Intégration dans le Processus
# Definition of Done étendue
Critères d'acceptation technique:
- ✅ Code review approuvé
- âś… Tests unitaires > 80% coverage
- âś… PHPStan niveau 8 sans erreur
- âś… Pas de duplication > 6 lignes
- ✅ Complexité cyclomatique < 10
- âś… Documentation Ă jour
Outils Recommandés
Analyse Statique
# PHPStan - Analyse statique
composer require --dev phpstan/phpstan-symfony
# Psalm - Alternative Ă PHPStan
composer require --dev vimeo/psalm
# PHP CS Fixer - Style de code
composer require --dev friendsofphp/php-cs-fixer
Métriques de Qualité
# PHPLOC - Métriques de base
composer require --dev phploc/phploc
# PHPMD - Détection de code smell
composer require --dev phpmd/phpmd
# PHPUnit - Coverage
composer require --dev phpunit/phpunit
Automatisation
# .github/workflows/quality.yml
name: Quality Check
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Run PHPStan
run: vendor/bin/phpstan analyse
- name: Run Tests
run: vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Check Coverage
run: |
COVERAGE=$(php -r "echo round(simplexml_load_file('coverage.xml')->project->metrics['coveredstatements'] / simplexml_load_file('coverage.xml')->project->metrics['statements'] * 100, 2);")
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage trop faible: $COVERAGE% < 80%"
exit 1
fi
Conclusion
La gestion de la dette technique n'est pas un projet ponctuel mais un processus continu. En tant que développeur senior à Toulouse, j'ai constaté que les équipes qui réussissent sont celles qui :
- Mesurent régulièrement leur dette
- Priorisent en fonction de l'impact business
- Intègrent la réduction dans leur processus quotidien
- Communiquent les enjeux aux parties prenantes
La dette technique bien gérée devient un avantage concurrentiel : elle permet de livrer rapidement quand nécessaire, tout en maintenant une base de code saine sur le long terme.
Vous avez des questions sur la gestion de la dette technique dans vos projets Symfony ? Contactez-moi pour un audit de votre codebase !