Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Wzorzec Adapter i Facade

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 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 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.

Vai a CodeWorlds