Vol. 3 · № 01 · Un atelier d'architectureAn architecture workshop

Strategy, ou le comportement qu'on injecteStrategy, or the behavior you inject

Un if/else qui grandit d'une branche à chaque nouveau cas transforme une fonction stable en liste ouverte qu'on rouvre à l'infini. Strategy nomme ce qui varie — la formule — l'extrait derrière un contrat et la passe au code qui s'en sert. Puis le fil rouge du volume : quand le contrat n'a qu'une méthode, l'objet n'était qu'une fonction déguisée.An if/else that grows a branch per case turns a stable function into an open list you reopen forever. Strategy names what varies — the formula — extracts it behind a contract and passes it to the code that uses it. Then the volume's red thread: when the contract has a single method, the object was just a function in disguise.

AudienceAudience
Dev qui veut retirer un switch sans sur-construire une hiérarchieDev who wants to remove a switch without over-building a hierarchy
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●○○○Patterns d'objetsStrategyTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Un if/else qui grandit à chaque nouveau cas. Nomme ce qui varie.An if/else that grows with each new case. Name what varies.

Calculer un frais de port selon le transporteur. La version qu'on écrit tous : un if/elif qui s'allonge d'une branche à chaque transporteur ajouté. Ça marche — et ça transforme une fonction stable (« calculer un coût ») en une liste ouverte de cas qu'il faut rouvrir à l'infini. Ce qui varie ici, c'est l'algorithme de calcul. Le réflexe : isoler ce qui change du squelette qui reste.Computing shipping cost by carrier. The version we all write: an if/elif that grows a branch per added carrier. It works — and turns a stable function ('compute a cost') into an open list of cases you must reopen forever. What varies here is the calculation algorithm. The reflex: isolate what changes from the skeleton that stays.

Le code naïf — un cas par brancheThe naive code — one case per branch
def shipping_cost(order, carrier):
    if carrier == "ups":
        return 5 + 0.5 * order.weight
    elif carrier == "fedex":
        return 8 + 0.4 * order.weight
    elif carrier == "dhl":
        return 3 + 0.9 * order.weight
    # … et demain : "chronopost", "colissimo", "mondial_relay"
Ce qui change vs ce qui resteWhat changes vs what stays
« calculer un coût »'compute a cost'
le squelette · stablethe skeleton · stable
⊃
contientholds
la formule par transporteurthe per-carrier formula
le détail · volatilthe detail · volatile

Le squelette ne change jamais ; la liste des formules change tout le temps. Tant qu'ils vivent dans la même fonction, ajouter un transporteur oblige à rouvrir le calcul commun.The skeleton never changes; the list of formulas changes constantly. As long as they live in the same function, adding a carrier forces you to reopen the shared calculation.

Le réflexe du volumeThe volume's reflex

« Quel détail je veux pouvoir changer sans toucher au reste ? » Ici : la formule de calcul. Strategy répond en sortant chaque algorithme de la fonction pour le rendre interchangeable. C'est l'axe de variation du Vol 0 #1, descendu au niveau d'un seul comportement.'Which detail do I want to change without touching the rest?' Here: the calculation formula. Strategy answers by lifting each algorithm out of the function to make it interchangeable. It's Vol 0 #1's axis of variation, dropped down to a single behavior.

Le fil rouge du volume.The volume's red thread.

Garde une question en tête tout du long : ce « pattern » mérite-t-il une classe ? Strategy, dans un langage où les fonctions sont des valeurs, n'est qu'une fonction passée en paramètre. On va le construire en objets, puis le regarder s'effondrer en une lambda. Le pattern n'est pas la solution — c'est le nom qu'on donne à un manque du langage.Keep one question in mind throughout: does this 'pattern' deserve a class? Strategy, in a language where functions are values, is just a function passed as a parameter. We'll build it in objects, then watch it collapse into a lambda. The pattern isn't the solution — it's the name we give to a language gap.

№ 02La douleurThe pain4 min

Chaque nouveau cas rouvre la même fonction — et la fait tout connaître.Each new case reopens the same function — and makes it know everything.

Le switch ne plante pas : il pourrit lentement. Chaque transporteur ajouté est une branche de plus dans une fonction qui devrait être close. Et comme elle les contient tous, elle finit par dépendre de tout ce dont chaque cas a besoin — un client API ici, une table de tarifs là. Le coût n'est pas la lisibilité d'aujourd'hui ; c'est qu'on doit modifier du code qui marche pour ajouter du code qui n'existait pas.The switch doesn't crash: it rots slowly. Each added carrier is one more branch in a function that ought to be closed. And since it holds them all, it ends up depending on everything each case needs — an API client here, a rate table there. The cost isn't today's readability; it's that you must modify working code to add code that didn't exist.

