Agile Software Development
Chapitre 13 / 15 · 19 min de lecture

Proxy, passerelles et l'étude de cas Weather Station

Gérer les API tierces et la persistance (Proxy, Gateway), puis l'étude de cas complète de la station météo.

Un système logiciel est traversé de frontières (barriers). Quand on déplace des données vers une base relationnelle, on franchit la frontière de la base ; quand on envoie un message d'une machine à une autre, on franchit celle du réseau. Le danger est connu : si l'on n'y prend garde, le code finit par parler davantage des frontières que du problème à résoudre. Ce chapitre rassemble les patterns qui permettent de franchir ces frontières en gardant le programme centré sur le métier — puis les déploie grandeur nature dans l'étude de cas de la station météo Nimbus-LC, où chaque pattern émerge d'un vrai besoin, jamais d'un catalogue.

Le problème : le métier noyé sous la base

Imaginons un système de panier d'achat (shopping cart) pour un site web : des objets Customer, Order (le panier) et Product. Ajouter un article à une commande, dans le modèle objet, est trivial.

// Le modèle objet ignore tout de la base de données.
class Order {
  private items: Item[] = [];

  ajouterArticle(produit: Product, quantite: number): void {
    this.items.push(new Item(produit, quantite));
  }

  total(): number {
    return this.items.reduce(
      (somme, item) => somme + item.produit.prix() * item.quantite,
      0,
    );
  }
}

Mais ces objets représentent en réalité des lignes d'une base relationnelle. Pour insérer le même article, on est tenté d'écrire du code qui manipule directement le schéma SQL.

// ❌ Le métier disparaît derrière SQL, connexions et chaînes de requête.
class TransactionAjoutArticle {
  ajouterArticle(idCommande: number, sku: string, quantite: number): void {
    const requete = this.connexion.preparer(
      "insert into items values (?, ?, ?)",
    );
    requete.executer(idCommande, sku, quantite);
  }
}

Ces deux fragments accomplissent la même fonction logique — relier un article à une commande — mais le second « glorifie » la base au point d'en oublier le métier. C'est une violation flagrante du principe de responsabilité unique (Single-Responsibility Principle, SRP) : on mêle deux concepts qui changent pour des raisons différentes (les articles et commandes d'un côté, les schémas relationnels et SQL de l'autre). Si l'un de ces concepts doit évoluer, l'autre en sera affecté — ce qui, sous réserve, constitue aussi une atteinte au principe de fermeture commune (Common-Closure Principle, CCP), qui veut que les classes changeant ensemble pour les mêmes raisons soient regroupées et isolées de celles qui changent autrement. C'est enfin une violation du principe d'inversion des dépendances (Dependency-Inversion Principle, DIP), car la politique du programme dépend désormais des détails du mécanisme de stockage. Le pattern Proxy est l'un des remèdes possibles à ces maux.

PROXY

L'idée du Proxy (proxy) est d'interposer, entre le client et l'objet réel, un objet de même interface qui contrôle l'accès. Chaque objet à « proxifier » est scindé en trois parties :

            «interface»
             Product

        ┌───────┴────────┐
   ProductDBProxy ──────► ProductImpl
   (connaît la base)   (ignore la base)


       Base
  1. Une interface qui déclare toutes les méthodes dont les clients ont besoin.
  2. Une implémentation qui réalise ces méthodes sans rien savoir de la base.
  3. Le proxy, qui connaît la base : il récupère les données, crée l'implémentation et lui délègue le message.

Le client envoie prix() à ce qu'il croit être un Product mais qui est en réalité un ProductDBProxy. Le proxy va chercher l'implémentation en base, puis lui délègue l'appel. Ni le client ni l'implémentation ne savent que cela s'est produit. C'est là toute la beauté du pattern : en théorie, on peut l'insérer entre deux objets collaborants sans qu'aucun ne le sache, pour franchir une frontière comme une base ou un réseau.

Note

