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

Factory & Builder, ou l'objet reçu prêt à l'emploiFactory & Builder, or the object received ready to use

Un code qui écrit new sur des classes concrètes et assemble ses objets à la main mêle deux savoirs étrangers à sa mission : quel type créer, et comment le monter. Factory isole le choix du type, Builder isole l'assemblage — et ne livre que des objets valides. Puis le fil rouge du volume, poussé à son terme : une Factory n'est qu'une fonction, un Builder des arguments nommés, et le Singleton, l'écarté qui déguise un état global en pattern.Code that writes new on concrete classes and assembles its objects by hand mixes two concerns foreign to its mission: which type to create, and how to assemble it. Factory isolates the type choice, Builder isolates the assembly — and delivers only valid objects. Then the volume's red thread, pushed to its end: a Factory is just a function, a Builder named arguments, and the Singleton, the outcast that dresses up global state as a pattern.

AudienceAudience
Dev qui veut sortir le new de son code sans bâtir une usine pour deux objetsDev who wants the new out of their code without building a factory for two objects
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●●○○Patterns d'objetsFactoryTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Un code qui fabrique ce qu'il veut seulement utiliser.Code that builds what it only wants to use.

ReportService veut une seule chose : rendre un rapport. Mais avant d'appeler render(), il doit choisir la bonne classe d'exporteur et l'assembler — marges, polices, en-tête, filigrane. Deux savoirs étrangers à sa mission se sont infiltrés : quel type concret instancier, et comment le monter pièce par pièce. Ce qui varie ici, ce n'est pas l'usage — c'est la création. Le réflexe : un code qui se sert d'un objet ne devrait ni connaître sa classe concrète, ni savoir l'assembler.ReportService wants one thing: render a report. But before calling render(), it must pick the right exporter class and assemble it — margins, fonts, header, watermark. Two concerns foreign to its mission have crept in: which concrete type to instantiate, and how to assemble it piece by piece. What varies here isn't the usage — it's the creation. The reflex: code that uses an object should know neither its concrete class nor how to assemble it.

Le code naïf — l'usage noyé sous la fabricationThe naive code — usage drowned under construction
class ReportService:
    def export(report, format):
        if format == "pdf":
            exp = PdfExporter(margins=20, font=load_font("Inter"),
                              header=True, watermark=company_logo())   # assemblage
        elif format == "csv":
            exp = CsvExporter(delimiter=";", encoding="utf-8")
        elif format == "html":
            exp = HtmlExporter(theme=load_theme("light"), inline_css=True)
        return exp.render(report)   # … le SEUL truc que ce code voulait faire
Deux savoirs, une seule missionTwo concerns, one mission
quel type ? new ?which type? new?
la création · volatilethe creation · volatile
margins · font · header…margins · font · header…
l'assemblage · volatilethe assembly · volatile
→
pourjust to
render(report)render(report)
l'usage · stablethe usage · stable

render() ne change jamais. La liste des formats et la recette de montage de chacun changent tout le temps. Tant que le service les écrit lui-même, ajouter un format ou une option rouvre le code qui voulait juste rendre.render() never changes. The list of formats and each one's assembly recipe change constantly. As long as the service writes them itself, adding a format or an option reopens the code that only wanted to render.

Le réflexe du volumeThe volume's reflex

« Quel détail je veux pouvoir changer sans toucher au reste ? » Au #1 c'était l'algorithme, au #2 le public ; ici c'est la création. Deux outils, deux questions distinctes : Factory répond à « quel objet construire ? » (le choix du type), Builder répond à « comment le construire ? » (l'assemblage d'un objet complexe). On les traite ensemble parce qu'ils isolent le même intrus : la fabrication, sortie de l'usage.'Which detail do I want to change without touching the rest?' In #1 it was the algorithm, in #2 the audience; here it's the creation. Two tools, two distinct questions: Factory answers 'which object to build?' (the type choice), Builder answers 'how to build it?' (assembling a complex object). We treat them together because they isolate the same intruder: construction, pulled out of usage.

Le pont vers l'injection.The bridge to injection.

