Le carnet de détails du compagnon
Sur Le Chantier, personne ne réinvente la façon de poser un linteau au-dessus d'une ouverture. Le problème revient à chaque bâtiment : il faut reprendre la charge du mur quand on perce un vide en dessous. La solution est connue, dessinée, dimensionnée, et transmise d'un chantier au suivant.
Le chef de chantier ne parle pas de « pattern ». Il a un carnet de détails, des habitudes de métier, et un compagnon qui montre au nouveau comment on fait ici. Le résultat est identique : une solution éprouvée, portant un nom, que deux personnes qui ne se sont jamais croisées comprennent de la même manière.
Le logiciel a exactement le même mécanisme, et il porte un nom un peu solennel : les design patterns.
Un pattern ne rend pas ton code meilleur par magie. Il donne un nom partagé à une solution que tu aurais fini par retrouver seul, et il évite que ton voisin la retrouve autrement.
Quatre suffisent pour commencer, et ce sont ceux qu'on croise dans à peu près toutes les applications de gestion : Factory, Repository, Façade et Command. Le billet les prend un par un, avec du code, et dit à chaque fois ce qui compte autant que le pattern lui-même : à quel signe on voit qu'on en a besoin, et à quel signe on voit qu'on en abuse.
Ce qu'est un pattern, et l'avertissement qui va avec
Le billet précédent est resté à l'intérieur du composant : les couches, les interfaces, l'injection de dépendances. Les patterns se logent au même niveau, un cran en dessous des couches. C'est de la micro-architecture : la forme que prennent quelques classes qui collaborent pour résoudre un problème récurrent.
La référence commune date de 1994. Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides publient Design Patterns: Elements of Reusable Object-Oriented Software, resté dans le métier sous le surnom de « livre du Gang of Four ». Vingt-trois patterns, rangés en trois familles. L'idée de départ vient de l'architecture du bâtiment, avec A Pattern Language de Christopher Alexander, ce qui tombe plutôt bien pour cette série.
| Famille | Ce qu'elle traite | Exemples du livre | Dans ce billet |
|---|---|---|---|
| Création | Comment les objets sont fabriqués | Factory Method, Abstract Factory, Builder, Singleton | Factory |
| Structure | Comment les objets s'assemblent | Adapter, Décorateur, Façade, Proxy | Façade |
| Comportement | Comment les objets se répartissent le travail | Command, Observateur, Stratégie, Itérateur | Command |
Le Repository ne figure pas dans ce livre. Il vient du catalogue de Patterns of Enterprise Application Architecture de Martin Fowler (2002), et il s'est imposé avec le Domain-Driven Design. Il a sa place ici parce qu'il traite le problème le plus fréquent d'une application de gestion : la donnée persistée.
Un pattern n'est ni une bibliothèque ni une classe à recopier. C'est une forme, adaptée à chaque fois au contexte, et parfois déjà fournie par le langage. Peter Norvig l'a montré dès 1996 dans sa présentation Design Patterns in Dynamic Programming : seize des vingt-trois patterns du Gang of Four se simplifient ou disparaissent dans un langage dynamique, parce que le langage les offre nativement. Le panorama des design patterns reprend cette critique.
Appliquer un pattern à un problème qu'on n'a pas coûte plus cher que de l'ignorer. Un pattern posé sans besoin ajoute des classes, des indirections et du vocabulaire, sans rien résoudre.
Voici où ces quatre-là se placent dans un composant en couches.
une opération, une séquence"] FAC --> BIZ["Logique métier"] BIZ --> CMD["Command
l'action devient un objet"] BIZ --> FACT["Factory
choisir l'implémentation"] BIZ --> REPO["Repository
lire et écrire sans savoir où"] REPO --> DATA[("Magasin de données")] classDef layer fill:#ede9e1,stroke:#b5651d,stroke-width:2px,color:#1a1a1a; classDef pat fill:#f5f2ec,stroke:#a0413e,stroke-width:2px,color:#1a1a1a; classDef store fill:#2c3338,stroke:#8a4d16,stroke-width:2px,color:#f5f2ec; class UI,BIZ layer; class FAC,CMD,FACT,REPO pat; class DATA store;
Factory : créer sans nommer la classe concrète
Le maçon ne fabrique pas son béton à la pelle au pied du mur. Il passe commande à la centrale : « du C25/30, vingt mètres cubes, mardi matin ». Il ignore la carrière d'origine, l'adjuvant utilisé, le réglage du malaxeur. Il connaît la commande et le résultat attendu.
Côté code, l'équivalent est le mot-clé new. Il a un défaut discret : il oblige celui qui appelle à écrire en dur le nom d'une classe concrète.
// avant : la classe concrète est écrite en dur dans le code métier
public class RapportService {
public byte[] exporter(Facture facture, String format) {
if ("pdf".equals(format)) {
return new ExportPdf().rendre(facture);
} else if ("csv".equals(format)) {
return new ExportCsv().rendre(facture);
}
throw new IllegalArgumentException("Format inconnu : " + format);
}
}
Deux problèmes dans ces dix lignes. RapportService dépend de ExportPdf et ExportCsv à la compilation, donc il ne se teste plus sans elles. Et l'ajout d'un format XLSX impose de rouvrir la classe, ce qui contredit frontalement le principe ouvert/fermé vu dans le billet sur les composants, les couches et SOLID.
La fabrique déplace la décision dans un seul endroit.
public interface Export {
byte[] rendre(Facture facture);
}
public final class ExportFactory {
private static final Map<String, Supplier<Export>> REGISTRE = Map.of(
"pdf", ExportPdf::new,
"csv", ExportCsv::new
);
public static Export pour(String format) {
Supplier<Export> fournisseur = REGISTRE.get(format);
if (fournisseur == null) {
throw new IllegalArgumentException("Format inconnu : " + format);
}
return fournisseur.get();
}
}
public class RapportService {
public byte[] exporter(Facture facture, String format) {
return ExportFactory.pour(format).rendre(facture);
}
}
RapportService ne connaît plus que l'interface Export. Ajouter un format devient une entrée de plus dans le registre, sans toucher au code métier. En pratique, on injecte souvent la fabrique par le constructeur plutôt que de l'appeler statiquement, pour la même raison qu'on injecte tout le reste : la testabilité.
Il existe une version plus légère du même principe, la fabrique nommée : une méthode statique qui remplace le constructeur et dit ce qu'elle fabrique. LocalDate.of(...), List.of(...), Optional.empty(). La dernière section du billet, sur java.time, est bâtie là-dessus.
Le signe qu'on en abuse est facile à reconnaître : une fabrique devant une implémentation unique, stable depuis trois ans, dont personne n'a jamais envisagé de variante.
La fabrique n'existe pas pour éviter le mot-clé new. Elle existe pour que le code qui utilise un objet n'ait pas besoin de connaître le nom de sa classe.Repository : ignorer où la donnée est rangée
Le plaquiste qui monte une cloison au troisième étage ne sait pas si le bâtiment repose sur des semelles filantes ou sur des pieux forés à douze mètres. Ce n'est pas de la négligence, c'est de la répartition des rôles : la nature des fondations relève de l'étude de sol et du gros œuvre, pas du second œuvre.
La règle informatique est la même. Les modules qui ne travaillent pas avec le magasin de données ne doivent pas savoir lequel c'est. Le Repository est le contrat qui rend ça possible : il expose une collection d'objets métier, et rien d'autre.
public interface ClientRepository {
Optional<Client> parId(UUID id);
List<Client> actifsDepuis(LocalDate date);
void enregistrer(Client client);
}
Regarde le vocabulaire. Pas de select, pas de executeQuery, pas de nom de table. Trois méthodes qui parlent de clients. Derrière l'interface, l'implémentation peut interroger PostgreSQL, un document JSON ou une API distante, et le code métier ne bouge pas. Le billet sur le choix du magasin de données explique pourquoi ce choix mérite d'être isolé : c'est celui qu'on refait le moins volontiers.
La confusion la plus courante est avec la couche d'accès aux données. Ce ne sont pas des concurrents : le repository est le plus souvent implémenté par une couche d'accès aux données. La différence tient au contrat exposé.
| Critère | Couche d'accès aux données | Repository |
|---|---|---|
| Vocabulaire | Celui du stockage : insérer, mettre à jour, requêter | Celui du métier : retrouver, lister, enregistrer |
| Granularité | Une opération par table, souvent générée | Une méthode par besoin métier réel |
| Ce qui fuit vers le métier | Le modèle relationnel remonte dans les signatures | Rien, le magasin reste derrière l'interface |
| Effet d'un changement de moteur | L'implémentation change, le métier suit | L'implémentation change, le métier ne bouge pas |
Deux signes d'abus, symétriques. Le repository anémique d'abord : quarante méthodes générées, une par colonne, qui ne font que déléguer à l'ORM sans jamais exprimer un besoin réel. Le repository poreux ensuite : celui qui renvoie un objet de requête et laisse l'appelant finir le travail. Dans les deux cas, la base a traversé la frontière.
Le repository ne cache pas la base pour pouvoir en changer un jour, ce qui arrive rarement. Il la cache pour que le code métier reste lisible sans elle, ce qui sert tous les jours.
Façade : une opération devant une séquence
Sur Le Chantier, le maître d'ouvrage a le choix entre deux montages. Soit il signe avec chaque corps de métier, gère douze contrats et coordonne le planning lui-même. Soit il confie l'ensemble à une entreprise générale : un contrat, un interlocuteur, un numéro de téléphone. Les douze corps de métier existent toujours, ils sont simplement derrière.
La façade fait ça pour une séquence d'appels. Le cas d'école est le virement bancaire : vérifier le compte source, contrôler le plafond, débiter, créditer, écrire l'écriture comptable, notifier. Six étapes dans un ordre imposé, où l'oubli de la cinquième produit un compte juste et une comptabilité fausse.
public class VirementFacade {
private final CompteRepository comptes;
private final ControleLimites limites;
private final JournalComptable journal;
private final Notificateur notificateur;
// constructeur avec injection des quatre dépendances
public ReferenceVirement virer(Iban source, Iban destination, Montant montant) {
Compte debiteur = comptes.parIban(source);
limites.verifier(debiteur, montant);
debiteur.debiter(montant);
Compte crediteur = comptes.parIban(destination);
crediteur.crediter(montant);
comptes.enregistrer(debiteur);
comptes.enregistrer(crediteur);
ReferenceVirement reference = journal.ecrire(source, destination, montant);
notificateur.virementExecute(reference);
return reference;
}
}
L'appelant, qu'il soit une interface web, un traitement par lot ou une API, écrit une ligne. L'ordre des étapes n'est plus une connaissance à partager entre trois équipes, c'est une propriété d'une seule classe.
Point capital : la façade n'invente aucune logique métier. Elle orchestre. Le jour où le calcul du plafond change, il change dans ControleLimites. Si tu vois apparaître des if sur le type de compte à l'intérieur de la façade, la logique est en train de migrer au mauvais endroit, et la classe finira en objet-dieu.
Le même pattern existe à l'échelle du système, sous le nom de passerelle d'API : un point d'entrée unique devant un ensemble de services. C'est le sujet du billet suivant.
Une façade se juge à ce qu'elle n'expose pas. Si l'appelant doit encore connaître l'ordre des étapes, ce n'est pas une façade, c'est un raccourci.
Command : l'action devient un objet
Le maître d'œuvre n'attrape pas le chef d'équipe dans un couloir pour lui demander de décaler une cloison. Il émet un ordre de service : un document daté, numéroté, qui dit qui fait quoi, sur quel lot, à quelle date. Le document existe indépendamment de son exécution. On peut le classer, le suspendre, le retrouver six mois plus tard, ou en émettre un autre qui l'annule.
Command applique ce principe au code : au lieu d'appeler une méthode, on construit un objet qui contient l'action et ses paramètres. Cet objet peut ensuite être stocké, transmis, différé ou inversé.
public interface Commande {
void executer();
void annuler();
}
public class DeplacerCloison implements Commande {
private final Plan plan;
private final Cloison cloison;
private final Position avant;
private final Position apres;
// constructeur
@Override
public void executer() {
plan.placer(cloison, apres);
}
@Override
public void annuler() {
plan.placer(cloison, avant);
}
}
L'objet transporte l'état d'avant. C'est précisément ce qui débloque l'annulation, impossible avec un simple appel de méthode qui n'a rien gardé.
public class Historique {
private final Deque<Commande> pile = new ArrayDeque<>();
public void lancer(Commande commande) {
commande.executer();
pile.push(commande);
}
public void annulerDerniere() {
if (!pile.isEmpty()) {
pile.pop().annuler();
}
}
}
L'annulation est l'usage le plus visible, ce n'est pas le seul. Dès qu'une action est un objet, elle peut être mise dans une file et exécutée plus tard, sérialisée et envoyée à un autre service, réexécutée après un incident, ou tracée avec son intention plutôt qu'avec son seul résultat. Les systèmes de tâches asynchrones reposent sur cette idée, même quand ils ne prononcent jamais le mot Command.
Le coût est immédiat et se paie en classes. Une classe par action, c'est acceptable pour vingt actions dans un éditeur, beaucoup moins pour habiller trois appels CRUD qu'aucun utilisateur n'annulera jamais.
Command ne se justifie que si tu as besoin de traiter les actions comme des données : les empiler, les différer, les rejouer. Sinon, appelle la méthode.
Les quatre en une page
| Pattern | Problème résolu | Signe qu'on en a besoin | Signe qu'on en abuse |
|---|---|---|---|
| Factory | Choisir une implémentation sans écrire son nom dans le code appelant | Une cascade de if ou un switch sur un type, recopiée à plusieurs endroits |
Une fabrique devant une implémentation unique et stable |
| Repository | Travailler sur des objets métier sans savoir où ils sont rangés | La couche métier importe des classes du pilote de base ou de l'ORM | Une méthode par colonne, ou un repository qui renvoie une requête à finir |
| Façade | Réduire une séquence imposée à une seule opération | Trois appelants répètent la même suite de six appels | La façade grossit et se met à prendre des décisions métier |
| Command | Manipuler une action comme une donnée | Il faut annuler, différer, rejouer ou tracer l'intention | Une classe par action, sans jamais rien empiler ni annuler |
La colonne de droite mérite autant d'attention que celle de gauche. Un pattern posé trop tôt se paie en fichiers à lire et en vocabulaire à apprendre, pour une souplesse dont personne ne se servira.
La littérature du refactoring propose un garde-fou simple, la règle de trois : la première fois on écrit, la deuxième fois on tolère la répétition, la troisième fois on factorise. Trois occurrences réelles valent mieux qu'une abstraction imaginée le premier jour.
Le bon moment pour introduire un pattern arrive quand le problème s'est déjà manifesté trois fois. Pas quand tu as lu le chapitre.
Scénario B : quand Java a refait sa gestion du temps
L'exemple le plus complet de ce billet ne se trouve pas dans une application, mais dans une plateforme entière. La bibliothèque de dates de Java a été refaite de zéro, publiquement, avec un avant et un après documentés.
Le départ : java.util.Date, présent depuis les premières versions du JDK, puis java.util.Calendar. Trois défauts de conception, dont un que ce billet a déjà traité : la création.
// java.util.Date et Calendar : constructeurs ambigus, mois indexé à zéro, objets modifiables
Date d = new Date(96, 0, 15); // le 15 janvier 1996 : année depuis 1900, mois à partir de 0
Calendar c = Calendar.getInstance();
c.set(2026, Calendar.AUGUST, 24); // le mois d'août vaut 7
c.add(Calendar.DAY_OF_MONTH, 30); // l'objet c vient d'être modifié sur place
Les objets sont modifiables, donc deux morceaux de code qui partagent la même instance se marchent dessus. Les constructeurs surchargés ne disent pas ce qu'ils construisent. Et SimpleDateFormat, le formateur associé, n'est pas sûr en contexte concurrent, ce que sa propre documentation signale.
En attendant mieux, une bibliothèque tierce s'est imposée : Joda-Time, écrite par Stephen Colebourne. C'est ce même Colebourne qui a ensuite conduit la JSR-310, Date and Time API, livrée dans Java 8 en mars 2014 sous le paquet java.time.
// java.time : fabriques nommées, objets immuables, mois de 1 à 12
LocalDate naissance = LocalDate.of(1996, 1, 15);
LocalDate depart = LocalDate.of(2026, Month.AUGUST, 24);
LocalDate echeance = depart.plusDays(30); // depart n'a pas bougé
LocalDate lue = LocalDate.parse("2026-08-24");
| Problème de conception | java.util.Date et Calendar |
java.time |
|---|---|---|
| Création | Constructeurs surchargés, sémantique implicite | Fabriques nommées : of, parse, now, from |
| Mutabilité | Objets modifiables après création | Objets immuables, chaque opération renvoie une nouvelle valeur |
| Contexte concurrent | SimpleDateFormat doit être confiné à un fil d'exécution |
DateTimeFormatter immuable, partageable sans précaution |
| Séparation des concepts | Un type unique pour l'instant, la date et l'heure | Types distincts : LocalDate, LocalTime, Instant, Duration, Period |
Le remplacement du constructeur par des fabriques nommées est la clé de voûte. LocalDate.of(2026, 8, 24) et LocalDate.parse("2026-08-24") construisent le même objet, et chacune dit ce qu'elle fait. Une fabrique porte un nom, une surcharge de constructeur n'en a pas. Elle peut aussi renvoyer une instance partagée plutôt qu'une neuve, ce que new interdit par définition.
Détail qui compte pour un architecte : rien n'a été supprimé. java.util.Date est resté, avec des passerelles ajoutées dans les deux sens (Date.from(Instant) et Date.toInstant()). On a posé la nouvelle conception à côté de l'ancienne, avec un chemin de migration, plutôt que de casser des millions de lignes existantes.
Les concepteurs de java.time n'ont pas coché la case « appliquer le pattern Factory ». Ils ont réglé des problèmes précis, et le remède portait un nom déjà connu. C'est le sens de lecture correct : le problème d'abord, le pattern ensuite.Points clés
- Un design pattern est une solution éprouvée à un problème récurrent, avec un nom partagé. Le catalogue de référence reste celui du Gang of Four, publié en 1994.
- Factory découple l'appelant de la classe concrète : le code métier connaît une interface, pas un nom de classe, et l'ajout d'une variante n'ouvre plus le code existant.
- Repository expose une collection d'objets métier et laisse le magasin de données derrière l'interface. C'est le contrat posé devant la couche d'accès aux données, pas son remplaçant.
- Façade réduit une séquence imposée à une opération unique, sans absorber la logique métier des composants qu'elle orchestre. Command transforme une action en objet, ce qui ouvre l'annulation, la file d'attente et la réexécution.
- La colonne « signe qu'on en abuse » vaut la colonne « signe qu'on en a besoin ». Un pattern appliqué sans problème réel n'apporte que de l'indirection à lire.
Dans la série
Palier 3 : Conduire le chantier. Domaine : Solutions.
- Précédent : Architecture des composants : couches, interfaces, injection, SOLID
- Suivant : Couplage lâche, stateless et cache : tenir la charge
- Vue d'ensemble : Architecture logicielle : par où commencer
Pour aller plus loin
- Panorama des design patterns : les familles, l'origine chez Christopher Alexander, et la section critique qui remet les patterns à leur place.
- Le catalogue Repository de Martin Fowler : la définition d'origine, issue de Patterns of Enterprise Application Architecture.
- JSR-310, Date and Time API : la spécification publique derrière
java.time. - Documentation du paquet java.time : la liste des types et le principe d'immuabilité, expliqués par les auteurs.
- Joda-Time : la bibliothèque qui a servi de banc d'essai à la refonte.
- Design Patterns: Elements of Reusable Object-Oriented Software, Gamma, Helm, Johnson et Vlissides, 1994 : les vingt-trois patterns d'origine.
- Pour le niveau juste au-dessus, les couches et l'injection : Architecture des composants.
- Pour ce que le Repository met à l'abri, le choix du stockage : SQL ou NoSQL.