La forme canonique du Proxy (interface + implémentation + proxy délégant) est élégante sur le papier. En pratique, comme nous allons le voir, elle se déforme presque toujours sous la pression du vrai code. C'est l'esprit même de ce livre : les patterns émergent et se plient à la réalité, ils ne s'appliquent pas mécaniquement.

Proxifier le panier d'achat

Le Product est le plus simple à proxifier : la table des produits n'est qu'un dictionnaire chargé en un seul endroit. On commence — méthode pilotée par les tests (Test-Driven Development, TDD) oblige — par un test, puis on sépare l'interface de son implémentation.

// L'interface : tout ce que le client invoque.
interface Product {
  prix(): number;
  nom(): string;
  sku(): string;
}

// L'implémentation pure, sans la moindre trace de base.
class ProductImpl implements Product {
  constructor(
    private readonly _sku: string,
    private readonly _nom: string,
    private readonly _prix: number,
  ) {}

  prix(): number { return this._prix; }
  nom(): string { return this._nom; }
  sku(): string { return this._sku; }
}

Le proxy, lui, implémente la même interface mais va chercher chaque donnée en base via un utilitaire DB.

// Le proxy : même interface, mais il interroge la base.
class ProductProxy implements Product {
  constructor(private readonly _sku: string) {}

  prix(): number {
    return DB.donneesProduit(this._sku).prix;
  }

  nom(): string {
    return DB.donneesProduit(this._sku).nom;
  }

  sku(): string {
    return this._sku; // déjà connu : inutile de toucher la base.
  }
}

Première surprise : l'implémentation ne respecte pas la forme canonique. La version « pure » du pattern aurait fait créer un ProductImpl dans chaque méthode du proxy, puis lui aurait délégué l'appel.

// La forme canonique... qui n'a ici aucun sens.
prix(): number {
  const pd = DB.donneesProduit(this._sku);
  const p = new ProductImpl(pd.sku, pd.nom, pd.prix);
  return p.prix(); // gaspillage : le proxy a déjà la donnée.
}

Créer le ProductImpl serait un pur gaspillage : le proxy possède déjà la donnée que les accesseurs renverraient. Le sku() pousse même la logique plus loin : il ne touche pas la base du tout, puisqu'il connaît déjà le sku. Le code vous éloigne des patterns que vous attendiez.

Astuce

On pourrait croire ce proxy inefficace : il interroge la base à chaque accesseur. Mais rien ne nous pousse à mettre en cache, sinon la peur. Tant que rien ne prouve un problème de performance — et le moteur de base fait déjà du cache —, n'inventez pas vos propres ennuis. Ajouter un cache trop tôt est un excellent moyen de dégrader les performances. Mesurez d'abord, optimisez ensuite.

Proxifier les relations

Le cas de Order est plus retors : une commande contient plusieurs Item. Dans le modèle objet, c'est une collection ; dans le schéma relationnel, chaque ligne d'items porte la clé de sa commande. Le proxy doit traduire entre les deux formes, ce qui le force à abandonner toute idée de délégation simple.

class OrderProxy implements Order {
  constructor(private readonly idCommande: number) {}

  // ajouterArticle ne peut PAS déléguer : il doit insérer une ligne.
  ajouterArticle(produit: Product, quantite: number): void {
    DB.stocker(new ItemData(this.idCommande, quantite, produit.sku()));
  }

  // total() reconstruit une vraie commande pour DÉLÉGUER le calcul,
  // afin que la règle métier reste dans OrderImpl.
  total(): number {
    const imp = new OrderImpl(this.clientId());
    for (const ligne of DB.articlesDeCommande(this.idCommande)) {
      imp.ajouterArticle(new ProductProxy(ligne.sku), ligne.quantite);
    }
    return imp.total();
  }
}

On voit ici deux entorses au schéma canonique. ajouterArticle ne peut pas déléguer : il doit écrire une ligne en base. total, au contraire, veut déléguer pour garder la règle de calcul encapsulée dans OrderImpl — mais pour cela il doit d'abord reconstruire la commande complète depuis la base. La délégation, repoussée hors de ajouterArticle, ressurgit dans total.

Bilan du Proxy : puissant mais coûteux