№ 1 · Ouvert là où il faudrait être ferméOpen where it should be closed

Ajouter Chronopost oblige à rouvrir shipping_cost — une fonction testée, en production — pour y greffer une branche. Le risque de casser UPS en ajoutant DHL est gratuit et permanent.Adding Chronopost forces you to reopen shipping_cost — a tested, in-production function — to graft a branch. The risk of breaking UPS while adding DHL is free and permanent.

№ 2 · La fonction accumule toutThe function accumulates everything

Le cas Mondial Relay a besoin d'appeler une API de points relais. La voilà importée dans la fonction de calcul — qui dépend maintenant du réseau pour un simple barème. Chaque branche traîne ses dépendances dans le tronc commun.The Mondial Relay case needs to call a pickup-point API. Now it's imported into the calculation function — which now depends on the network for a plain tariff. Each branch drags its dependencies into the shared trunk.

№ 3 · On teste tout pour tester unYou test all to test one

Vérifier la formule DHL exige d'instancier le contexte entier et de passer les branches voisines. Impossible d'éprouver un algorithme isolément : ils sont soudés dans le même bloc.Checking the DHL formula means instantiating the whole context and routing past neighboring branches. You can't exercise one algorithm in isolation: they're welded in the same block.

La branche qui contamine le troncThe branch that contaminates the trunk
def shipping_cost(order, carrier):
    if carrier == "mondial_relay":
        pts = relay_api.nearest(order.address)   # ✗ réseau dans le calcul
        return 2 + 0.3 * order.weight + fee(pts)
    elif carrier == "ups": ...                    # et tout le reste reste soudé
Le diagnostic.The diagnosis.

Le détecteur : « pour ajouter un comportement, dois-je modifier un code existant, ou seulement en ajouter ? ». Si la réponse est « modifier », le point de variation est à l'intérieur d'une unité fermée. Le réflexe : extraire chaque variante derrière un contrat commun, pour que le squelette n'ait plus jamais à savoir lesquelles existent.The detector: 'to add a behavior, must I modify existing code, or only add some?'. If the answer is 'modify', the variation point lives inside a closed unit. The reflex: extract each variant behind a common contract, so the skeleton never again has to know which ones exist.

№ 03L'extractionThe extraction5 min

On sort l'algorithme de la fonction, puis on le regarde devenir une fonction.Lift the algorithm out of the function, then watch it become a function.

Strategy nomme un comportement variable, l'extrait derrière un contrat commun, et le passe au code qui s'en sert. Le squelette cesse de connaître les cas : il appelle une abstraction. L'animation vide le switch, transforme chaque branche en stratégie injectable — puis pousse jusqu'au bout du fil rouge du volume : quand le contrat n'a qu'une méthode, l'objet n'était qu'une fonction déguisée.Strategy names a variable behavior, extracts it behind a common contract, and passes it to the code that uses it. The skeleton stops knowing the cases: it calls an abstraction. The animation empties the switch, turns each branch into an injectable strategy — then pushes the volume's red thread to its end: when the contract has a single method, the object was just a function in disguise.

Animation — du switch à la lambdaAnimation — from switch to lambda
Départ — le switchStart — the switch · 1/4
shipping_cost()
if upselif fedexelif dhl
Les trois formules vivent dans la même fonction. Ajouter un transporteur, c'est rouvrir ce bloc.The three formulas live in one function. Adding a carrier means reopening this block.

Le même contrat, deux écrituresThe same contract, two spellings
# Objet (Strategy classique)
interface ShippingCost: cost(order) -> Money
class Ups implements ShippingCost: cost(o) = 5 + 0.5 * o.weight

class Checkout(rate: ShippingCost):
    self.rate.cost(order)             # le contexte appelle l'abstraction

# Fonctionnel — le contrat à une méthode EST le type
ups = lambda o: 5 + 0.5 * o.weight    # ShippingCost = (Order) -> Money
Checkout(rate = ups)                  # même injection, zéro classe
Le motif transférable.The transferable pattern.

