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 (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!
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}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 (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.
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 malo paliwa']); return; }
33 if (!crewReady) { setErrors(e => [...e, 'Zaloga 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 malo paliwa');
28 if (!crewReady) newErrors.push('Zaloga 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 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}| 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}Wzorce Adapter i Facade to niezbędne narzędzia w arsenale kosmicznego programisty React:
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.