Isoler la création, c'est le premier pas vers l'inversion de dépendance (Vol 1) : si le service ne fabrique plus ses collaborateurs, quelqu'un d'autre les lui donne. Poussé à l'échelle d'une application, ce « quelqu'un d'autre » devient le conteneur d'injection (DI container) — un méga-factory câblé à la composition root. On installe ici le réflexe en petit ; on le généralise plus tard.Isolating creation is the first step toward dependency inversion (Vol 1): if the service no longer builds its collaborators, someone else hands them over. Scaled to a whole application, that 'someone else' becomes the DI container — a mega-factory wired at the composition root. We install the reflex small here; we generalize it later.

№ 02La douleurThe pain4 min

Le new se répand, et le constructeur télescope.The new spreads, and the constructor telescopes.

Fabriquer dans le code qui utilise a deux coûts, un par question. Côté Factory : chaque new lie l'appelant à une classe concrète, et le même switch de formats se duplique partout où l'on exporte. Côté Builder : un objet à dix réglages se monte par un constructeur à dix arguments — illisible, et qui laisse passer des objets à moitié construits. L'un couple au type, l'autre rend l'assemblage fragile.Building inside the code that uses has two costs, one per question. On the Factory side: every new ties the caller to a concrete class, and the same format switch duplicates everywhere you export. On the Builder side: a ten-setting object is assembled through a ten-argument constructor — unreadable, and it lets half-built objects through. One couples to the type, the other makes assembly fragile.

№ 1 · Le new couple au concretThe new couples to the concrete

Dès que l'appelant écrit PdfExporter(...), il dépend de cette classe, de son constructeur et de ses dépendances. Le code qui voulait juste render() ne compile plus sans tout l'arbre de fabrication derrière.The moment the caller writes PdfExporter(...), it depends on that class, its constructor and its dependencies. The code that only wanted render() no longer compiles without the whole construction tree behind it.

№ 2 · Le switch de formats se dupliqueThe format switch duplicates

Le if format == … vit dans export(). Demain on exporte aussi depuis un mail, un cron, une API : le même switch est recopié. Ajouter « xlsx » oblige à retrouver chaque copie — un choix de création éparpillé.The if format == … lives in export(). Tomorrow you also export from an email, a cron, an API: the same switch gets copied. Adding 'xlsx' forces you to find every copy — a creation choice scattered everywhere.

№ 3 · Le constructeur télescopeThe telescoping constructor

PdfExporter(margins, font, header, watermark, footer, page_size, …) : dix arguments positionnels, des None partout, et rien n'empêche d'oublier la police. L'objet peut naître invalide — l'erreur n'éclate qu'au render().PdfExporter(margins, font, header, watermark, footer, page_size, …): ten positional arguments, None everywhere, and nothing stops you forgetting the font. The object can be born invalid — the error only surfaces at render().

L'objet né à moitié construitThe object born half-built
exp = PdfExporter(20, None, True, None, None, "A4")  # ✗ font oubliée
# … 200 lignes plus loin, dans un autre fichier :
exp.render(report)   # 💥 AttributeError: NoneType — trop tard pour savoir d'où
Le diagnostic.The diagnosis.

Deux détecteurs, deux outils. « Ce code écrit-il new sur une classe concrète qu'il pourrait ne pas connaître ? » → sors le choix dans une Factory. « Cet objet a-t-il assez d'options pour qu'on puisse le construire à moitié ? » → impose un Builder qui valide avant de livrer. Le réflexe commun : séparer le moment de la création du moment de l'usage, pour qu'aucun objet invalide ne circule et qu'aucun appelant ne nomme un type concret.Two detectors, two tools. 'Does this code write new on a concrete class it might not know?' → pull the choice into a Factory. 'Does this object have enough options that it can be built halfway?' → impose a Builder that validates before delivering. The shared reflex: separate the moment of creation from the moment of usage, so no invalid object circulates and no caller names a concrete type.

№ 03L'assemblage déportéThe relocated assembly5 min

On sort la fabrication de l'usage : le choix, puis le montage.Pull construction out of usage: the choice, then the assembly.