L'exemple dissipe toute illusion sur l'élégance des proxies. Le modèle de délégation pure se matérialise rarement : on court-circuite la délégation pour les getters triviaux, on la diffère pour les relations 1:N, et l'on affronte le spectre du cache. Les proxies ne sont pas triviaux.

Mais ils offrent un bénéfice puissant : la séparation des préoccupations (separation of concerns). Dans notre exemple, règles métier et base sont totalement disjointes. OrderImpl n'a aucune dépendance vers la base : on peut changer le schéma ou le moteur sans toucher aux classes du domaine. Le Proxy peut séparer le métier de n'importe quel détail d'implémentation — COM, CORBA, EJB, peu importe la technologie à la mode.

Considérons la relation initiale entre une application et une API tierce : l'application appelle directement l'API, et son code se pollue peu à peu d'appels SQL. Quand l'API ou le schéma change, il faut retoucher partout. On introduit alors une couche d'isolation (ODBC, JDBC en sont des exemples), mais une dépendance transitive subsiste. Pour une isolation parfaite, il faut inverser la dépendance entre l'application et la couche : c'est exactement ce que réalise le Proxy.

   Application                Application
       │            ⟹            ▲
       ▼                          │
     LAYER                      LAYER
       │                          │
       ▼                          ▼
      API                        API

Piège courant

Cette concentration du savoir fait des proxies des cauchemars. Le proxy change quand l'API change, et change quand l'application change. Mais au moins, « il est bon de savoir où vivent ses cauchemars » : sans le proxy, ils seraient dispersés dans tout le code. La plupart des applications n'ont pas besoin de proxies — c'est une solution très lourde. Réservez-la aux très gros systèmes soumis à un changement fréquent de schéma ou d'API, ou aux systèmes devant chevaucher plusieurs moteurs de base.

STAIRWAY TO HEAVEN

L'« escalier vers le ciel » (Stairway to Heaven) atteint la même inversion de dépendance que le Proxy, mais par une variante de l'Adaptateur (Adapter) de classe. Une classe abstraite PersistentObject connaît la base et fournit deux méthodes abstraites, read et write, ainsi qu'un outillage pour les implémenter. PersistentProduct s'en sert pour lire/écrire les champs de Product ; PersistentAssembly hérite de cette capacité et y ajoute les champs propres à Assembly.

        PersistentObject
          + write / + read

        ┌───────┴───────────┐
   Product            PersistentProduct
      ▲                       ▲
      └───────┬───────────────┘
        Assembly        PersistentAssembly

Ce pattern ne fonctionne qu'avec l'héritage multiple d'implémentation : PersistentAssembly hérite de deux classes concrètes et se retrouve dans un losange d'héritage (le fameux « diamant de la mort ») avec Product à son sommet. En C++, on emploie l'héritage virtuel pour éviter la double inclusion de Product. En TypeScript, qui ne connaît pas l'héritage multiple d'implémentation, on transpose l'esprit du pattern : la classe de base pilote l'écriture via un patron de méthode (Template Method), et chaque dérivé fournit l'en-tête, les champs et le pied.

// La base connaît la base de données ; write() est le Template Method.
abstract class PersistentObject {
  write(flux: string[]): void {
    this.writeHeader(flux);
    this.writeFields(flux);
    this.writeFooter(flux);
  }

  protected abstract writeHeader(flux: string[]): void;
  protected abstract writeFields(flux: string[]): void;
  protected abstract writeFooter(flux: string[]): void;
}

class PersistentProduct extends PersistentObject {
  constructor(protected nom: string) { super(); }

  protected writeHeader(flux: string[]): void { flux.push("<PRODUCT>"); }
  protected writeFooter(flux: string[]): void { flux.push("</PRODUCT>"); }
  protected writeFields(flux: string[]): void {
    flux.push(`<NAME>${this.nom}</NAME>`);
  }
}

