Cours JavaScript et React · Module 8: Patterns React avancés

Patterns Adapter et Facade

8 min de lecture
Dans cette leçon4

Au cours de notre voyage cosmique à travers React, notre vaisseau doit se connecter à de nombreux systèmes différents - stations spatiales, satellites, bases planétaires. Chacun de ces systèmes possède son propre protocole de communication. Les patterns Adapter et Facade sont comme un traducteur universel qui permet à notre vaisseau de communiquer avec n'importe quel système sans avoir à connaître ses détails internes.

Pattern Adapter

Qu'est-ce qu'un Adapter ?

Le pattern Adapter (aussi appelé Wrapper) permet à des objets ayant des interfaces incompatibles de fonctionner ensemble. En React, nous l'utilisons le plus souvent pour envelopper des API externes avec une interface cohérente et prévisible.

Imagine que ta base de données météo cosmique pour différentes planètes utilise trois fournisseurs d'API différents. Chacun d'eux renvoie des données dans un format différent. Un Adapter te permet d'unifier ces données :

1// Réponses brutes de différentes API spatiales
2// L'API Mars renvoie : { temp_celsius: -60, wind_kph: 25, condition: "dusty" }
3// L'API Jupiter renvoie : { temperature: { value: -145, unit: "C" }, windSpeed: 350 }
4// L'API Saturn renvoie : { readings: { t: -178, w: 500 }, status: "stormy" }

Sans adapter, nous devrions vérifier dans chaque composant de quelle API proviennent les données et les traiter en conséquence. Cela mène au chaos !

Implémenter un Adapter en React

Créons des adapters qui unifient les données provenant de différentes API :

1// Interface de données météo unifiée
2// { planet, temperature, windSpeed, condition }
3
4function adaptMarsWeather(rawData) {
5  return {
6    planet: 'Mars',
7    temperature: rawData.temp_celsius,
8    windSpeed: rawData.wind_kph,
9    condition: rawData.condition,
10  };
11}
12
13function adaptJupiterWeather(rawData) {
14  return {
15    planet: 'Jupiter',
16    temperature: rawData.temperature.value,
17    windSpeed: rawData.windSpeed,
18    condition: rawData.windSpeed > 300 ? 'extreme' : 'moderate',
19  };
20}
21
22function adaptSaturnWeather(rawData) {
23  return {
24    planet: 'Saturn',
25    temperature: rawData.readings.t,
26    windSpeed: rawData.readings.w,
27    condition: rawData.status,
28  };
29}

Nous avons maintenant trois fonctions simples, chacune prenant des données brutes et renvoyant un objet à la structure identique. Notre composant météo peut utiliser ces données sans savoir d'où elles viennent :

1function PlanetWeatherCard({ weatherData }) {
2  // weatherData a TOUJOURS la même forme grâce aux adapters
3  return (
4    <div className="weather-card">
5      <h3>{weatherData.planet}</h3>
6      <p>Température : {weatherData.temperature} C</p>
7      <p>Vent : {weatherData.windSpeed} km/h</p>
8      <p>Condition : {weatherData.condition}</p>
9    </div>
10  );
11}

Adapter sous forme de Custom Hook

Les adapters fonctionnent à merveille en tant que custom hooks qui enveloppent des bibliothèques externes :

1// Adapter pour différentes bibliothèques de cartes spatiales
2function useSpaceMap(provider) {
3  const [map, setMap] = useState(null);
4
5  useEffect(() => {
6    let mapInstance;
7
8    if (provider === 'galactic-maps') {
9      // La bibliothèque GalacticMaps a sa propre API
10      mapInstance = new GalacticMaps.Chart('#map');
11      mapInstance.init({ zoom: 5 });
12    } else if (provider === 'star-atlas') {
13      // StarAtlas a une API complètement différente
14      mapInstance = StarAtlas.create(document.getElementById('map'), {
15        level: 5,
16        mode: '3d',
17      });
18    }
19
20    // Nous renvoyons une interface unifiée
21    setMap({
22      zoomIn: () => {
23        if (provider === 'galactic-maps') mapInstance.setZoom(mapInstance.getZoom() + 1);
24        else mapInstance.changeLevel(mapInstance.level + 1);
25      },
26      zoomOut: () => {
27        if (provider === 'galactic-maps') mapInstance.setZoom(mapInstance.getZoom() - 1);
28        else mapInstance.changeLevel(mapInstance.level - 1);
29      },
30      centerOn: (coords) => {
31        if (provider === 'galactic-maps') mapInstance.panTo(coords);
32        else mapInstance.focus({ x: coords.lat, y: coords.lng });
33      },
34    });
35
36    return () => {
37      if (provider === 'galactic-maps') mapInstance.destroy();
38      else mapInstance.dispose();
39    };
40  }, [provider]);
41
42  return map;
43}

