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.

flowchart TB UI["Interface de service"] --> FAC["Façade
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();
        }
    }
}
sequenceDiagram participant IHM participant Historique participant Commande participant Plan IHM->>Commande: construire avec avant et apres IHM->>Historique: lancer la commande Historique->>Commande: exécuter Commande->>Plan: placer la cloison en position apres Historique->>Historique: empiler la commande IHM->>Historique: annuler la dernière Historique->>Commande: annuler Commande->>Plan: replacer la cloison en position avant

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.

Pour aller plus loin