Deux gestes en un. D'abord la Factory absorbe le « quel type » : le service reçoit une abstraction et cesse de connaître les classes concrètes. Ensuite le Builder absorbe le « comment » : l'objet se monte option par option et n'est livré que validé. L'animation déporte d'abord le choix, puis l'assemblage — puis pousse le fil rouge du volume : ces deux patterns ne sont qu'une fonction et des arguments nommés.Two moves in one. First the Factory absorbs the «which type»: the service receives an abstraction and stops knowing the concrete classes. Then the Builder absorbs the «how»: the object is assembled option by option and delivered only once validated. The animation relocates the choice first, then the assembly — then pushes the volume's red thread: these two patterns are just a function and named arguments.

Animation — la fabrication quitte l'usageAnimation — construction leaves usage
Départ — l'usage fabriqueStart — usage builds · 1/4
ReportService
fabrique + utilisebuilds + uses
▶
new PdfExporter(…)new PdfExporter(…)
PdfExporter(…)
classe concrète · coupléconcrete class · coupled
dépend de : classes concrètesdepends on: concrete classes
ReportService tient le choix du type ET l'assemblage. Il dépend de chaque classe concrète et peut produire un objet à moitié monté.ReportService holds the type choice AND the assembly. It depends on every concrete class and can produce a half-built object.

La même création, deux écritures — 3 lignes à retrouverThe same creation, two spellings — 3 lines to recall
# Objet — Factory + Builder explicites
class ExporterFactory:        if format == "pdf":        ...
exp = ExporterFactory.create("pdf")        # l'appelant ignore la classe

# Fonctionnel — la factory EST une fonction, le builder des args nommés
3 ligne(s) à retrouver — écris-la de tête avant de cliquer3 line(s) to recall — write it in your head before clicking
Le motif transférable.The transferable pattern.

« Ne fabrique pas ce que tu utilises ; reçois-le prêt. » Le même geste, à plus grande échelle, devient l'Abstract Factory (une famille d'objets cohérents au lieu d'un seul), le Prototype (créer en clonant un modèle plutôt qu'en appelant un constructeur), et le conteneur d'injection (un factory universel câblé une fois). Un seul principe : qui crée n'est pas qui utilise.'Don't build what you use; receive it ready.' The same move, at larger scale, becomes the Abstract Factory (a family of consistent objects instead of one), the Prototype (creating by cloning a model rather than calling a constructor), and the DI container (a universal factory wired once). One principle: who creates is not who uses.

Modèle mentalMental model
L'imageThe image

Le comptoir d'un café : tu commandes « un café », tu précises les options — lait, sucre, taille — et le barista te tend une tasse finie. Tu ne touches ni la machine ni un breuvage à moitié versé ; tu nommes ce que tu veux et tu le reçois prêt.A café counter: you order 'a coffee', you state the options — milk, sugar, size — and the barista hands you a finished cup. You touch neither the machine nor a half-poured drink; you name what you want and receive it ready.

La règleThe rule

Qui utilise un objet ne le fabrique pas : il nomme le type voulu (Factory), précise les options (Builder), et le reçoit monté et valide.Whoever uses an object doesn't build it: they name the type wanted (Factory), state the options (Builder), and receive it assembled and valid.

Où l'analogie casseWhere the analogy breaks

Le comptoir fait croire qu'il faut toujours un préparateur séparé pour créer. Faux : quand le type ne varie pas et que l'objet ne peut pas naître à moitié monté, le new direct est plus clair — et là où le langage offre les arguments nommés, le « barista » n'est qu'une signature de fonction, pas une classe. On n'embauche un préparateur que si la création porte un vrai choix, ou un vrai risque d'objet incomplet.The counter suggests you always need a separate maker to create. You don't: when the type doesn't vary and the object can't be born half-built, the direct new is clearer — and where the language offers named arguments, the 'barista' is just a function signature, not a class. You hire a maker only when creation carries a real choice, or a real risk of an incomplete object.

№ 04Le piègeThe trap4 min

Cacher le new ne suffit pas. La Factory dont le concret fuit encore.Hiding the new isn't enough. The Factory whose concrete type still leaks.