Grâce à ce hook, le composant utilisant la carte n'a pas besoin de savoir quelle bibliothèque est utilisée en dessous. Si nous voulions changer de fournisseur de cartes, il suffirait de modifier le hook - aucun composant ne changerait.

Pattern Facade

Qu'est-ce qu'une Facade ?

Le pattern Facade fournit une interface simplifiée à un sous-système complexe. En React, nous l'implémentons le plus souvent sous forme de hooks qui cachent une logique compliquée derrière une API simple.

Pense au panneau de contrôle d'un vaisseau spatial. Le pilote n'a pas besoin de connaître les détails du fonctionnement de chaque système - il appuie sur le bouton « Démarrer les moteurs » et tout le processus complexe démarre automatiquement. Une Facade fonctionne exactement de la même manière.

Exemple simple : un Hook comme Facade

Sans facade, un composant doit gérer lui-même de nombreux détails :

1// SANS Facade - le composant est submergé de détails
2function MissionLauncher() {
3  const [fuel, setFuel] = useState(0);
4  const [systems, setSystems] = useState([]);
5  const [crewReady, setCrewReady] = useState(false);
6  const [countdown, setCountdown] = useState(null);
7  const [launchStatus, setLaunchStatus] = useState('idle');
8  const [errors, setErrors] = useState([]);
9
10  // Vérification du carburant
11  useEffect(() => {
12    fetchFuelLevel().then(setFuel);
13  }, []);
14
15  // Vérification des systèmes
16  useEffect(() => {
17    checkAllSystems().then(setSystems);
18  }, []);
19
20  // Compte à rebours
21  useEffect(() => {
22    if (countdown !== null && countdown > 0) {
23      const timer = setTimeout(() => setCountdown(c => c - 1), 1000);
24      return () => clearTimeout(timer);
25    }
26    if (countdown === 0) {
27      executeLaunch().then(() => setLaunchStatus('launched'));
28    }
29  }, [countdown]);
30
31  const startLaunch = async () => {
32    if (fuel < 80) { setErrors(e => [...e, 'Not enough fuel']); return; }
33    if (!crewReady) { setErrors(e => [...e, 'Crew not ready']); return; }
34    const failedSystems = systems.filter(s => s.status !== 'ok');
35    if (failedSystems.length > 0) {
36      setErrors(e => [...e, 'Systems not operational']);
37      return;
38    }
39    setCountdown(10);
40  };
41
42  // ... reste du composant avec une énorme quantité de logique
43}

Avec une facade, nous déplaçons toute cette logique dans un hook :

