Le panier idéal : 5 design patterns qui gagnent leur place (et 2 refusés)

Tout part d'une question de relecture. Je retravaillais ma fiche du livre Design Patterns, celle du Gang of Four, avec des exemples qui sortaient tous du même univers : une boutique en ligne. Les frais de port pour Strategy, la commande pour State, le stock pour Observer. Et la question est tombée : « du coup, le panier idéal, ce serait quoi ? Une classe qui reprend tous les patterns ? »

Excellente question. Mauvaise direction. Et la réponse mérite mieux qu'un paragraphe, parce qu'elle contient à peu près tout ce que je sais sur la conception.

Le piège : la classe Panier ultime

L'intuition de départ est saine : si les cinq exemples viennent de la même boutique, pourquoi ne pas tout assembler ? Le piège, c'est le mot « classe ». Une classe Panier qui empilerait Strategy, Decorator, State, Observer et une Facade « pour montrer », c'est exactement ce que le livre de 1994 appelle la fièvre des patterns, et il met en garde contre elle dès la page 31 : un pattern ne s'applique que lorsque la flexibilité qu'il apporte est réellement nécessaire.

Le panier idéal n'est pas une classe. C'est une petite architecture où chaque pattern occupe le poste exact où il gagne sa place. Et le critère d'embauche tient en une question, la plus utile du livre : qu'est-ce qui va changer ?

Cinq embauches, cinq problèmes nommables

J'ai donc construit le tunnel de commande complet, en partant de zéro et en n'introduisant chaque pattern que lorsqu'une demande réelle le réclamait. Voilà le journal d'embauche :

Les frais de port varient → Strategy. Colissimo, Chronopost, retrait en magasin : trois calculs pour la même question « combien ? ». Une interface FraisDePort, une classe par transporteur. Ajouter Mondial Relay demain : une classe, zéro modification ailleurs.

interface FraisDePort {
    public function calculer(Panier $p): float;
}
class Colissimo implements FraisDePort {
    public function calculer(Panier $p): float { return 4.99; }
}

La promo s'empile → Decorator. « Livraison offerte dès 50 € » n'est pas une propriété des transporteurs, c'est une enveloppe autour d'eux. Même interface que ce qu'elle enveloppe : pour le reste du code, un transporteur décoré est un transporteur.

class FraisOffertsDes implements FraisDePort {
    public function __construct(private FraisDePort $base, private float $seuil) {}
    public function calculer(Panier $p): float {
        return $p->total() >= $this->seuil ? 0.0 : $this->base->calculer($p);
    }
}

La commande a des étapes de vie → State. « Annuler » ne veut pas dire la même chose au panier (vider), payée (rembourser) ou expédiée (retour transporteur). Au lieu d'un if/elseif sur le statut dupliqué dans chaque méthode, la commande porte un objet-état qu'elle remplace à chaque étape, et annuler() délègue. Zéro if, pour toujours.

Payer doit déclencher l'inconnu → Observer. Email, stock, compta, et le SMS que le marketing demandera le mois prochain. La méthode payer() ne doit pas connaître cette liste : elle annonce, et des abonnés réagissent. Une liste de callbacks et une boucle : c'est tout le pattern, et c'est le même que ton addEventListener.

Le contrôleur veut un bouton → Facade. Un objet Checkout reçoit la stratégie de livraison (éventuellement décorée) et le bus d'événements, et expose une méthode commander(). Le code qui reçoit le POST ne connaît que lui.

Le détail qui change tout : où vivent les décisions

Le morceau le plus important du panier idéal n'est aucun des cinq patterns. C'est l'assemblage :

// Le SEUL endroit du programme qui connaît les classes concrètes.
$checkout = new Checkout(
    new FraisOffertsDes(new Colissimo(), 50),   // un Decorator autour d'une Strategy
    $evenements,
);

Tous les choix concrets se prennent à la racine, au moment d'assembler. Checkout reçoit des interfaces : il ne sait pas si la livraison est décorée, ni qui est abonné aux événements. Si tu as lu ma fiche Clean Architecture, tu reconnais la règle de dépendance : les deux livres, écrits à vingt-trois ans d'écart, convergent sur exactement le même geste.

Et la deuxième leçon est ma préférée : la classe la plus centrale est la plus bête. Le Panier lui-même n'utilise aucun pattern. Il additionne des lignes. Les patterns vivent autour de lui, aux endroits qui changent, jamais au centre.

Les deux refus (aussi importants que les embauches)

En route, deux patterns ont postulé et ont été refusés. Un Singleton (Panier::getInstance(), « pour accéder au panier partout ») : c'est une variable globale déguisée, intestable, et l'injection à la racine couvre déjà le besoin. Un Command avec historique undo/redo, « au cas où » : personne n'a demandé d'annuler pas à pas, et un pattern installé pour un besoin imaginaire est de la complexité payée d'avance.

Un bon design se reconnaît autant à ses patterns absents qu'à ses patterns présents. La liste de ce que ton code refuse de faire est plus parlante que la liste de ce qu'il sait faire ; c'est la même idée que le progrès des langages par soustraction, un cran plus bas.

Le résultat, jouable

Tout ça n'est pas resté un dessin. La boutique tourne en vrai, et j'en ai fait le 13ᵉ projet tutoré de la section apprendre : « La boutique aux patterns », où le tunnel se construit pas à pas, avec les prompts IA réels, les premiers jets qui craquent, deux prédictions à faire avant de lire, et un défi final (une deuxième promo, empilée sur la première, sans modifier une seule classe existante).

Chaque bouton de la démo traverse un pattern, et un journal d'événements en bas de page montre les abonnés Observer qui se réveillent quand tu paies. Si tu veux juste lire le squelette, il est aussi dans la fiche du livre, en encadré.

Conclusion

Ce qui me reste de cet exercice, ce n'est pas le code, c'est l'ordre des opérations. Je n'ai pas une seule fois demandé « quel pattern mettre ici ? ». J'ai listé les demandes probables de la boutique, et chaque pattern est arrivé comme une réponse à une demande précise, avec un nom de problème attaché. Les deux fois où un pattern est arrivé sans problème à résoudre, il est reparti.

C'est peut-être la vraie définition du panier idéal : pas celui qui contient le plus de patterns, celui où chaque pattern peut répondre à la question « tu sers à quoi, toi ? » sans bafouiller.

Commentaires (0)