Le piège n'est pas d'oublier la fonction de création — c'est de croire qu'emballer le new a découplé l'appelant. Si la factory retourne un type concret, ou si l'appelant doit l'inspecter (isinstance) pour atteindre une méthode spécifique, le couplage est intact : on a juste déménagé le switch, et souvent en double. Le new caché, mais la dépendance restée. Bascule le toggle : regarde le type de retour et l'isinstance.The trap isn't forgetting the creation function — it's believing that wrapping the new decoupled the caller. If the factory returns a concrete type, or if the caller must inspect it (isinstance) to reach a specific method, the coupling is intact: you just relocated the switch, often duplicating it. The new is hidden, the dependency remains. Flip the toggle: watch the return type and the isinstance.

# la factory retourne l'ABSTRACTION ; l'appelant n'en sait pas plus
class ExporterFactory:
    def create(format) -> Exporter:        # type de retour = l'interface
        return _REGISTRY[format]()         # une table, pas un switch qui pourrit
exp = ExporterFactory.create(fmt)
exp.render(report)                          # appelle l'interface, rien d'autre
# ajouter "xlsx" : on enregistre une classe. ZÉRO appelant touché.
Pourquoi c'est justeWhy it's right

La factory déclare Exporter en type de retour et l'appelant ne connaît que cette interface : pas d'isinstance, pas de méthode concrète. Tout ce qui est propre à un format vit derrière le contrat. Ajouter « xlsx », c'est enregistrer une classe dans la table ; aucun appelant ne bouge. Le new est caché ET la dépendance est coupée — le seul cas qui mérite le nom de Factory.The factory declares Exporter as its return type and the caller knows only that interface: no isinstance, no concrete method. Everything format-specific lives behind the contract. Adding 'xlsx' means registering a class in the table; no caller moves. The new is hidden AND the dependency is cut — the only case that earns the name Factory.

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

L'appelant ne doit jamais nommer un type concret — ni au new, ni au retour, ni dans un isinstance. Le test : son type de retour est-il l'abstraction, et puis-je ajouter une variante sans rouvrir un seul appelant ? Sinon la factory n'a caché que le new ; le couplage qu'on visait, lui, est resté. Côté Builder, le test jumeau : build() peut-il refuser un objet incomplet ? Sinon il déplace le risque, il ne l'élimine pas.The caller must never name a concrete type — not at the new, not in the return, not in an isinstance. The test: is its return type the abstraction, and can I add a variant without reopening a single caller? If not, the factory only hid the new; the coupling you targeted stayed. On the Builder side, the twin test: can build() refuse an incomplete object? If not, it relocates the risk, it doesn't remove it.

Le piège inverse : la sur-fabrication.The opposite trap: over-construction.

