Design Pattern
Structurels

Adapter

Rendre compatible une interface avec une autre

Problème

On intègre une librairie externe ou une API dont le format de données ne correspond pas à ce qu'attend le reste de l'application. Plutôt que d'adapter tout le code existant, on crée un Adapter qui traduit d'une interface à l'autre.

Exemple : adapter une API externe

L'API externe retourne des données dans un format non maîtrisé. Le modèle interne est différent.

// Format de l'API externe (on ne le contrôle pas)
interface ApiProduct {
  product_id: string;
  product_name: string;
  unit_price: number;
  stock_quantity: number;
  category_id: string;
  image_url: string;
}

// Modèle interne de l'application
interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  categoryId: string;
  imageUrl: string;
}
// L'Adapter traduit du format externe vers le modèle interne
function adaptApiProduct(apiProduct: ApiProduct): Product {
  return {
    id: apiProduct.product_id,
    name: apiProduct.product_name,
    price: apiProduct.unit_price,
    stock: apiProduct.stock_quantity,
    categoryId: apiProduct.category_id,
    imageUrl: apiProduct.image_url,
  };
}

// À l'usage dans le repository
async function getProducts(): Promise<Product[]> {
  const res = await fetch('/api/products');
  const data: ApiProduct[] = await res.json();
  return data.map(adaptApiProduct);
}

Le reste de l'application ne connaît que Product. Si l'API externe change de format, seul l'Adapter change.

Adapter de classe

Pour des objets complexes avec comportement, on utilise une classe :

// Interface attendue par l'application
interface Logger {
  info(message: string): void;
  warn(message: string): void;
  error(message: string, error?: Error): void;
}

// Librairie externe avec une API différente
class ThirdPartyLogger {
  log(level: 'INFO' | 'WARN' | 'ERROR', msg: string, meta?: object): void {
    // implémentation externe
  }
}

// L'Adapter
class LoggerAdapter implements Logger {
  constructor(private logger: ThirdPartyLogger) {}

  info(message: string) {
    this.logger.log('INFO', message);
  }

  warn(message: string) {
    this.logger.log('WARN', message);
  }

  error(message: string, error?: Error) {
    this.logger.log('ERROR', message, error ? { stack: error.stack } : undefined);
  }
}

// L'application utilise Logger, pas ThirdPartyLogger
const logger: Logger = new LoggerAdapter(new ThirdPartyLogger());

En React : adapter des hooks externes

// Un hook de drag-and-drop externe avec une API compliquée
import { useDraggable } from 'some-dnd-library';

// L'Adapter expose une interface simplifiée adaptée aux besoins
function useDraggableItem(id: string) {
  const { setNodeRef, attributes, listeners, isDragging, transform } = useDraggable({ id });

  return {
    ref: setNodeRef,
    dragProps: { ...attributes, ...listeners },
    isDragging,
    style: transform
      ? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` }
      : undefined,
  };
}

// Les composants utilisent useDraggableItem, pas useDraggable directement
// Si on change de librairie, on ne modifie que l'Adapter
function DraggableCard({ id, children }: { id: string; children: React.ReactNode }) {
  const { ref, dragProps, isDragging, style } = useDraggableItem(id);

  return (
    <div ref={ref} {...dragProps} style={style} data-dragging={isDragging}>
      {children}
    </div>
  );
}

Exercice : Adapter dans le Pokémon Builder

Contexte

Dans App.tsx, les appels réseau sont faits directement dans le composant : deux niveaux de fetch imbriqués, 151 requêtes en parallèle, parsing manuel de la réponse. Le composant est couplé à la structure exacte de la PokéAPI.

fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
  .then(res => res.json())
  .then(data => {
    const requests = data.results.map((entry) =>
      fetch(entry.url).then(r => r.json())
    );
    Promise.all(requests).then((details) => { /* ... */ });
  });

Si l'API change d'URL, de format ou si on veut la remplacer par une autre source (fichier JSON, mock, autre API), il faut tout réécrire dans App.tsx.

Objectif

Créer un PokemonApiAdapter qui encapsule tous les appels réseau derrière une interface simple.

Instructions

  1. Crée le fichier src/adapter/PokemonApiAdapter.ts.

  2. Implémente une classe PokemonApiAdapter avec une méthode fetchAll(): Promise<Pokemon[]> qui contient toute la logique de fetch actuelle (en utilisant AppConfig et PokemonFactory).

  3. Dans App.tsx, instancie l'adapter et remplace le bloc fetch par un simple appel à adapter.fetchAll().

  4. L'useEffect dans App.tsx ne doit plus contenir aucun appel fetch direct.

Résultat attendu

App.tsx ne sait pas comment les données sont récupérées. On pourrait remplacer PokemonApiAdapter par un MockPokemonAdapter pour les tests sans toucher au composant.