Polar Code 🎭

Command Palette

Search for a command to run...

19
Pièce N°19

CHAPITRE 18 : PROBLÈMES DE COMPORTEMENT

La salle des archives était un labyrinthe. Des dossiers sur des étagères, des formulaires dans des classeurs, des procédures affichées au mur. Un type voulait une information. Il allait au bureau A : « Désolé, pas mon rayon, allez au B. » Le B renvoyait au C. Le C disait : « Ça dépend de la date, voyez avec le D. » La logique était dispersée, les responsabilités mal délimitées. Le système était une machine de Rube Goldberg comportementale. Ce n’était plus une question de structure, mais de comportement. De comment les objets parlent, décident, délèguent.

Logique dispersée

La première maladie comportementale : la logique du métier éclatée en mille if/else ou switch dans tout le code.

class ProcesseurPaiement {
    traiter(paiement: Paiement, utilisateur: Utilisateur) {
        // Logique de validation dispersée
        if (paiement.montant <= 0) { throw new Error('Montant invalide'); }
        if (!utilisateur.estActif) { throw new Error('Utilisateur inactif'); }
        if (paiement.mode === 'CARTE' && !utilisateur.carteValide) { throw new Error('Carte invalide'); }
        if (paiement.mode === 'PAYPAL' && !utilisateur.paypalLie) { throw new Error('PayPal non lié'); }
        if (paiement.montant > 1000 && !utilisateur.estVerifie) { throw new Error('Vérification requise'); }
        // ... 20 autres conditions métier ...
        // Puis la logique de traitement proprement dite, noyée.
        this.debiterCompte();
        this.creerTransaction();
    }
}

Chaque nouvelle règle métier demande de fouiller dans cette méthode monolithique. La logique est dispersée dans le sens où elle n’est pas encapsulée dans des objets dédiés. Elle est aussi dupliquée si d’autres endroits ont besoin des mêmes vérifications.

Enchaînement de responsabilités

Le deuxième problème : une action doit être traitée par une série de handlers, mais on ne sait pas à l’avance lequel sera compétent.

// Approche naïve et rigide
function traiterRequete(requete: Requete) {
    if (requete.type === 'A' && requete.urgence === 'FAIBLE') {
        handlerA.traiterFaible(requete);
    } else if (requete.type === 'A' && requete.urgence === 'HAUTE') {
        handlerA.traiterUrgent(requete);
        handlerB.notifier(requete);
    } else if (requete.type === 'B' && requete.origine === 'INTERNE') {
        handlerC.traiterInterne(requete);
    } else if (requete.type === 'B' && requete.origine === 'EXTERNE') {
        handlerD.traiterExterne(requete);
        handlerA.logger(requete);
    }
    // ... un cauchemar de conditions qui mélange qui fait quoi et dans quel ordre.
}

L’enchaînement est figé dans le code. Ajouter un nouveau type de requête ou un nouveau handler signifie retoucher cette fonction centrale, avec un risque élevé de régression.

États complexes

Le troisième problème : un objet dont le comportement change radicalement selon son état interne. Gérer ça avec des flags et des conditions devient illisible.

class Commande {
    private estPayee: boolean = false;
    private estExpediee: boolean = false;
    private estAnnulee: boolean = false;
    private estRetournee: boolean = false;

    ajouterArticle(article: Article) {
        if (this.estPayee) {
            throw new Error('Impossible, commande déjà payée.');
        }
        if (this.estExpediee) {
            throw new Error('Impossible, commande déjà expédiée.');
        }
        if (this.estAnnulee) {
            throw new Error('Impossible, commande annulée.');
        }
        if (this.estRetournee) {
            throw new Error('Impossible, commande retournée.');
        }
        // Logique d'ajout...
    }

    payer() {
        if (this.estPayee) { throw new Error('Déjà payée.'); }
        if (this.estAnnulee) { throw new Error('Annulée.'); }
        // ... vérifications similaires pour chaque état...
        this.estPayee = true;
    }

    // Chaque méthode est encombrée de gardes-fous pour chaque état.
    // Le comportement global de l'objet est une somme de conditions.
}

La logique étatique est disséminée dans toutes les méthodes. Changer un comportement lié à un état (ex: que se passe-t-il si on annule une commande expédiée ?) demande de parcourir plusieurs méthodes.

Algorithmes variables

Le quatrième problème : un algorithme dont certaines étapes doivent être interchangeables ou personnalisables.

class GenerateurRapport {
    generer(donnees: Donnees, format: string, options: Options) {
        // Étape 1: Filtrer
        let donneesFiltrees;
        if (options.filtre === 'ACTIFS') {
            donneesFiltrees = donnees.filter(d => d.estActif);
        } else if (options.filtre === 'RECENTS') {
            donneesFiltrees = donnees.filter(d => d.date > Date.now() - 86400000);
        } else {
            donneesFiltrees = donnees;
        }

        // Étape 2: Trier
        if (options.tri === 'ALPHABETIQUE') {
            donneesFiltrees.sort((a, b) => a.nom.localeCompare(b.nom));
        } else if (options.tri === 'CHRONOLOGIQUE') {
            donneesFiltrees.sort((a, b) => a.date - b.date);
        }

        // Étape 3: Exporter
        if (format === 'JSON') {
            return JSON.stringify(donneesFiltrees);
        } else if (format === 'CSV') {
            return this.convertirEnCSV(donneesFiltrees);
        } else if (format === 'HTML') {
            return this.genererHTML(donneesFiltrees);
        }
        throw new Error('Format non supporté');
    }
}

L’algorithme est une recette dont les ingrédients (filtrage, tri, export) pourraient être choisis indépendamment. Mais ici, ils sont soudés. Ajouter un nouveau filtre, un nouveau tri, un nouveau format, c’est modifier la classe, violant l’OCP.


Je refermai le classeur des procédures. Ces problèmes ne se résolvaient pas en réorganisant les dépendances ou en ajoutant des couches. Il fallait des patterns de comportement. Des modèles pour :

  • Distribuer la logique (Chain of Responsibility, Command).
  • Centraliser la communication complexe (Mediator).
  • Encapsuler des algorithmes interchangeables (Strategy).
  • Gérer les changements d’état propres (State).
  • Notifier des changements à des observateurs (Observer).
  • Sauvegarder et restaurer des états (Memento).
  • Parcourir des collections (Iterator).
  • Définir un squelette d’algorithme (Template Method).
  • Séparer un algorithme de la structure qu’il traverse (Visitor).

La famille des Design Patterns Comportementaux répond à ces maux. Elle ne change pas la structure des objets, elle change la façon dont ils collaborent et se répartissent les tâches.

Le premier d’entre eux, Chain of Responsibility, est la réponse directe à l’enfer des if/else et à la dispersion des responsabilités. Il formalise la chaîne de renvoi, le « passe au suivant ».

C’était le début de la fin. La dernière ligne droite pour maîtriser le chaos logiciel.