Kurs JavaScript i React · Moduł 8: Zaawansowane wzorce React

Wzorzec Adapter i Facade

7 min czytania
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?

CechaAdapterFacade
CelUjednolicenie niekompatybilnych interfejsówUproszczenie złożonego systemu
KiedyWiele źródeł danych o różnych formatachSkomplikowana logika do ukrycia
W ReactOpakowywanie zewnętrznych API/bibliotekCustom hooki ukrywające złożoność
Analogia kosmicznaTranslator języków obcych cywilizacjiPanel 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:

Przydatne artykuły