Le bénéfice du pattern : il sépare entièrement la connaissance de la base des règles métier (Product et Assembly n'ont aucune trace de persistance), et son intrusion dans la hiérarchie métier reste minimale. La partie de l'application qui n'a pas besoin de lire/écrire reste indépendante du versant persistant : on demande simplement à l'objet s'il se conforme à PersistentObject et, le cas échéant, on appelle write.

Autres patterns et la passerelle (Gateway)

D'autres patterns peuvent franchir la frontière de la base : un objet d'extension (Extension Object) qui sait écrire l'objet étendu ; un Visiteur (Visitor) DatabaseWriterVisitor qu'on fait accepter par l'objet ; un Décorateur (Decorator) qui ajoute la persistance à un objet métier (ou les règles métier à un objet de données, courant avec les bases orientées objet).

Mais le point de départ favori de Martin est la Façade (Facade), une forme de passerelle (gateway) bien plus simple que le Proxy. Une classe DatabaseFacade offre simplement des méthodes readProduct, writeProduct, readAssembly… Elle couple les objets métier à la base et vice-versa, mais ce couplage est acceptable dans les petites applications ou celles qui commencent à grandir. Surtout, la Façade se refactorise facilement : si le couplage devient gênant, on bascule vers un Proxy plus tard.

À retenir

Compromis Proxy vs Gateway. Il est très tentant d'anticiper le besoin d'un Proxy ou d'un Stairway to Heaven bien avant qu'il n'existe réellement. C'est presque toujours une mauvaise idée, surtout pour le Proxy. Commencez par une Façade (Gateway), puis refactorisez vers le Proxy si nécessaire. Vous gagnerez du temps et vous épargnerez bien des ennuis. Le Proxy est puissant — séparation totale du métier et de l'API — mais coûteux ; la Gateway est simple et efficace, au prix d'un couplage modéré.

Étude de cas : la station météo Nimbus-LC

La Cloud Company, leader des systèmes de surveillance météo (Weather Monitoring System, WMS) haut de gamme, voit un concurrent, Microburst, attaquer le bas de gamme avec un produit évolutif et interconnectable. La parade : annoncer immédiatement le Nimbus-LC, un produit bas coût livrable en six mois. Le logiciel doit tourner à l'identique sur deux plateformes matérielles très différentes — le matériel 1.0 (existant, repackagé) et le futur 2.0, dont le processeur sera différent et que les développeurs ne verront que tardivement. La portabilité devient la contrainte reine, et l'objectif d'architecture de la phase I est de rendre le gros du logiciel indépendant du matériel.

Capteurs polymorphes et classes de test

Pour afficher la température quelle que soit la configuration matérielle, on définit un capteur abstrait dont chaque plateforme fournit une implémentation concrète de read().

              TemperatureSensor
              + read(): number

        ┌────────────┼─────────────┐
  Nimbus1.0     Nimbus2.0      TestTemperature
   Sensor        Sensor          Sensor

Un dérivé est crucial : TestTemperatureSensor. Il permet de tester le logiciel sur un poste de travail sans matériel Nimbus, d'écrire des tests unitaires et d'acceptation, et même de simuler des pannes difficiles à reproduire sur le vrai matériel. En faisant fonctionner le logiciel à la fois avec le 1.0 et avec la classe de test, on l'exécute déjà sur plusieurs plateformes, ce qui réduit drastiquement le risque de portabilité du 2.0.

Vers Observer : découpler l'IHM

La première ébauche plaçait un Scheduler au centre du système : il appelait périodiquement chaque capteur et poussait les valeurs vers l'écran. Mauvais départ. Ce planificateur est connecté à tous les capteurs et à toutes les interfaces ; chaque nouveau capteur ou écran le force à changer. C'est une violation nette du principe ouvert/fermé (Open-Closed Principle, OCP).

Les interfaces utilisateur étant les plus volatiles, on les découple en premier avec le pattern Observer (Observer). L'IHM devient observatrice des capteurs : quand une lecture change, elle est notifiée automatiquement. La dépendance est indirecte — l'observateur réel est un Adaptateur TemperatureObserver qui, notifié par le capteur, appelle displayTemp sur l'écran.

interface Observer {
  update(valeur: number): void;
}

abstract class Observable {
  private observateurs: Observer[] = [];

  ajouterObservateur(o: Observer): void {
    this.observateurs.push(o);
  }

  protected notifierObservateurs(valeur: number): void {
    for (const o of this.observateurs) o.update(valeur);
  }
}

// L'écran ignore les capteurs : il ne connaît que des Observables.
class TemperatureObserver implements Observer {
  constructor(private ecran: MonitoringScreen) {}
  update(valeur: number): void {
    this.ecran.displayTemp(valeur);
  }
}

Le Scheduler ne sait désormais plus rien de l'IHM. Cette décision résout d'un même geste le problème de la tendance barométrique (rising/falling/stable) : un BarometricPressureTrendSensor distinct observe le BarometricPressureSensor et calcule la tendance, au lieu d'engorger un capteur ou le planificateur avec un historique de trois heures.

Le planificateur devient une horloge (Listener)

Le Scheduler viole encore l'OCP : ajouter, retirer ou simplement reconfigurer la cadence d'un capteur le force à changer. Or la connaissance de la fréquence d'échantillonnage appartient au capteur lui-même. On le découple via le paradigme Listener de Java, proche d'Observer mais déclenché par un événement temporel. Les capteurs créent des adaptateurs anonymes AlarmListener et s'enregistrent auprès de l'AlarmClock (le nouveau nom du planificateur) en indiquant leur cadence.

interface AlarmListener {
  wakeup(): void;
}

class AlarmClock {
  wakeEvery(intervalleMs: number, listener: AlarmListener): void {
    // Enregistre le listener et le réveille à la cadence demandée.
  }
}

// Le capteur s'enregistre lui-même avec sa propre cadence.
class TemperatureSensor extends Observable {
  constructor(horloge: AlarmClock, private imp: TemperatureSensorImp) {
    super();
    horloge.wakeEvery(60_000, { wakeup: () => this.check() });
  }

  // Template Method : check() (générique) appelle read() (spécifique).
  private check(): void {
    const valeur = this.imp.read();
    if (valeur !== this.derniereLecture) {
      this.derniereLecture = valeur;
      this.notifierObservateurs(valeur);
    }
  }

  private derniereLecture = NaN;
}

La transformation est spectaculaire : d'élément central, le planificateur passe sur le côté du système. Il ne connaît plus rien des autres composants, respecte le SRP en ne faisant qu'une chose — planifier, ce qui n'a rien à voir avec la météo — et devient réutilisable dans tout autre contexte. D'où son nouveau nom, AlarmClock.

Astuce

Le capteur emploie un Template Method : la méthode générique check() de la classe de base appelle la méthode abstraite read(), implémentée par le dérivé pour dialoguer avec le matériel. Pour chaque nouvelle plateforme, il suffit de redéfinir une seule fonction simple, read() ; tout le reste vit dans la classe de base, là où il doit être.

Bridge : extraire la vraie API

Un objectif de la release II est une API Java simple, extensible, donnant un accès direct au matériel — sans observers, sans auto-polling, sans AlarmClock. Rien de ce qu'on a construit ne s'y prête. On applique alors le pattern Bridge (Bridge) pour séparer l'abstraction (TemperatureSensor) de son implémentation (TemperatureSensorImp), elle-même réalisée par Nimbus1_0TemperatureSensor.

   TemperatureSensor ────────► TemperatureSensorImp  ◄─ "l'API"
   (abstraction métier)        (interface d'implémentation)

                              Nimbus1_0TemperatureSensor


                                 Nimbus 1.0 C-API

Fabrique : créer sans connaître la plateforme

Lier un TemperatureSensor à son implémentation concrète n'est pas portable, puisque cela suppose de connaître la classe dépendante de la plateforme. Plutôt que d'éparpiller cette connaissance dans le main, on emploie une Fabrique abstraite (Abstract Factory) baptisée StationToolkit : chaque plateforme en fournit un dérivé qui crée les bonnes implémentations.

// La fabrique abstraite : une méthode de création par implémentation.
interface StationToolkit {
  makeTemperature(): TemperatureSensorImp;
  makeBarometricPressure(): BarometricPressureSensorImp;
  getAlarmClock(): AlarmClockImp;
  getPersistentImp(): PersistentImp;
}

// Le main devient quasi indépendant de la plateforme.
function main(nomToolkit: string): void {
  const st: StationToolkit = chargerToolkit(nomToolkit); // par nom !
  const ac = new AlarmClock(st);
  const ts = new TemperatureSensor(ac, st);
  const bps = new BarometricPressureSensor(ac, st);
  new BarometricPressureTrend(bps);
}

Chaque capteur reçoit le StationToolkit et crée sa propre implémentation. Au fil des raffinements (la fabrique crée aussi l'AlarmClock via un Bridge), le main ne contient plus qu'une seule ligne dépendante de la plateforme — voire zéro, car Java permet d'instancier le toolkit par son nom, passé en argument de ligne de commande. Changer de plateforme ne demande plus de recompiler quoi que ce soit.

Principes de packaging : briser les cycles

Le découpage en paquets (packages) tombe presque naturellement : un paquet par plateforme, dérivant du paquet API, dont l'unique client est WeatherMonitoringSystem. Mais l'IHM reste mêlée au système. La sortir crée un cycle : WeatherStation crée l'écran, et l'écran doit connaître les capteurs pour s'y abonner. C'est une violation du principe des dépendances acycliques (Acyclic-Dependencies Principle, ADP), qui interdirait toute publication indépendante des deux paquets.

On brise le cycle en extrayant le main de WeatherStation : c'est désormais le main qui crée l'écran et la station, puis passe la station à l'écran. La WeatherStation expose des méthodes addTempObserver, addBPObserver… que l'écran utilise pour s'abonner.

class WeatherStation implements WeatherStationComponent {
  addTempObserver(o: Observer): void { this.ts.ajouterObservateur(o); }
  addBPObserver(o: Observer): void { this.bps.ajouterObservateur(o); }
  addBPTrendObserver(o: Observer): void { this.bpt.ajouterObservateur(o); }
}

Reste que UI et WeatherMonitoringSystem sont deux paquets concrets, et qu'un paquet concret dépendant d'un autre viole le DIP. La solution applique le principe d'inversion des dépendances stables (Stable-Dependencies Principle, SDP) et le principe des abstractions stables (Stable-Abstractions Principle, SAP) : on crée une interface WeatherStationComponent, placée dans son propre paquet abstrait, dont WeatherStation dérive et dont l'écran dépend. UI et WeatherMonitoringSystem sont alors totalement découplés et peuvent évoluer indépendamment.

Note

Cette étude de cas illustre concrètement les principes de packaging propres à ce livre : la cohésion (REP, CCP, CRP) guide le regroupement initial des classes en paquets, tandis que le couplage (ADP, SDP, SAP) impose de briser les cycles et de faire pointer les dépendances vers l'abstraction stable.

Persistance : du SRP violé au Proxy salvateur

La spécification exige un historique persistant de 24 heures, avec les hauts et bas du jour calendaire précédent — détail vérifié auprès des parties prenantes, car « implémenter une spec qui ne reflète pas le besoin client ne sert à rien ». L'API définit un mécanisme bas niveau PersistentImp fondé sur la sérialisation : stocker et récupérer des objets par nom.

interface PersistentImp {
  store(nom: string, obj: Serializable): void;
  retrieve(nom: string): unknown;
  directory(regExp: string): string[];
}

Le calcul des hauts/bas est un Observer : un TemperatureHiLo, réveillé chaque minuit par l'AlarmClock et observant le capteur, met à jour une HiLoData. On sépare TemperatureHiLo de HiLoData pour deux raisons : isoler la connaissance du capteur et de l'horloge de l'algorithme, et réutiliser cet algorithme pour la pression, le vent, le point de rosée…

La première implémentation de HiLoDataImp mélange tout : les méthodes currentReading et newDay relèvent de la politique (gestion des extrêmes), tandis que store, calculateStorageKey, le constructeur et les variables transientes relèvent de la persistance. C'est une violation du SRP, et le germe d'un cauchemar de maintenance. On découple les deux avec le Proxy : HiLoDataProxy absorbe toute la laideur de persistance et délègue la politique à HiLoDataImp, qui n'a alors plus la moindre notion de stockage.

// Le proxy concentre la persistance et délègue la politique.
class HiLoDataProxy implements HiLoData {
  constructor(private imp: HiLoDataImp) {}

  currentReading(valeur: number, temps: number): boolean {
    const change = this.imp.currentReading(valeur, temps);
    if (change) this.store(); // on n'écrit la NVRAM que si ça a changé.
    return change;
  }

  newDay(initial: number, temps: number): void {
    this.store();
    this.imp.newDay(initial, temps);
    this.calculateStorageKey(new Date(temps)); // nouvelle clé pour le nouveau jour
    this.store();
  }

  private store(): void { /* sérialise via PersistentImp sous itsStorageKey */ }
  private calculateStorageKey(date: Date): void { /* dérive itsStorageKey du jour */ }
}

// L'implémentation : politique pure, zéro persistance.
class HiLoDataImp implements HiLoData {
  currentReading(valeur: number, temps: number): boolean {
    if (valeur > this.haut) { this.haut = valeur; return true; }
    if (valeur < this.bas) { this.bas = valeur; return true; }
    return false;
  }
  newDay(initial: number, temps: number): void {
    this.haut = this.bas = initial;
  }
}

Une subtilité « du monde réel » : currentReading retourne désormais un booléen, pour que le proxy sache quand appeler store. Pourquoi ne pas stocker à chaque lecture ? Parce que certaines NVRAM ont un nombre limité d'écritures ; on n'écrit que lorsque les valeurs changent, pour prolonger leur durée de vie. Une Fabrique (DataToolkit) crée ensuite le proxy sans que TemperatureHiLo n'en sache rien — il ne connaît que l'interface HiLoData.

Attention

Toute symétrie a ses limites. HiLoDataProxy doit créer un HiLoDataImp quand la persistance ne trouve rien à récupérer (cas rare où retrieve ne rend aucun objet du stockage), ce qui introduit une dépendance du paquet de persistance vers le paquet wmsDataImp — autrement dit vers le paquet d'implémentation de la politique (HiLoDataImp), et non vers la politique abstraite (HiLoData). On pourrait insérer une fabrique de plus pour la supprimer — mais les règles métier météo logées dans wmsDataImp sont stables et ne changent guère. On décide donc de vivre avec cette dépendance : il faut savoir tracer la ligne quelque part et ne pas multiplier les abstractions sans bénéfice réel.

À retenir

  • Le Proxy interpose un objet de même interface pour franchir une frontière (base, réseau) à l'insu du client et de l'implémentation, réalisant une séparation totale du métier et de l'infrastructure — au prix d'une forte complexité (délégation court-circuitée, relations 1:N, cache).
  • La forme canonique du Proxy se déforme presque toujours sous la pression du vrai code : c'est la marque d'une conception pilotée par le code et les tests, pas par un catalogue.
  • Compromis clé : commencez par une Gateway/Façade, simple et facile à refactoriser ; ne passez au Proxy (ou au Stairway to Heaven, réservé aux langages à héritage multiple) que lorsque la volatilité du schéma ou de l'API le justifie vraiment.
  • Dans la station météo, les patterns émergent d'un besoin : Observer découple l'IHM volatile, Listener libère le planificateur, Bridge extrait l'API, Abstract Factory rend le main indépendant de la plateforme.
  • Les principes guident chaque décision : OCP pour fermer le planificateur aux nouveaux capteurs, SRP pour séparer politique et persistance, DIP pour faire dépendre l'IHM d'une abstraction.
  • Les principes de packaging sont à l'œuvre : briser un cycle (ADP) en extrayant le main, puis isoler une interface dans son propre paquet abstrait (SDP/SAP) pour découpler totalement IHM et système.
  • Sachez tracer la ligne : la peur de la performance ne justifie pas un cache prématuré, et toute dépendance résiduelle ne mérite pas une fabrique supplémentaire si le risque de maintenance est faible.