1// AVEC Facade - le hook cache toute la complexité
2function useMissionLaunch() {
3  const [fuel, setFuel] = useState(0);
4  const [systems, setSystems] = useState([]);
5  const [crewReady, setCrewReady] = useState(false);
6  const [countdown, setCountdown] = useState(null);
7  const [status, setStatus] = useState('idle');
8  const [errors, setErrors] = useState([]);
9
10  useEffect(() => {
11    fetchFuelLevel().then(setFuel);
12    checkAllSystems().then(setSystems);
13  }, []);
14
15  useEffect(() => {
16    if (countdown !== null && countdown > 0) {
17      const timer = setTimeout(() => setCountdown(c => c - 1), 1000);
18      return () => clearTimeout(timer);
19    }
20    if (countdown === 0) {
21      executeLaunch().then(() => setStatus('launched'));
22    }
23  }, [countdown]);
24
25  const launch = () => {
26    const newErrors = [];
27    if (fuel < 80) newErrors.push('Not enough fuel');
28    if (!crewReady) newErrors.push('Crew not ready');
29    const failedSystems = systems.filter(s => s.status !== 'ok');
30    if (failedSystems.length > 0) newErrors.push('Systems not operational');
31
32    if (newErrors.length > 0) {
33      setErrors(newErrors);
34      return;
35    }
36    setCountdown(10);
37    setStatus('countdown');
38  };
39
40  const abort = () => {
41    setCountdown(null);
42    setStatus('aborted');
43  };
44
45  return {
46    // API simple et lisible
47    launch,
48    abort,
49    status,
50    countdown,
51    errors,
52    isReady: fuel >= 80 && crewReady && systems.every(s => s.status === 'ok'),
53    fuelLevel: fuel,
54    setCrewReady,
55  };
56}
57
58// Le composant est maintenant propre et simple
59function MissionLauncher() {
60  const mission = useMissionLaunch();
61
62  return (
63    <div>
64      <h1>Lancement de la mission</h1>
65      <p>Carburant : {mission.fuelLevel}%</p>
66      <p>Statut : {mission.status}</p>
67      {mission.countdown !== null && <p>Compte à rebours : {mission.countdown}</p>}
68      {mission.errors.map((err, i) => <p key={i} style={{color: 'red'}}>{err}</p>)}
69      <button onClick={mission.launch} disabled={!mission.isReady}>
70        Lancer
71      </button>
72      <button onClick={mission.abort}>Avorter</button>
73    </div>
74  );
75}

Facade pour des opérations complexes

La Facade est particulièrement utile lorsque tu dois coordonner plusieurs opérations :

1function useCrewManagement(missionId) {
2  // Gère en interne plusieurs sources de données
3  const [crew, setCrew] = useState([]);
4  const [assignments, setAssignments] = useState({});
5  const [healthRecords, setHealthRecords] = useState({});
6
7  useEffect(() => {
8    Promise.all([
9      fetchCrew(missionId),
10      fetchAssignments(missionId),
11      fetchHealthRecords(missionId),
12    ]).then(([crewData, assignData, healthData]) => {
13      setCrew(crewData);
14      setAssignments(assignData);
15      setHealthRecords(healthData);
16    });
17  }, [missionId]);
18
19  // Interface simple pour le composant
20  return {
21    crewMembers: crew.map(member => ({
22      ...member,
23      role: assignments[member.id]?.role || 'Unassigned',
24      healthStatus: healthRecords[member.id]?.status || 'Unknown',
25    })),
26    assignRole: (memberId, role) => {
27      updateAssignment(missionId, memberId, role)
28        .then(() => setAssignments(prev => ({
29          ...prev,
30          [memberId]: { ...prev[memberId], role },
31        })));
32    },
33    isCrewReady: crew.length > 0 && crew.every(m =>
34      assignments[m.id]?.role && healthRecords[m.id]?.status === 'fit'
35    ),
36  };
37}

Quand utiliser Adapter vs Facade ?

CaractéristiqueAdapterFacade
ButUnifier des interfaces incompatiblesSimplifier un système complexe
QuandPlusieurs sources de données aux formats différentsLogique complexe à cacher
En ReactEnvelopper des API/bibliothèques externesCustom hooks cachant la complexité
Analogie spatialeTraducteur de langue d'une civilisation extraterrestrePanneau de contrôle du vaisseau

Les deux patterns peuvent être combinés - une Facade peut utiliser des adapters en interne :

1function useGalacticWeather(planets) {
2  // Facade : simplifie l'interface
3  const [weather, setWeather] = useState({});
4
5  useEffect(() => {
6    const adapters = {
7      mars: adaptMarsWeather,
8      jupiter: adaptJupiterWeather,
9      saturn: adaptSaturnWeather,
10    };
11
12    // Adapter : unifie les données de différentes API
13    Promise.all(
14      planets.map(planet =>
15        fetchWeather(planet).then(raw => adapters[planet](raw))
16      )
17    ).then(results => {
18      const weatherMap = {};
19      results.forEach(w => { weatherMap[w.planet] = w; });
20      setWeather(weatherMap);
21    });
22  }, [planets]);
23
24  return {
25    getWeather: (planet) => weather[planet] || null,
26    isLoaded: Object.keys(weather).length === planets.length,
27    coldestPlanet: Object.values(weather)
28      .sort((a, b) => a.temperature - b.temperature)[0]?.planet || null,
29  };
30}

