Software Architecture

Refactoring des monolithes avec le principe de responsabilité unique

Les systèmes legacy souffrent souvent de classes surdimensionnées qui gèrent trop de préoccupations. Ce contre-modèle « God Object » rend le code difficile à tester, maintenir et étendre. En appliquant le principe de responsabilité unique (SRP), nous pouvons décomposer ces services monolithiques en unités plus petites et ciblées sans réécrire l'ensemble du système. Ce guide fournit une feuille de route pratique pour implémenter le SRP dans des environnements de production.

Identifier les violations de responsabilité

La première étape consiste à identifier où le SRP est violé. Une classe a une responsabilité unique s'il n'y a qu'une seule raison pour qu'elle change. Dans le code legacy, cela se manifeste souvent par une classe qui gère simultanément l'accès à la base de données, la logique métier et le formatage de l'interface utilisateur.

Recherchez ces signes dans votre base de code existante :

  • Des classes avec des dépendances excessives envers d'autres composants.
  • Des méthodes qui effectuent plusieurs tâches non liées (par exemple, valider des données et envoyer des e-mails).
  • Une forte complexité cyclomatique, indiquant une logique enchevêtrée.
  • Des conflits de fusion fréquents dans des fichiers spécifiques lors du développement collaboratif.

Étape 1 : Extraire la logique métier

La façon la plus sûre de commencer le refactoring est d'extraire la logique métier pure des préoccupations d'infrastructure. Considérez une classe legacy `OrderProcessor` qui gère tout, du calcul des totaux à la mise à jour de la base de données.


// Violation Legacy : God Object
public class OrderProcessor {
    public void processOrder(Order order) {
        // 1. Logique métier
        double tax = order.getTotal() * 0.1;
        order.setTotal(order.getTotal() + tax);

        // 2. Préoccupation d'infrastructure
        databaseConnection.update("orders", order);

        // 3. Effet de bord
        emailService.sendConfirmation(order.getCustomerEmail());
    }
}

Cette classe change pour trois raisons : les règles fiscales changent, les schémas de base de données changent, ou les fournisseurs d'e-mails changent. Pour appliquer le SRP, nous séparons la logique de calcul dans un service dédié.

Étape 2 : Implémenter un motif Stratégie pour la tarification

En extrayant le calcul des taxes, nous créons une interface `TaxCalculator`. Cela isole la logique et nous permet de changer les implémentations sans toucher au flux principal de commande.


// Nouvelle structure conforme au SRP
public interface TaxCalculator {
    double calculate(Order order);
}

public class StandardTaxCalculator implements TaxCalculator {
    public double calculate(Order order) {
        return order.getTotal() * 0.1;
    }
}

// Processeur refactorisé
public class OrderProcessor {
    private final TaxCalculator taxCalculator;
    private final OrderRepository repository;
    private final NotificationService notifier;

    public OrderProcessor(TaxCalculator taxCalculator, OrderRepository repo, NotificationService notifier) {
        this.taxCalculator = taxCalculator;
        this.repository = repo;
        this.notifier = notifier;
    }

    public void processOrder(Order order) {
        order.setTotal(order.getTotal() + taxCalculator.calculate(order));
        repository.save(order);
        notifier.sendConfirmation(order);
    }
}

Étape 3 : Séparer les préoccupations d'infrastructure

Ensuite, nous nous assurons que la logique de persistance et de notification est déléguée à des interfaces. Cela découple le processus métier central des choix technologiques spécifiques. Le `OrderProcessor` ne connaît désormais que les abstractions, respectant le principe d'inversion des dépendances, qui soutient le SRP en gardant la classe concentrée uniquement sur l'orchestration du flux de travail.

Lors de la test de cette nouvelle implémentation, vous pouvez facilement simuler (`mock`) le `TaxCalculator` et le `NotificationService`. Auparavant, le test unitaire du `OrderProcessor` legacy nécessitait une configuration de base de données et de serveur d'e-mails, rendant les tests lents et instables.

Gérer la transition

Refactoriser un grand monolithe est risqué. Utilisez le motif « Strangler Fig » pour atténuer ce risque. Introduisez les nouveaux composants conformes au SRP derrière un drapeau fonctionnel (feature flag). Orientez progressivement le trafic vers la nouvelle implémentation pendant que l'ancien code existe encore. Une fois que vous êtes confiant dans la stabilité des nouveaux modules, supprimez le code legacy.

Les tests automatisés ne sont pas négociables pendant ce processus. Avant de toucher à n'importe quel code legacy, écrivez des tests d'intégration qui couvrent le comportement existant. Ces tests servent de filet de sécurité, garantissant que votre refactoring change la structure mais pas la fonctionnalité.

Conclusion

Appliquer le principe de responsabilité unique au code legacy ne consiste pas à tout réécrire de zéro ; il s'agit d'une amélioration incrémentale. En isolant la logique métier, en séparant les préoccupations d'infrastructure et en utilisant des motifs de conception pour gérer les dépendances, vous pouvez transformer des monolithes fragiles en services maintenables. Commencez petit, testez soigneusement et engagez-vous dans la discipline d'une seule raison de changement par classe. Le résultat est un code plus facile à comprendre, à tester et à étendre pendant des années à venir.

Share: