Kurs JavaScript i React · Moduł 8: Zaawansowane wzorce React
Wzorzec Adapter i Facade
W tej lekcji4
W kosmicznej podróży przez React, nasz statek musi łączyć się z wieloma różnymi systemami - stacjami kosmicznymi, satelitami, bazami na planetach. Każdy z tych systemów ma swój własny protokół komunikacji. Wzorce Adapter i Facade to jak uniwersalny translator, który pozwala naszemu statkowi porozumieć się z dowolnym systemem bez konieczności znajomości jego wewnętrznych szczegółów.
Wzorzec Adapter
Czym jest Adapter?
Wzorzec Adapter (znany też jako Wrapper) pozwala na współpracę obiektów o niekompatybilnych interfejsach. W React używamy go najczęściej do opakowywania zewnętrznych API spójnym, przewidywalnym interfejsem.
Wyobraź sobie, że Twoja kosmiczna baza danych pogodowych na różnych planetach korzysta z trzech różnych dostawców API. Każdy z nich zwraca dane w innym formacie. Adapter pozwala ujednolicić te dane:
1// Surowe odpowiedzi z różnych API kosmicznych
2// API Marsa zwraca: { temp_celsius: -60, wind_kph: 25, condition: "dusty" }
3// API Jowisza zwraca: { temperature: { value: -145, unit: "C" }, windSpeed: 350 }
4// API Saturna zwraca: { readings: { t: -178, w: 500 }, status: "stormy" }Bez adaptera musielibyśmy w każdym komponencie sprawdzać, z którego API przyszły dane i odpowiednio je przetwarzać. To prowadzi do bałaganu!
Implementacja Adaptera w React
Stwórzmy adaptery, które ujednolicą dane z różnych API:
1// Zunifikowany interfejs danych pogodowych
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}Teraz mamy trzy proste funkcje, z których każda przyjmuje surowe dane i zwraca obiekt o identycznej strukturze. Nasz komponent pogodowy może korzystać z tych danych bez wiedzy o tym, skąd pochodzą:
1function PlanetWeatherCard({ weatherData }) {
2 // weatherData ma ZAWSZE ten sam kształt dzięki adapterom
3 return (
4 <div className="weather-card">
5 <h3>{weatherData.planet}</h3>
6 <p>Temperatura: {weatherData.temperature} C</p>
7 <p>Wiatr: {weatherData.windSpeed} km/h</p>
8 <p>Stan: {weatherData.condition}</p>
9 </div>
10 );
11}Adapter jako Custom Hook
Adaptery doskonale sprawdzają się jako custom hooki, które opakowują zewnętrzne biblioteki:
1// Adapter dla różnych bibliotek map kosmicznych
2function useSpaceMap(provider) {
3 const [map, setMap] = useState(null);
4
5 useEffect(() => {
6 let mapInstance;
7
8 if (provider === 'galactic-maps') {
9 // Biblioteka GalacticMaps ma swoje API
10 mapInstance = new GalacticMaps.Chart('#map');
11 mapInstance.init({ zoom: 5 });
12 } else if (provider === 'star-atlas') {
13 // StarAtlas ma zupełnie inne API
14 mapInstance = StarAtlas.create(document.getElementById('map'), {
15 level: 5,
16 mode: '3d',
17 });
18 }
19
20 // Zwracamy zunifikowany interfejs
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}Dzięki temu hookowi, komponent korzystający z mapy nie musi wiedzieć, która biblioteka jest używana pod spodem. Gdybyśmy chcieli zmienić dostawcę map, wystarczy zmodyfikować tylko hook - żaden komponent się nie zmieni.
Wzorzec Facade
Czym jest Facade?
Wzorzec Facade (Fasada) dostarcza uproszczony interfejs do złożonego podsystemu. W React najczęściej implementujemy go jako hooki, które ukrywają skomplikowaną logikę za prostym API.
Pomyśl o panelu kontrolnym statku kosmicznego. Pilot nie musi znać szczegółów działania każdego systemu - naciska przycisk "Start silników" i cały złożony proces uruchamia się automatycznie. Fasada działa dokładnie tak samo.
Prosty przykład: Hook jako Facade
Bez fasady, komponent musi sam zarządzać wieloma szczegółami:
1// BEZ Facade - komponent jest przytłoczony szczegółami
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 // Sprawdzanie paliwa
11 useEffect(() => {
12 fetchFuelLevel().then(setFuel);
13 }, []);
14
15 // Sprawdzanie systemów
16 useEffect(() => {
17 checkAllSystems().then(setSystems);
18 }, []);
19
20 // Countdown
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, 'Za mało paliwa']); return; }
33 if (!crewReady) { setErrors(e => [...e, 'Załoga niegotowa']); return; }
34 const failedSystems = systems.filter(s => s.status !== 'ok');
35 if (failedSystems.length > 0) {
36 setErrors(e => [...e, 'Systemy niesprawne']);
37 return;
38 }
39 setCountdown(10);
40 };
41
42 // ... reszta komponentu z ogromną ilością logiki
43}Z fasadą przenosimy całą tę logikę do hooka:
1// Z Facade - hook ukrywa całą złożoność
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('Za mało paliwa');
28 if (!crewReady) newErrors.push('Załoga niegotowa');
29 const failedSystems = systems.filter(s => s.status !== 'ok');
30 if (failedSystems.length > 0) newErrors.push('Systemy niesprawne');
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 // Proste, czytelne API
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// Komponent jest teraz czysty i prosty
59function MissionLauncher() {
60 const mission = useMissionLaunch();
61
62 return (
63 <div>
64 <h1>Start misji</h1>
65 <p>Paliwo: {mission.fuelLevel}%</p>
66 <p>Status: {mission.status}</p>
67 {mission.countdown !== null && <p>Odliczanie: {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 Start
71 </button>
72 <button onClick={mission.abort}>Przerwij</button>
73 </div>
74 );
75}Facade dla złożonych operacji
Facade jest szczególnie przydatny, gdy musisz skoordynować wiele operacji:
1function useCrewManagement(missionId) {
2 // Wewnętrznie zarządza wieloma źródłami danych
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 // Prosty interfejs dla komponentu
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}Kiedy stosować Adapter, a kiedy Facade?
| Cecha | Adapter | Facade |
|---|---|---|
| Cel | Ujednolicenie niekompatybilnych interfejsów | Uproszczenie złożonego systemu |
| Kiedy | Wiele źródeł danych o różnych formatach | Skomplikowana logika do ukrycia |
| W React | Opakowywanie zewnętrznych API/bibliotek | Custom hooki ukrywające złożoność |
| Analogia kosmiczna | Translator języków obcych cywilizacji | Panel sterowania statku |
Oba wzorce można łączyć - Facade może wewnętrznie korzystać z adapterów:
1function useGalacticWeather(planets) {
2 // Facade: upraszcza interfejs
3 const [weather, setWeather] = useState({});
4
5 useEffect(() => {
6 const adapters = {
7 mars: adaptMarsWeather,
8 jupiter: adaptJupiterWeather,
9 saturn: adaptSaturnWeather,
10 };
11
12 // Adapter: ujednolica dane z różnych 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}Podsumowanie
Wzorce Adapter i Facade to niezbędne narzędzia w arsenale kosmicznego programisty React:
- Adapter pozwala ujednolicić niekompatybilne interfejsy, co jest kluczowe przy integracji z wieloma zewnętrznymi API i bibliotekami
- Facade upraszcza złożone podsystemy, tworząc czytelne i łatwe w użyciu hooki
- Oba wzorce zwiększają czytelność kodu, ułatwiają testowanie i redukują coupling między komponentami
Podobnie jak uniwersalny adapter zasilania pozwala podłączyć statek kosmiczny do dowolnej stacji dokującej w galaktyce, tak wzorce Adapter i Facade pozwalają naszym komponentom React współpracować z dowolnymi systemami bez zbędnych komplikacji.
Kod do tej lekcji: App.jsx
1import React, { useState, useEffect } from 'react';
2
3// ============================================
4// WZORZEC ADAPTER I FACADE
5// ============================================
6
7// --- ADAPTER: Ujednolicanie danych z roznych API planet ---
8
9// Symulacja surowych odpowiedzi z roznych 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// Adaptery - kazdy zamienia surowe dane na wspolny format
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// Komponent korzystajacy z ujednoliconych danych
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>Temperatura: <strong>{weatherData.temperature} C</strong></p>
58 <p>Wiatr: <strong>{weatherData.windSpeed} km/h</strong></p>
59 <p>
60 Stan:{' '}
61 <span style={{ color: getConditionColor(weatherData.condition), fontWeight: 'bold' }}>
62 {weatherData.condition}
63 </span>
64 </p>
65 </div>
66 );
67}
68
69// --- FACADE: Hook ukrywajacy zlozonosc ---
70
71function useGalacticWeather() {
72 const [weather, setWeather] = useState([]);
73 const [loading, setLoading] = useState(true);
74 const [selectedPlanet, setSelectedPlanet] = useState(null);
75
76 useEffect(() => {
77 // Facade ukrywa cala logike pobierania i adaptowania danych
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 // Prosty interfejs dla komponentu
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// --- Glowna aplikacja ---
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 i Facade - Pogoda w galaktyce
118 </h1>
119 <p style={{ textAlign: 'center', color: '#aaa', marginBottom: '24px' }}>
120 Dane z 3 roznych API, ujednolicone przez Adaptery, uproszczone przez Facade
121 </p>
122
123 {loading ? (
124 <p style={{ textAlign: 'center', color: '#ffd700' }}>Laczenie z sondami...</p>
125 ) : (
126 <>
127 <div style={statsStyle}>
128 <span>Najzimniejsza: <strong style={{ color: '#00d4ff' }}>{coldestPlanet}</strong></span>
129 <span>Srednia temp: <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' }}>Szczegoly: {selectedPlanet.planet}</h3>
147 <p>Temperatura: {selectedPlanet.temperature} C</p>
148 <p>Wiatr: {selectedPlanet.windSpeed} km/h</p>
149 <p>Warunki: {selectedPlanet.condition}</p>
150 <button
151 onClick={() => selectPlanet(null)}
152 style={btnStyle}
153 >
154 Zamknij
155 </button>
156 </div>
157 )}
158
159 {/* TODO: Dodaj nowa planete (np. Neptun) z wlasnym surowym formatem danych
160 i napisz dla niej adapter. Surowy format Neptuna:
161 { metrics: { tempC: -214, windMs: 580 }, weather: 'icy' }
162 Adapter powinien zamienic windMs na 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;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż poprawną składnię lazy loading komponentu: