Polar Code 🎭

Command Palette

Search for a command to run...

20
Pièce N°20

CHAPITRE 19 – CHAIN OF RESPONSIBILITY

La plainte déposée au commissariat suivait un circuit. D'abord l'accueil, qui vérifiait les formalités. Puis un agent pour le classement. Ensuite, selon la gravité, elle remontait à un brigadier, puis à un inspecteur. Personne ne traitait tout. Chacun faisait sa partie, et si c'était au-dessus de ses compétences, il passait le dossier au suivant. Le système évitait la surcharge d'un seul point. C'était une Chaîne de Responsabilité. Dans le code, les if/else en pagaille qui décidaient qui fait quoi étaient la version pourrie de ce principe.

Pipeline de traitement

Le pattern Chain of Responsibility évite de coupler l'émetteur d'une requête à son récepteur. Au lieu d'avoir une monstrueuse fonction centrale qui décide (if type A then handlerA else if type B then handlerB...), on crée une chaîne d'objets handlers. Chaque handler a une chance de traiter la requête. S'il ne peut pas (ou ne veut pas), il la passe au suivant dans la chaîne.

La requête voyage ainsi le long d'un pipeline jusqu'à ce qu'un handler la prenne en charge, ou qu'elle arrive en fin de chaîne.

Découplage émetteur / receveur

L'émetteur (le client) ne sait pas qui va traiter sa requête. Il la donne simplement au premier maillon de la chaîne. Les handlers, eux, ne connaissent que leur successeur potentiel. Ce découplage permet :

  • D'ajouter ou de modifier des handlers sans changer le client.
  • De changer l'ordre de la chaîne dynamiquement.
  • D'attribuer des responsabilités à des objets de manière flexible.

Implémentation : Système de validation de dossier

// La requête qui circule dans la chaîne
interface Dossier {
    id: string;
    type: 'POLICE' | 'DOUANE' | 'JUSTICE';
    urgence: 'BASSE' | 'MOYENNE' | 'HAUTE';
    contenu: any;
    valide?: boolean;
    raisonRejet?: string;
}

// L'interface Handler commune
interface Handler {
    setSuccesseur(successeur: Handler): Handler;
    traiter(dossier: Dossier): void;
}

// Handler abstrait avec gestion du successeur intégrée
abstract class AbstractHandler implements Handler {
    private successeur: Handler | null = null;

    public setSuccesseur(successeur: Handler): Handler {
        this.successeur = successeur;
        return successeur; // Permet le chaînage fluide.
    }

    public traiter(dossier: Dossier): void {
        // Si on a un successeur, on lui passe la main.
        if (this.successeur) {
            this.successeur.traiter(dossier);
        } else {
            // Fin de chaîne : dossier non traité par défaut.
            console.log(`📭 Fin de chaîne. Dossier ${dossier.id} non pris en charge.`);
        }
    }
}

// Handlers concrets
class HandlerValidateurFormat extends AbstractHandler {
    public traiter(dossier: Dossier): void {
        console.log(`[ValidateurFormat] Vérification du dossier ${dossier.id}...`);
        if (!dossier.id || !dossier.type) {
            dossier.valide = false;
            dossier.raisonRejet = 'Format invalide (ID ou type manquant)';
            console.log(`❌ Rejeté: ${dossier.raisonRejet}`);
            return; // On arrête la chaîne, le dossier est rejeté.
        }
        console.log('✅ Format OK.');
        // On passe au handler suivant
        super.traiter(dossier);
    }
}

class HandlerFiltreUrgence extends AbstractHandler {
    public traiter(dossier: Dossier): void {
        console.log(`[FiltreUrgence] Analyse urgence: ${dossier.urgence}`);
        if (dossier.urgence === 'HAUTE') {
            console.log('🚨 Dossier urgent, traitement prioritaire déclenché.');
            // On pourrait appeler un traitement spécial et arrêter la chaîne.
            // Ici, on décide de le passer quand même au spécialiste.
        }
        // Dans tous les cas, on passe au suivant.
        super.traiter(dossier);
    }
}

class HandlerSpecialistePolice extends AbstractHandler {
    public traiter(dossier: Dossier): void {
        if (dossier.type !== 'POLICE') {
            // Pas mon domaine, je passe.
            console.log(`[SpecialistePolice] Pas mon domaine (type: ${dossier.type}), je passe.`);
            super.traiter(dossier);
            return;
        }
        console.log(`[SpecialistePolice] Traitement du dossier police ${dossier.id}...`);
        // Logique métier complexe...
        dossier.valide = true;
        console.log('✅ Dossier police traité et accepté.');
        // On pourrait décider de ne PAS passer au suivant (traitement terminal).
        // super.traiter(dossier); // Décommentez pour continuer.
    }
}

class HandlerSpecialisteDouane extends AbstractHandler {
    public traiter(dossier: Dossier): void {
        if (dossier.type !== 'DOUANE') {
            console.log(`[SpecialisteDouane] Pas mon domaine, je passe.`);
            super.traiter(dossier);
            return;
        }
        console.log(`[SpecialisteDouane] Inspection douanière du dossier ${dossier.id}...`);
        if (dossier.conteneu?.contrabande) {
            dossier.valide = false;
            dossier.raisonRejet = 'Contrebande détectée';
            console.log('❌ Saisie !');
            return;
        }
        dossier.valide = true;
        console.log('✅ Dossier douane autorisé.');
    }
}