Résumé

Les patterns Adapter et Facade sont des outils essentiels dans l'arsenal d'un programmeur React cosmique :

  • Adapter te permet d'unifier des interfaces incompatibles, ce qui est crucial lors de l'intégration avec de multiples API et bibliothèques externes
  • Facade simplifie des sous-systèmes complexes, en créant des hooks lisibles et faciles à utiliser
  • Les deux patterns augmentent la lisibilité du code, simplifient les tests et réduisent le couplage entre composants

Tout comme un adaptateur d'alimentation universel permet à un vaisseau spatial de se connecter à n'importe quelle station d'amarrage de la galaxie, les patterns Adapter et Facade permettent à nos composants React de fonctionner avec n'importe quel système sans complications inutiles.

Code de cette leçon : App.jsx
1import React, { useState, useEffect } from 'react';
2
3// ============================================
4// PATTERNS ADAPTER ET FACADE
5// ============================================
6
7// --- ADAPTER : Unification des donnees de differentes API de planetes ---
8
9// Simulation de reponses brutes de differentes API
10const rawMarsData = { temp_celsius: -60, wind_kph: 25, condition: 'dusty' };
11const rawJupiterData = { temperature: { value: -145, unit: 'C' }, windSpeed: 350 };
12const rawSaturnData = { readings: { t: -178, w: 500 }, status: 'stormy' };
13
14// Adapters - chacun convertit les donnees brutes en un format commun
15function adaptMarsWeather(raw) {
16  return {
17    planet: 'Mars',
18    temperature: raw.temp_celsius,
19    windSpeed: raw.wind_kph,
20    condition: raw.condition,
21  };
22}
23
24function adaptJupiterWeather(raw) {
25  return {
26    planet: 'Jupiter',
27    temperature: raw.temperature.value,
28    windSpeed: raw.windSpeed,
29    condition: raw.windSpeed > 300 ? 'extreme' : 'moderate',
30  };
31}
32
33function adaptSaturnWeather(raw) {
34  return {
35    planet: 'Saturn',
36    temperature: raw.readings.t,
37    windSpeed: raw.readings.w,
38    condition: raw.status,
39  };
40}
41
42// Composant utilisant les donnees unifiees
43function PlanetWeatherCard({ weatherData }) {
44  const getConditionColor = (condition) => {
45    const colors = {
46      dusty: '#ff9800', extreme: '#f44336',
47      stormy: '#9c27b0', moderate: '#4caf50',
48    };
49    return colors[condition] || '#00d4ff';
50  };
51
52  return (
53    <div style={cardStyle}>
54      <h3 style={{ color: '#ffd700', marginBottom: '12px' }}>
55        {weatherData.planet}
56      </h3>
57      <p>Température : <strong>{weatherData.temperature} C</strong></p>
58      <p>Vent : <strong>{weatherData.windSpeed} km/h</strong></p>
59      <p>
60        État :{' '}
61        <span style={{ color: getConditionColor(weatherData.condition), fontWeight: 'bold' }}>
62          {weatherData.condition}
63        </span>
64      </p>
65    </div>
66  );
67}
68
69// --- FACADE : Hook cachant la complexite ---
70
71function useGalacticWeather() {
72  const [weather, setWeather] = useState([]);
73  const [loading, setLoading] = useState(true);
74  const [selectedPlanet, setSelectedPlanet] = useState(null);
75
76  useEffect(() => {
77    // La Facade cache toute la logique de recuperation et d'adaptation des donnees
78    const timer = setTimeout(() => {
79      const adapted = [
80        adaptMarsWeather(rawMarsData),
81        adaptJupiterWeather(rawJupiterData),
82        adaptSaturnWeather(rawSaturnData),
83      ];
84      setWeather(adapted);
85      setLoading(false);
86    }, 800);
87    return () => clearTimeout(timer);
88  }, []);
89
90  // Interface simple pour le composant
91  return {
92    weather,
93    loading,
94    selectedPlanet: selectedPlanet
95      ? weather.find(w => w.planet === selectedPlanet)
96      : null,
97    selectPlanet: setSelectedPlanet,
98    coldestPlanet: weather.length > 0
99      ? weather.reduce((a, b) => a.temperature < b.temperature ? a : b).planet
100      : null,
101    averageTemp: weather.length > 0
102      ? Math.round(weather.reduce((sum, w) => sum + w.temperature, 0) / weather.length)
103      : 0,
104  };
105}
106
107// --- Application principale ---
108function App() {
109  const {
110    weather, loading, selectedPlanet,
111    selectPlanet, coldestPlanet, averageTemp,
112  } = useGalacticWeather();
113
114  return (
115    <div style={appStyle}>
116      <h1 style={{ color: '#00d4ff', textAlign: 'center', marginBottom: '8px' }}>
117        Adapter et Facade - Meteo dans la galaxie
118      </h1>
119      <p style={{ textAlign: 'center', color: '#aaa', marginBottom: '24px' }}>
120        Donnees de 3 API differentes, unifiees par les Adapters, simplifiees par la Facade
121      </p>
122
123      {loading ? (
124        <p style={{ textAlign: 'center', color: '#ffd700' }}>Connexion aux sondes...</p>
125      ) : (
126        <>
127          <div style={statsStyle}>
128            <span>La plus froide : <strong style={{ color: '#00d4ff' }}>{coldestPlanet}</strong></span>
129            <span>Temp. moyenne : <strong style={{ color: '#ff9800' }}>{averageTemp} C</strong></span>
130          </div>
131
132          <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', justifyContent: 'center' }}>
133            {weather.map(w => (
134              <div
135                key={w.planet}
136                onClick={() => selectPlanet(w.planet)}
137                style={{ cursor: 'pointer', flex: '1', minWidth: '200px' }}
138              >
139                <PlanetWeatherCard weatherData={w} />
140              </div>
141            ))}
142          </div>
143
144          {selectedPlanet && (
145            <div style={{ ...cardStyle, marginTop: '20px', borderColor: '#ffd700' }}>
146              <h3 style={{ color: '#ffd700' }}>Details : {selectedPlanet.planet}</h3>
147              <p>Température : {selectedPlanet.temperature} C</p>
148              <p>Vent : {selectedPlanet.windSpeed} km/h</p>
149              <p>Conditions : {selectedPlanet.condition}</p>
150              <button
151                onClick={() => selectPlanet(null)}
152                style={btnStyle}
153              >
154                Fermer
155              </button>
156            </div>
157          )}
158
159          {/* TODO : Ajoute une nouvelle planète (par ex. Neptune) avec son propre format de données brutes
160              et écris un adapter pour elle. Format brut de Neptune :
161              { metrics: { tempC: -214, windMs: 580 }, weather: 'icy' }
162              L'adapter devrait convertir windMs en km/h (x 3.6) */}
163        </>
164      )}
165    </div>
166  );
167}
168
169const appStyle = {
170  background: '#0d1b2a', minHeight: '100vh', padding: '20px',
171  fontFamily: 'Arial, sans-serif', color: '#e0e1dd',
172};
173const cardStyle = {
174  background: '#1b2838', borderRadius: '12px', padding: '20px',
175  border: '1px solid #2a3f5f',
176};
177const statsStyle = {
178  display: 'flex', justifyContent: 'center', gap: '30px',
179  marginBottom: '20px', padding: '12px',
180  background: '#1b2838', borderRadius: '8px',
181};
182const btnStyle = {
183  marginTop: '10px', padding: '8px 20px', background: '#ff9800',
184  color: '#000', border: 'none', borderRadius: '6px',
185  cursor: 'pointer', fontWeight: 'bold',
186};
187
188export default App;

Tu as repéré une erreur dans cette leçon ?

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne la syntaxe correcte d'un composant en lazy loading :

Articles utiles