L'autre excès : une AbstractFactory, un Builder et un registre pour instancier un objet à deux champs qu'aucun appelant ne choisit. Si le type ne varie pas et que le constructeur tient en une ligne lisible, le new direct est la bonne réponse — l'indirection serait de la cérémonie (YAGNI, Vol 0 #3). Et son cousin le Singleton : un factory qui impose une instance unique cache un état global et complique le test ; le voisin le plus souvent écarté du volume.The other excess: an AbstractFactory, a Builder and a registry to instantiate a two-field object no caller chooses. If the type doesn't vary and the constructor fits on one readable line, the direct new is the right answer — the indirection would be ceremony (YAGNI, Vol 0 #3). And its cousin the Singleton: a factory that forces a single instance hides global state and complicates testing; the neighbor most often discarded in this volume.

№ 05Bilan & éditoWrap-up & editorial2 min

Le new écrit dans l'usage, ou l'objet reçu prêt à l'emploi.The new written in the usage, or the object received ready to use.

Les deux versions, côte à côteThe two versions, side by side
new directdirect newFactory / BuilderFactory / Builder
Ajouter un typeAdd a typerouvrir chaque appelantreopen every callerenregistrer une classe, appelants intactsregister a class, callers untouched
L'appelant connaîtThe caller knowsla classe concrètethe concrete classl'abstraction seulethe abstraction only
Objet incompletIncomplete objectpossible, casse au runtimepossible, breaks at runtimerefusé par build()refused by build()
Coût payéCost paidnul, puis le new se dupliquenone, then the new duplicatesune indirection (souvent une fonction)one indirection (often a function)
Idée cléKey ideaqui utilise fabriquewho uses, buildsqui utilise reçoit, prêtwho uses, receives it ready

Quand ne pas isoler la créationWhen not to isolate creation

Factory et Builder ont un loyer : une indirection de plus, un contrat à tenir, du câblage à la composition root. On garde le new direct — et c'est correct — quand :Factory and Builder have rent: one more indirection, a contract to maintain, wiring at the composition root. Keep the direct new — and that's fine — when:

  • le type ne varie pas : un seul exporteur, qui ne deviendra pas trois, ne mérite pas une Factory. Cacher un new stable derrière une fonction, c'est de la cérémonie — YAGNI (Vol 0 #3).the type doesn't vary: a single exporter that won't become three doesn't deserve a Factory. Hiding a stable new behind a function is ceremony — YAGNI (Vol 0 #3).
  • le constructeur est simple : deux ou trois champs, aucun invalide possible. Un Builder n'a de sens que si l'objet a assez d'options pour naître à moitié monté ; sinon le constructeur lisible suffit.the constructor is simple: two or three fields, no invalid state possible. A Builder only earns its place when the object has enough options to be born half-built; otherwise the readable constructor is enough.
  • le langage offre déjà les arguments nommés et les valeurs par défaut : alors « Builder » n'est pas une classe à écrire mais une signature, et « Factory » une fonction qui retourne le produit. Ne construis la machinerie d'objets que si la création porte un vrai choix ou un vrai état.the language already offers named arguments and default values: then 'Builder' isn't a class to write but a signature, and 'Factory' a function returning the product. Build the object machinery only if creation carries a real choice or real state.

Le réflexe n'est donc pas « emballe tout new dans une Factory ». C'est : « le type que je crée va-t-il varier, ou l'objet peut-il naître invalide ? Si oui, sors la création de l'usage — une fonction si ça suffit, une classe si elle porte un choix ou un état ; sinon, écris le new et économise l'indirection. »So the reflex isn't 'wrap every new in a Factory'. It's: 'will the type I create vary, or can the object be born invalid? If yes, pull creation out of usage — a function if that suffices, a class if it carries a choice or state; if not, write the new and save the indirection.'

Mot de l'éditeurFrom the editor

Factory et Builder poussent le fil rouge du volume à sa démonstration la plus nette : ce sont des patterns que beaucoup de langages ont avalés. Le Builder de Joshua Bloch — sa réponse au constructeur télescopique de Java — n'est, en Python, Kotlin ou Swift, qu'une signature à arguments nommés avec des défauts. La Factory n'est qu'une fonction qui retourne une abstraction. Ce qui reste, ce n'est pas la classe : c'est l'opération — séparer qui crée de qui utilise — et le jugement de savoir quand elle vaut son loyer.Factory and Builder push the volume's red thread to its clearest demonstration: they're patterns many languages have swallowed. Joshua Bloch's Builder — his answer to Java's telescoping constructor — is, in Python, Kotlin or Swift, just a named-argument signature with defaults. The Factory is just a function returning an abstraction. What remains isn't the class: it's the operation — separate who creates from who uses — and the judgment of when it earns its rent.

Et c'est pourquoi le Singleton est l'écarté du volume : il déguise un état global et une instance imposée en « pattern de création », alors qu'il rend la dépendance invisible et le test difficile — l'inverse exact de ce que Factory cherche. Depuis le #1, une seule clé : ne demande pas « quel pattern du GoF appliquer ? » mais « quel détail je veux changer sans toucher au reste, et mon langage me le donne-t-il déjà ? ». Le #4 pose cette question à l'état plutôt qu'au comportement — et la retourne : quelles transitions veux-tu rendre littéralement impossibles ?And that's why the Singleton is the volume's outcast: it dresses up global state and a forced instance as a 'creational pattern', when it makes the dependency invisible and testing hard — the exact opposite of what Factory seeks. Since #1, one key: don't ask 'which GoF pattern to apply?' but 'which detail do I want to change without touching the rest, and does my language already give me that?'. #4 asks that question of state rather than behavior — and turns it around: which transitions do you want to make literally impossible?

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

  • Abstract Factory — une Factory qui produit une famille d'objets cohérents entre eux (le thème sombre : son exporteur, ses polices, ses icônes) plutôt qu'un seul. Même geste, une dimension de plus : la cohérence du lot.a Factory that produces a family of mutually consistent objects (the dark theme: its exporter, its fonts, its icons) rather than one. Same move, one extra dimension: the batch's consistency.
  • Prototype — créer en clonant un modèle déjà configuré plutôt qu'en rappelant un constructeur. Utile quand l'assemblage coûte cher ou quand l'objet de départ est lui-même la meilleure recette.creating by cloning an already-configured model instead of recalling a constructor. Useful when assembly is expensive or when the starting object is itself the best recipe.
  • Conteneur d'injection (DI)DI container — la Factory portée à l'échelle de l'application : un registre unique qui sait construire et câbler tous les collaborateurs, à la composition root. C'est le pont direct vers l'inversion de dépendance du Vol 1.the Factory scaled to the whole application: a single registry that knows how to build and wire every collaborator, at the composition root. It's the direct bridge to Vol 1's dependency inversion.

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

Tu as vu la fabrication quitter l'usage, puis les deux patterns s'effondrer en une signature dans l'animation du chapitre 3. Reste le geste qui ancre : l'écrire. Copie ce fichier de tests dans ton langage habituel — une factory à registre, un builder qui valide — et fais-les passer au vert sans revenir au numéro. Le dernier test est le sournois : il enregistre un format APRÈS coup et exige que create() le serve sans qu'on rouvre la factory. Si tu dois ajouter un elif pour le faire passer, ta factory est un switch déguisé.You watched construction leave usage, then both patterns collapse into a signature in chapter 3's animation. What's left is the gesture that anchors it: writing it. Copy this test file into your usual language — a registry factory, a builder that validates — and turn them green without coming back to the issue. The last test is the sneaky one: it registers a format AFTER the fact and demands that create() serve it without reopening the factory. If you must add an elif to make it pass, your factory is a disguised switch.

test_exporter_factory.py
# test_exporter_factory.py — la spec EST le harnais : fais-le passer au vert.
# la factory retourne l'ABSTRACTION Exporter ; le builder ne livre qu'un objet
# validé. ajouter un format = enregistrer une classe, zéro appelant touché.

def test_la_factory_retourne_l_abstraction():
    exp = ExporterFactory.create("csv")
    assert isinstance(exp, Exporter)          # le contrat, jamais le type concret

def test_un_format_inconnu_echoue_clairement():
    with raises(UnknownFormat):
        ExporterFactory.create("xml")         # pas de None silencieux qui casse plus loin

def test_le_builder_refuse_l_objet_incomplet():
    b = PdfBuilder().margins(20)              # on oublie la police, obligatoire
    with raises(Incomplete):
        b.build()                             # build() valide AVANT de livrer

def test_le_builder_livre_un_objet_valide():
    exp = PdfBuilder().margins(20).font("Inter").build()
    assert exp.margins == 20 and exp.font == "Inter"

def test_ajouter_un_format_ne_touche_aucun_appelant():
    # LE test du pattern : on enregistre un exporteur APRÈS coup, et create() le
    # sert sans qu'on rouvre la factory ni un seul appelant. c'est l'ouvert/fermé.
    class XlsxExporter(Exporter):
        def render(self, r): return b"xlsx-bytes"
    ExporterFactory.register("xlsx", XlsxExporter)
    assert isinstance(ExporterFactory.create("xlsx"), Exporter)
Chez toi, cette semaine.In your codebase, this week.

Débusque tes deux intrus. Le premier — une mini-factory qui fuit : grep -rnE "if .*==.*: *return " src/ repère les switch qui instancient ; regarde leur type de retour, annonce-t-il l'abstraction ou une classe concrète ? Le second — un constructeur télescopique : grep -rnE "def __init__\(self(, \w+){5,}" src/ (cinq paramètres ou plus). Chacun est un Builder qui s'ignore, ou — plus souvent — un objet qui gagnerait juste des arguments nommés.Flush out your two intruders. First — a leaky mini-factory: grep -rnE "if .*==.*: *return " src/ spots switches that instantiate; check their return type — does it announce the abstraction or a concrete class? Second — a telescoping constructor: grep -rnE "def __init__\(self(, \w+){5,}" src/ (five parameters or more). Each is a Builder in disguise, or — more often — an object that would just benefit from named arguments.