Cours JavaScript et React · Module 8: Patterns React avancés
Patterns Adapter et Facade
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éristique | Adapter | Facade |
|---|---|---|
| But | Unifier des interfaces incompatibles | Simplifier un système complexe |
| Quand | Plusieurs sources de données aux formats différents | Logique complexe à cacher |
| En React | Envelopper des API/bibliothèques externes | Custom hooks cachant la complexité |
| Analogie spatiale | Traducteur de langue d'une civilisation extraterrestre | Panneau 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 :