// Handler "Attrape-tout" en fin de chaîne (optionnel)
class HandlerParDefaut extends AbstractHandler {
    public traiter(dossier: Dossier): void {
        console.log(`[ParDefaut] Dossier ${dossier.id} de type ${dossier.type} non pris en charge par les spécialistes.`);
        dossier.valide = false;
        dossier.raisonRejet = 'Aucun service compétent trouvé';
    }
}

// --- Construction et utilisation de la chaîne ---
function configurerChaîne(): Handler {
    // Création des handlers
    const validateur = new HandlerValidateurFormat();
    const filtreUrgence = new HandlerFiltreUrgence();
    const police = new HandlerSpecialistePolice();
    const douane = new HandlerSpecialisteDouane();
    const parDefaut = new HandlerParDefaut();

    // Assemblage de la chaîne (l'ordre est important !)
    validateur.setSuccesseur(filtreUrgence)
             .setSuccesseur(police)
             .setSuccesseur(douane)
             .setSuccesseur(parDefaut);

    return validateur; // Premier maillon de la chaîne
}

// --- Client ---
const chaîne = configurerChaîne();

console.log('\n=== SCÉNARIO 1 : Dossier Police ===');
const dossier1: Dossier = {
    id: 'POL-001',
    type: 'POLICE',
    urgence: 'MOYENNE',
    contenu: { suspect: 'M. X' }
};
chaîne.traiter(dossier1);
console.log('État final:', dossier1);

console.log('\n=== SCÉNARIO 2 : Dossier Douane avec problème ===');
const dossier2: Dossier = {
    id: 'DOU-777',
    type: 'DOUANE',
    urgence: 'BASSE',
    contenu: { contrabande: true }
};
chaîne.traiter(dossier2);
console.log('État final:', dossier2);

console.log('\n=== SCÉNARIO 3 : Dossier invalide (format) ===');
const dossier3: Dossier = {
    id: '',
    type: 'JUSTICE',
    urgence: 'HAUTE',
    contenu: {}
};
chaîne.traiter(dossier3);
console.log('État final:', dossier3);

console.log('\n=== SCÉNARIO 4 : Dossier sans spécialiste (Justice) ===');
const dossier4: Dossier = {
    id: 'JUS-123',
    type: 'JUSTICE',
    urgence: 'MOYENNE',
    contenu: {}
};
chaîne.traiter(dossier4);
console.log('État final:', dossier4);

Cas middleware

C’est l’application la plus célèbre du pattern. Les frameworks web (Express.js, ASP.NET Core) utilisent le Chain of Responsibility pour leurs middlewares.

// Interface similaire à Express
interface Requete { body: any; headers: any; }
interface Reponse { statusCode: number; end(): void; }
type NextFunction = () => void;
type Middleware = (req: Requete, res: Reponse, next: NextFunction) => void;

class Application {
    private middlewares: Middleware[] = [];

    use(middleware: Middleware): void {
        this.middlewares.push(middleware);
    }

    handleRequest(req: Requete, res: Reponse): void {
        let index = 0;
        const next = () => {
            if (index < this.middlewares.length) {
                const middleware = this.middlewares[index++];
                middleware(req, res, next);
            }
        };
        next(); // Démarre la chaîne
    }
}

// Utilisation
const app = new Application();
app.use((req, res, next) => {
    console.log('Middleware 1: Logging');
    next();
});
app.use((req, res, next) => {
    console.log('Middleware 2: Authentification');
    if (!req.headers['authorization']) {
        res.statusCode = 401;
        res.end();
        return; // On arrête la chaîne
    }
    next();
});
app.use((req, res, next) => {
    console.log('Middleware 3: Traitement métier');
    res.end('OK');
});

// Simuler une requête
const req = { body: {}, headers: { authorization: 'Bearer token' } };
const res = { statusCode: 200, end: () => console.log('Response sent') };
app.handleRequest(req, res);

Avantages et inconvénients

Avantages :

  • Découplage fort entre l'émetteur et les récepteurs.
  • Flexibilité : on peut ajouter, modifier, réorganiser les handlers à la volée.
  • Responsabilité unique : chaque handler a un rôle bien défini.
  • Principe Ouvert/Fermé : nouveaux handlers sans modifier le client.

Inconvénients :

  • Garantie : Il n’y a pas de garantie qu’une requête sera traitée (elle peut traverser toute la chaîne sans être prise). Il faut souvent un handler « par défaut ».
  • Performance : Si la chaîne est longue, traverser tous les handlers a un coût. Ce n’est pas adapté aux opérations critiques en termes de performance pure.
  • Debugging : Suivre le flot d’une requête à travers une chaîne dynamique peut être difficile.

La chaîne était bouclée. Le pattern Chain of Responsibility formalisait ce qui, dans le code spaghetti, était un désordre de conditions. Il remplaçait la logique centrale et rigide par un réseau flexible et évolutif d’experts.

Le prochain pattern comportemental, Command, irait encore plus loin dans le découplage : il transformerait une requête en un objet à part entière, pouvant être paramétré, mis en file d’attente, annulé. L’art de rendre une action manipulable comme une chose.