Le mot « factory » apparaît partout dans le code. Une classe UserFactory dans un projet Laravel, une méthode List.of() en Java, une fonction creer_client() dans un script Python, la méthode iterator() des collections Java : toutes se rattachent au terme, d'une façon ou d'une autre.
Ces usages ne désignent pas la même chose. Quand une offre d'emploi ou un entretien mentionne le pattern Fabrique, il s'agit d'une structure précise, la deuxième case du tableau périodique des design patterns, et cette structure n'a qu'un rapport lointain avec la plupart des classes qui portent le nom.
Le blocage vient de cette homonymie. Sans critère pour trier, on range sous la même étiquette une méthode statique de commodité, un générateur de données de test, une fonction à base de switch et le pattern du Gang of Four, puis on ne sait plus quand ce dernier se justifie.
Cet article donne ce critère, montre la Fabrique en place dans Java et dans Django, puis la compare aux voisins avec lesquels on la confond. Il s'adresse aux personnes en reconversion, aux développeurs juniors et aux curieux qui lisent du code orienté objet.
Le problème : un `if` qui grossit à chaque format
Une application exporte des factures. Au départ, seul le CSV existe. Puis arrive le JSON, puis un format demandé par un logiciel comptable. Le code d'export ressemble vite à ceci, présenté ici comme version « avant ».
public String exporter(List<Facture> factures, String format) {
Formateur formateur;
if (format.equals("csv")) {
formateur = new FormateurCsv();
} else if (format.equals("json")) {
formateur = new FormateurJson();
} else {
throw new IllegalArgumentException("Format inconnu : " + format);
}
// ... le reste du traitement, identique pour tous les formats
}
Chaque nouveau format oblige à rouvrir cette méthode et à risquer une régression sur les formats existants. Le traitement commun et le choix de l'objet à créer sont mélangés au même endroit, alors qu'ils changent pour des raisons différentes.
Ce mélange viole le principe ouvert-fermé décrit dans l'article sur les principes SOLID : ajouter un comportement devrait passer par du code nouveau, sans modifier celui qui fonctionne déjà.
Le principe : une méthode de création que les sous-classes redéfinissent
La Fabrique du Gang of Four sépare deux responsabilités. Une classe mère contient le traitement commun et déclare une méthode de création, souvent abstraite. Chaque sous-classe redéfinit cette méthode pour choisir l'objet concret à fabriquer. La classe mère manipule le produit uniquement par son interface, sans jamais connaître sa classe exacte.
Le vocabulaire du livre parle de « créateur » pour la classe qui déclare la méthode et de « produit » pour l'objet fabriqué. Le point qui distingue ce pattern de ses homonymes tient en une phrase : la décision de création passe par l'héritage, une sous-classe à la fois.
Deux bibliothèques courantes l'appliquent sous les yeux de ceux qui les utilisent.
- Java, `Iterable.iterator()`. L'interface
Iterabledéclare une méthodeiterator()dont la Javadoc dit seulement qu'elle renvoie un itérateur sur les éléments. Chaque collection l'implémente et fabrique son propre itérateur. La méthode par défautforEach()se comporte comme une boucleforsur l'objet, et cette boucle obtient ses éléments pariterator(): le traitement commun ignore quelle classe d'itérateur il reçoit. - Django, `get_form_class()`. Dans les vues génériques de Django 6.1,
FormMixin.get_form()instancie le formulaire à partir de la classe renvoyée parget_form_class(). Par défaut, cette méthode renvoie l'attributform_class, etModelFormMixinla redéfinit pour générer unModelFormà partir du modèle. Une vue peut la redéfinir à son tour pour choisir un formulaire différent selon l'utilisateur.
Dans les deux cas, la Fabrique travaille avec le Patron de méthode : la classe mère fixe le déroulé, et l'une des étapes consiste à créer un objet. Cette association est si fréquente que repérer l'une aide à trouver l'autre.
Exemples
Java 25 : un export de factures
L'exemple reprend le problème de départ. Il est écrit pour Java 25, la version LTS en cours au moment où ces lignes sont écrites, et a été exécuté sur un JDK 21 sans modification, puisqu'il n'utilise que des records et des blocs de texte, disponibles depuis Java 16.
import java.util.List;
import java.util.stream.Collectors;
public class ExportDemo {
record Facture(String numero, String client, long montantCentimes) {}
interface Formateur {
String formater(Facture facture);
}
static final class FormateurCsv implements Formateur {
@Override
public String formater(Facture f) {
return "%s;%s;%d".formatted(f.numero(), f.client(), f.montantCentimes());
}
}
static final class FormateurJson implements Formateur {
@Override
public String formater(Facture f) {
return """
{"numero":"%s","client":"%s","montantCentimes":%d}"""
.formatted(f.numero(), f.client(), f.montantCentimes());
}
}
abstract static class Export {
// La méthode de fabrique : chaque sous-classe choisit le formateur.
protected abstract Formateur creerFormateur();
// Le déroulé reste identique pour tous les exports.
public final String exporter(List<Facture> factures) {
Formateur formateur = creerFormateur();
return factures.stream()
.map(formateur::formater)
.collect(Collectors.joining("\n"));
}
}
static final class ExportCsv extends Export {
@Override
protected Formateur creerFormateur() {
return new FormateurCsv();
}
}
static final class ExportJson extends Export {
@Override
protected Formateur creerFormateur() {
return new FormateurJson();
}
}
public static void main(String[] args) {
var factures = List.of(
new Facture("F-001", "Atelier Nord", 125_000),
new Facture("F-002", "Studio Sud", 89_900));
System.out.println(new ExportCsv().exporter(factures));
System.out.println(new ExportJson().exporter(factures));
}
}
L'exécution affiche les deux factures en CSV, F-001;Atelier Nord;125000, puis en JSON. La méthode exporter() est déclarée final : aucune sous-classe ne peut modifier le déroulé, elles peuvent seulement fournir leur formateur. Ajouter un format comptable revient à écrire un formateur et une sous-classe d'Export, sans toucher une ligne du code existant.
Le formateur JSON construit sa chaîne à la main pour garder l'exemple court. Un nom de client contenant un guillemet produirait un JSON invalide, et un vrai projet confie ce travail à une bibliothèque de sérialisation.
Django 6.1 : choisir le formulaire selon l'utilisateur
Dans Django, la Fabrique s'utilise plus qu'elle ne s'écrit. Cette vue redéfinit get_form_class() pour donner un formulaire plus complet aux personnes qui ont le droit de publier. Le code a été relu ligne à ligne, et ses imports vérifiés sur Django 6.1.2.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import UpdateView
from .forms import ArticleAuteurForm, ArticleEditeurForm
from .models import Article
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
model = Article
template_name = "blog/article_form.html"
def get_form_class(self):
if self.request.user.has_perm("blog.publish_article"):
return ArticleEditeurForm
return ArticleAuteurForm
La vue générique continue de valider et d'enregistrer le formulaire comme d'habitude. Seule l'étape de création a changé, et elle tient en quatre lignes. Les formations Python : POO et traitement de données et Les fondamentaux de la programmation Java et POO travaillent l'héritage et la redéfinition de méthodes sur lesquels repose ce mécanisme.
Python 3.15 : la classe passée en argument
En Python, une classe est un objet comme un autre : on peut la passer à une fonction, puis l'appeler pour obtenir une instance. Cette propriété rend la sous-classe de créateur souvent superflue. La fonction reçoit directement « quelque chose qui fabrique un formateur », et ce quelque chose peut être la classe elle-même.
from collections.abc import Callable
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class Facture:
numero: str
client: str
montant_centimes: int
class Formateur(Protocol):
def formater(self, facture: Facture) -> str: ...
class FormateurCsv:
def formater(self, facture: Facture) -> str:
return f"{facture.numero};{facture.client};{facture.montant_centimes}"
def exporter(factures: list[Facture], fabrique: Callable[[], Formateur]) -> str:
formateur = fabrique()
return "\n".join(formateur.formater(f) for f in factures)
print(exporter([Facture("F-001", "Atelier Nord", 125_000)], FormateurCsv))
Le Protocol décrit ce qu'on attend d'un formateur sans imposer d'héritage : toute classe qui possède une méthode formater() avec la bonne signature convient, et un vérificateur de types comme mypy le contrôle avant l'exécution. Le paramètre fabrique accepte la classe FormateurCsv ou une lambda, ce qui couvre les cas où le formateur a besoin d'arguments. On retrouve l'intention de la Fabrique, découpler le traitement de l'objet créé, avec une seule fonction au lieu d'une hiérarchie.
La version Java par héritage et cette version Python répondent au même problème avec les outils que chaque langage rend naturels, et lire l'une aide à reconnaître l'autre dans un projet existant.
Fabrique ou Fabrique abstraite ?
Les deux patterns se suivent dans le tableau et partagent un nom, ce qui entretient la confusion. Leur structure diffère pourtant sur deux points faciles à vérifier dans le code.
- Ce qui est fabriqué. La Fabrique crée un seul type de produit, ici un formateur. La Fabrique abstraite crée une famille de produits qui doivent rester compatibles entre eux, par exemple une requête et une réponse HTTP issues de la même bibliothèque.
- Comment la variation arrive. La Fabrique passe par l'héritage : on redéfinit une méthode dans une sous-classe. La Fabrique abstraite passe par la composition : on transmet un objet fabrique complet, et on le remplace pour changer de famille entière.
Une Fabrique abstraite contient d'ailleurs souvent plusieurs méthodes de fabrique, une par produit de la famille. L'article dédié à la Fabrique abstraite détaille ce cas avec les interfaces PSR-17 de PHP.
Les pièges
Confondre avec la méthode de fabrique statique
List.of() en Java, présentée par sa Javadoc comme une méthode de fabrique statique, renvoie une liste non modifiable. Elle remplace un constructeur par une méthode nommée, capable de renvoyer un objet existant ou une sous-classe cachée. Une méthode statique ne se redéfinit pas dans une sous-classe : il manque donc la pièce centrale du pattern. Le procédé reste utile, sous son propre nom : méthode de fabrique statique.
Prendre les factories de Laravel pour le pattern
La documentation de Laravel présente les factories de modèles comme un moyen de définir des attributs par défaut pour créer des enregistrements en test ou lors du remplissage de la base. Le nom vient de l'idée générale de fabriquer des objets, sans lien avec la structure du Gang of Four. En entretien, citer User::factory() comme exemple de Fabrique expose à une relance difficile.
Créer une sous-classe pour chaque variante
Avec dix formats d'export, la Fabrique produit dix sous-classes d'Export en parallèle des dix formateurs. Cette hiérarchie parallèle coûte en lecture pour un gain faible. En Java moderne, passer un Supplier<Formateur> au constructeur donne le même découplage par composition, dans l'esprit du principe composition plutôt qu'héritage.
static final class Export {
private final Supplier<Formateur> fabrique;
Export(Supplier<Formateur> fabrique) {
this.fabrique = fabrique;
}
String exporter(List<Facture> factures) {
Formateur formateur = fabrique.get();
return factures.stream()
.map(formateur::formater)
.collect(Collectors.joining("\n"));
}
}
// usage
var export = new Export(FormateurCsv::new);
Mon avis : dans un code récent, cette variante est le bon point de départ. La Fabrique par héritage se justifie quand la classe mère porte un vrai traitement commun que les sous-classes ne doivent pas modifier, comme les vues génériques de Django.
Appeler la méthode de fabrique depuis le constructeur
En Java, si le constructeur de la classe mère appelle creerFormateur(), la version de la sous-classe s'exécute avant que les champs de cette sous-classe soient initialisés. Un formateur qui dépend d'un champ de la sous-classe reçoit alors null. L'exemple plus haut évite le problème en appelant la méthode de fabrique dans exporter(), au moment du traitement.
Ajouter une Fabrique là où un `new` suffisait
Une seule implémentation, aucune variation prévue : la Fabrique n'apporte que des fichiers supplémentaires. YAGNI s'applique pleinement, et la Rule of Three donne un repère : attendre la troisième variante avant d'introduire l'abstraction.
Questions fréquentes
Quelle différence entre une Fabrique et un simple constructeur ?
Une fonction qui contient un `switch` et renvoie différents objets, est-ce une Fabrique ?
La Fabrique est-elle utile en Python ?
FormateurCsv à une fonction suffit souvent. Le pattern reste présent dans les frameworks construits sur l'héritage, comme les vues génériques de Django.Comment reconnaître une Fabrique dans un code existant ?
create, make, new ou get_..._class, déclarée dans une classe mère et redéfinie dans des sous-classes, dont le résultat est utilisé par une autre méthode de la classe mère.