« Quand un comportement varie, fais-en un paramètre. » Que ce paramètre soit un objet implémentant une interface ou une fonction passée directement ne change pas l'idée — seulement la cérémonie. C'est aussi le squelette de State (la stratégie qui change d'elle-même dans le temps) et de Template Method (le squelette fixe avec des trous à remplir). Trois noms, une même opération : injecter ce qui change.'When a behavior varies, make it a parameter.' Whether that parameter is an object implementing an interface or a function passed directly doesn't change the idea — only the ceremony. It's also the skeleton of State (the strategy that changes itself over time) and Template Method (the fixed skeleton with holes to fill). Three names, one operation: inject what changes.

Modèle mentalMental model
L'imageThe image

Une perceuse à mandrin : le corps ne change jamais, on visse le foret adapté au matériau — bois, béton, métal. Le même geste entraîne n'importe quel foret ; c'est le foret, pas la perceuse, qui sait percer ce matériau-là.A drill with a chuck: the body never changes, you screw in the bit matched to the material — wood, concrete, metal. The same motion drives any bit; it's the bit, not the drill, that knows how to bore that material.

La règleThe rule

Quand un comportement varie, en faire un paramètre : le contexte appelle le contrat et ignore laquelle des variantes il tient.When a behavior varies, make it a parameter: the context calls the contract and ignores which of the variants it holds.

Où l'analogie casseWhere the analogy breaks

Le foret est un objet qu'on tient en main — il fait croire qu'une stratégie doit être une classe. C'est faux : un contrat à une méthode n'est qu'un type de fonction, et la lambda fait le même travail sans la hiérarchie. L'analogie matérialise ce qui, dans le code, peut n'être qu'une fonction passée en paramètre.The bit is an object you hold — it makes you believe a strategy must be a class. It isn't: a one-method contract is just a function type, and the lambda does the same work without the hierarchy. The analogy materializes what, in code, can be nothing more than a function passed as a parameter.

№ 04Le piègeThe trap4 min

Extraire des classes ne suffit pas. La Strategy que le contexte espionne encore.Extracting classes isn't enough. The Strategy the context still spies on.

Le piège n'est pas d'oublier l'interface — c'est de croire que créer des classes Strategy a éliminé le switch. Si le contexte demande encore « quelle stratégie concrète tiens-je ? » pour un cas spécial, le if/else est revenu, juste déplacé. On a payé le prix de l'abstraction (une interface, des classes) sans en toucher le bénéfice. Bascule le toggle : cherche le isinstance.The trap isn't forgetting the interface — it's believing that creating Strategy classes eliminated the switch. If the context still asks 'which concrete strategy do I hold?' for a special case, the if/else is back, just moved. You paid the abstraction's price (an interface, classes) without reaping its benefit. Flip the toggle: hunt the isinstance.

# le contexte ne dépend QUE de l'abstraction
class Checkout(rate: ShippingCost):
    def total(order):
        return order.subtotal + self.rate.cost(order)   # appelle, ne teste rien

# ajouter Chronopost : une classe de plus, ZÉRO ligne touchée ici
class Chronopost implements ShippingCost:
    def cost(o): return 4 + 0.6 * o.weight
Pourquoi c'est justeWhy it's right

Le contexte appelle rate.cost(order) et rien d'autre : il ignore totalement quelle stratégie il tient. Tout ce qui est propre à un transporteur — y compris ses surcharges — vit dans sa classe. Ajouter Chronopost, c'est écrire une classe et la câbler à la composition root ; Checkout ne bouge pas d'une ligne. Fermé à la modification, ouvert à l'extension.The context calls rate.cost(order) and nothing else: it has no idea which strategy it holds. Everything carrier-specific — including surcharges — lives in its class. Adding Chronopost means writing a class and wiring it at the composition root; Checkout doesn't move a line. Closed to modification, open to extension.

La règle qui tranche.The rule that settles it.

Le contexte ne doit jamais savoir quelle stratégie concrète il tient. Dès qu'apparaît un isinstance, un type tag ou un champ « kind » qu'on inspecte, la Strategy est fausse : le comportement spécial appartient à la stratégie, pas au contexte. Le test : puis-je ajouter une variante sans rouvrir l'appelant ? Sinon, on a déplacé le switch, pas supprimé.The context must never know which concrete strategy it holds. The moment an isinstance, a type tag, or an inspected 'kind' field appears, the Strategy is fake: the special behavior belongs to the strategy, not the context. The test: can I add a variant without reopening the caller? If not, you moved the switch, you didn't remove it.

Le piège inverse : la sur-cérémonie.The opposite trap: over-ceremony.

L'autre excès, côté fil rouge : sortir l'artillerie — interface, trois classes, une factory — pour un comportement d'une ligne qui ne variera qu'à deux valeurs. Si la stratégie tient en une expression, passe une fonction, pas une hiérarchie. La cérémonie d'objets ne se justifie que si une variante a son propre état ou ses propres dépendances.The other excess, on the red-thread side: rolling out the artillery — interface, three classes, a factory — for a one-line behavior that will only ever vary across two values. If the strategy fits in one expression, pass a function, not a hierarchy. The object ceremony is justified only when a variant has its own state or dependencies.

№ 05Bilan & éditoWrap-up & editorial2 min

Le cas branché dans la fonction, ou le comportement passé en paramètre.The case branched inside the function, or the behavior passed as a parameter.

Les deux versions, côte à côteThe two versions, side by side
if/elseif/elseStrategy injectéeInjected Strategy
Ajouter un casAdd a caserouvrir la fonctionreopen the functionune variante de plus, appelant intactone more variant, caller untouched
DépendancesDependenciestoutes dans le tronc communall in the shared trunkchacune dans sa varianteeach in its own variant
Tester un casTest one casevia tout le switchthrough the whole switchla variante, isoléethe variant, in isolation
Coût payéCost paidnul, puis le switch pourritnone, then the switch rotsune abstraction (ou une fonction)one abstraction (or a function)
Idée cléKey ideale cas est codé en durthe case is hardcodedle comportement est un paramètrethe behavior is a parameter

Quand ne pas extraire une StrategyWhen not to extract a Strategy

Strategy a un loyer : une indirection, un contrat à tenir, un câblage à la composition root. On le garde en if/else — et c'est correct — quand :Strategy has rent: an indirection, a contract to maintain, wiring at the composition root. Keep the if/else — and that's fine — when:

  • le comportement ne varie pas, ou ne varie qu'à deux valeurs stables : un if honnête se lit mieux qu'une hiérarchie. Inverser ici, c'est de la cérémonie — YAGNI (Vol 0 #3).the behavior doesn't vary, or varies across only two stable values: an honest if reads better than a hierarchy. Inverting here is ceremony — YAGNI (Vol 0 #3).
  • la variation est une donnée, pas un comportement : si la seule différence entre transporteurs est deux nombres, une table {base, parKilo} battra trois classes. On ne fait des objets que quand les variantes diffèrent par leur logique ou leurs dépendances.the variation is data, not behavior: if the only difference between carriers is two numbers, a {base, perKilo} table beats three classes. Make objects only when variants differ by logic or dependencies.
  • le langage offre déjà les fonctions comme valeurs : alors « Strategy » n'est pas un pattern à instancier mais une fonction à passer. Ne construis la hiérarchie de classes que si une variante a un état ou des dépendances propres.the language already offers functions as values: then 'Strategy' isn't a pattern to instantiate but a function to pass. Build the class hierarchy only if a variant has its own state or dependencies.

Le réflexe n'est donc pas « extrais une Strategy dès qu'il y a un if ». C'est : « ce comportement va-t-il varier, et par sa logique plutôt que par ses données ? Si oui, fais-en un paramètre — fonction si une expression suffit, objet s'il porte un état ; sinon, garde le if et économise l'indirection. »So the reflex isn't 'extract a Strategy whenever there's an if'. It's: 'will this behavior vary, and by its logic rather than its data? If yes, make it a parameter — a function if an expression suffices, an object if it carries state; if not, keep the if and save the indirection.'

Mot de l'éditeurFrom the editor

Strategy est le pattern où l'on voit le plus nettement que le GoF n'est pas un catalogue de solutions mais un catalogue de manques du langage. En Java de 2002, injecter un comportement exigeait une interface et trois classes ; la même intention tient aujourd'hui en une lambda. Le débutant apprend « le pattern Strategy » ; l'architecte apprend l'opération — paramétrer ce qui varie — et reconnaît qu'elle a autant de costumes que de langages.Strategy is the pattern where you see most clearly that the GoF isn't a catalog of solutions but a catalog of language gaps. In 2002 Java, injecting a behavior required an interface and three classes; the same intent fits today in a lambda. The beginner learns 'the Strategy pattern'; the architect learns the operation — parameterize what varies — and recognizes it wears as many costumes as there are languages.

On ouvre ici le volume des patterns d'objets avec sa clé de lecture : ne demande pas « quel pattern appliquer ? » mais « quel détail je veux pouvoir changer sans toucher au reste, et mon langage me le donne-t-il déjà ? ». Les prochains numéros — Observer, Factory — répondent à la même question pour d'autres axes : le récepteur, la création.We open the object-patterns volume here with its reading key: don't ask 'which pattern to apply?' but 'which detail do I want to change without touching the rest, and does my language already give me that?'. The next issues — Observer, Factory — answer the same question for other axes: the receiver, the creation.

Les voisins que ce numéro débloqueThe neighbors this issue unlocks

  • State — une Strategy qui change d'elle-même dans le temps : l'objet remplace sa propre stratégie au gré des transitions (commande → payée → expédiée). Même injection, plus la transition.a Strategy that changes itself over time: the object swaps its own strategy across transitions (order → paid → shipped). Same injection, plus the transition.
  • Template Method — la forme inverse : un squelette fixe avec des trous à remplir par sous-classe, au lieu d'un comportement entier injecté. Strategy compose, Template Method hérite.the inverted shape: a fixed skeleton with holes filled by subclass, instead of a whole behavior injected. Strategy composes, Template Method inherits.
  • Higher-Order Function — la version fonctionnelle de tous les précédents : passer un comportement, c'est passer une fonction. Le « pattern » s'évapore quand le langage traite les fonctions comme des valeurs.the functional version of all the above: passing a behavior is passing a function. The 'pattern' evaporates when the language treats functions as values.

Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it

Tu as vu l'objet s'effondrer en lambda dans l'animation du chapitre 3. Reste le geste qui ancre : l'écrire. Copie ce fichier de tests dans ton langage habituel — quatre stratégies et un contexte, la spec tient dans les noms — et fais-les passer au vert sans revenir au numéro. Le dernier test est le sournois : il vérifie qu'une stratégie écrite APRÈS le contexte passe sans qu'on touche une ligne de Checkout. Si tu as besoin d'un isinstance pour le faire passer, ta Strategy est fausse.You watched the object collapse into a lambda in chapter 3's animation. What's left is the gesture that anchors it: writing it. Copy this test file into your usual language — four strategies and a context, the spec fits in the names — and turn them green without coming back to the issue. The last test is the sneaky one: it checks that a strategy written AFTER the context passes without touching a line of Checkout. If you need an isinstance to make it pass, your Strategy is fake.

test_shipping_strategy.py
# test_shipping_strategy.py — la spec EST le harnais : fais-le passer au vert.
# Checkout ne connaît QUE le contrat « (Order) -> Money ». une variante de plus,
# c'est une valeur de plus — jamais un if dans Checkout. sans lib, sans framework.

def test_une_strategie_calcule_son_cout():
    assert Ups()(order(weight=10)) == 10             # 5 + 0.5*10, appelable directement

def test_le_contexte_delegue_a_la_strategie():
    co = Checkout(rate=FedEx())                       # 8 + 0.4*poids
    assert co.total(order(weight=10, subtotal=100)) == 112

def test_changer_de_strategie_change_le_total():
    o = order(weight=10, subtotal=100)
    assert Checkout(rate=Ups()).total(o) == 110       # 100 + 10
    assert Checkout(rate=Dhl()).total(o) != Checkout(rate=Ups()).total(o)

def test_une_fonction_vaut_une_strategie():
    flat = lambda o: 7                                # le contrat a UNE méthode : une lambda suffit
    assert Checkout(rate=flat).total(order(subtotal=100)) == 107

def test_le_contexte_n_inspecte_jamais_le_type():
    # LE test du pattern : une stratégie inédite, écrite APRÈS Checkout, passe par
    # le même contexte sans lui faire toucher une ligne. sinon le switch est revenu.
    def chronopost(o): return 4 + 0.6 * o.weight
    o = order(weight=10, subtotal=100)
    assert Checkout(rate=chronopost).total(o) == 110  # 100 + 4 + 6
Chez toi, cette semaine.In your codebase, this week.

Débusque ta plus longue chaîne de branches sur un type : grep -rnE "elif .*==" src/ | grep -iE "type|kind|mode|carrier|strateg". Chaque chaîne qui grandit d'une branche par cas est un candidat Strategy. Puis pose la question du chapitre 4 sur le code que tu as « déjà refactoré » : le contexte contient-il un isinstance ou un champ kind qu'il inspecte ? Si oui, le switch n'est pas parti, il a déménagé.Flush out your longest chain of branches on a type: grep -rnE "elif .*==" src/ | grep -iE "type|kind|mode|carrier|strateg". Every chain that grows a branch per case is a Strategy candidate. Then ask chapter 4's question about the code you've "already refactored": does the context hold an isinstance or a kind field it inspects? If so, the switch didn't leave, it relocated.