Kurs JavaScript i TypeScript · Moduł 9: Wzorce projektowe i architektura

Architektura event-driven i systemy czasowe w JavaScript

6 min czytania
W tej lekcji9

Po opanowaniu wzorców projektowych, zasad SOLID, programowania funkcyjnego i optymalizacji bundli, czas połączyć tę wiedzę w praktyce. W tym rozdziale zbudujemy system oparty na zdarzeniach (event-driven architecture) z symulacją czasu - wzorzec powszechnie stosowany w grach, symulacjach i systemach monitoringu.

Czym jest architektura event-driven?

Architektura event-driven to podejście, w którym przepływ programu jest sterowany przez zdarzenia - akcje użytkownika, sygnały z sensorów, upływ czasu czy komunikaty z innych systemów. Zamiast liniowego wykonywania kodu, system reaguje na zdarzenia w momencie ich wystąpienia.

1// Tradycyjne podejście - liniowe
2function checkPark() {
3  checkFences();
4  feedDinosaurs();
5  updateVisitors();
6  generateReport();
7}
8
9// Event-driven - reaktywne
10const eventBus = new EventEmitter();
11
12eventBus.on('fence:alarm', handleFenceAlarm);
13eventBus.on('dinosaur:hungry', scheduleFeedingRun);
14eventBus.on('visitor:entered', updateVisitorCount);
15eventBus.on('hour:changed', generateHourlyReport);

W podejściu event-driven każdy komponent działa niezależnie i reaguje tylko na zdarzenia, które go dotyczą. To prowadzi do luźnego powiązania (loose coupling) - celu, któremu służą też poznane wcześniej zasady SOLID, zwłaszcza zasada odwrócenia zależności.

Wzorzec EventEmitter

Sercem architektury event-driven jest EventEmitter - implementacja wzorca Observer, który poznaliśmy w pierwszej lekcji tego modułu.

1class EventEmitter {
2  constructor() {
3    this.listeners = new Map();
4  }
5
6  on(event, callback) {
7    if (!this.listeners.has(event)) {
8      this.listeners.set(event, []);
9    }
10    this.listeners.get(event).push(callback);
11    return this; // Fluent interface
12  }
13
14  off(event, callback) {
15    if (!this.listeners.has(event)) return this;
16    const callbacks = this.listeners.get(event);
17    const index = callbacks.indexOf(callback);
18    if (index > -1) callbacks.splice(index, 1);
19    return this;
20  }
21
22  emit(event, ...args) {
23    if (!this.listeners.has(event)) return false;
24    // Kopia tablicy, bo once() usuwa listener w trakcie pętli
25    [...this.listeners.get(event)].forEach(callback => {
26      callback(...args);
27    });
28    return true;
29  }
30
31  once(event, callback) {
32    const wrapper = (...args) => {
33      this.off(event, wrapper); // najpierw wyrejestruj, potem wywołaj
34      callback(...args);
35    };
36    return this.on(event, wrapper);
37  }
38}

Metoda once to elegancki przykład wzorca Decorator - opakowuje oryginalny callback w wrapper, który automatycznie się wyrejestrowuje po pierwszym wywołaniu.

Symulacja czasu - cykl dzień/noc

W systemach symulacyjnych czas jest kluczowym elementem. W Parku Jurajskim cykl dzień/noc wpływa na zachowanie dinozaurów, harmonogram karmienia, godziny odwiedzin i protokoły bezpieczeństwa.

1class TimeSimulation extends EventEmitter {
2  constructor() {
3    super();
4    this.currentHour = 6; // Start o 6:00
5    this.day = 1;
6    this.scheduledEvents = [];
7    this.isRunning = false;
8  }
9
10  advanceHour() {
11    this.currentHour++;
12
13    if (this.currentHour >= 24) {
14      this.currentHour = 0;
15      this.day++;
16      this.emit('day:changed', this.day);
17    }
18
19    this.emit('hour:changed', this.currentHour, this.day);
20
21    // Sprawdź zaplanowane zdarzenia
22    this.checkScheduledEvents();
23
24    // Emituj zdarzenie tylko przy zmianie pory dnia
25    const timeOfDay = this.getTimeOfDay();
26    if (timeOfDay !== this.timeOfDay) {
27      this.timeOfDay = timeOfDay;
28      this.emit('timeOfDay:changed', timeOfDay);
29    }
30  }
31
32  getTimeOfDay() {
33    if (this.currentHour >= 6 && this.currentHour < 12) return 'rano';
34    if (this.currentHour >= 12 && this.currentHour < 18) return 'dzien';
35    if (this.currentHour >= 18 && this.currentHour < 22) return 'wieczor';
36    return 'noc';
37  }
38
39  scheduleEvent(hour, callback, options = {}) {
40    const event = {
41      hour,
42      callback,
43      recurring: options.recurring || false,
44      label: options.label || 'unnamed'
45    };
46    this.scheduledEvents.push(event);
47    return event;
48  }
49
50  checkScheduledEvents() {
51    this.scheduledEvents = this.scheduledEvents.filter(event => {
52      if (event.hour === this.currentHour) {
53        event.callback(this.currentHour, this.day);
54        return event.recurring; // Zachowaj tylko cykliczne
55      }
56      return true;
57    });
58  }
59}

Zwróć uwagę, jak klasa TimeSimulation dziedziczy po EventEmitter - to zastosowanie zasady Open/Closed z SOLID. Możemy rozszerzać zachowanie symulacji przez dodawanie nowych listenerów, bez modyfikacji samej klasy.

Planowanie zdarzeń cyklicznych

W realnym systemie wiele zdarzeń powtarza się cyklicznie - karmienie dinozaurów, patrole bezpieczeństwa, raporty statusu.

1const sim = new TimeSimulation();
2
3// Karmienie mięsożerców - codziennie o 7:00 i 17:00
4sim.scheduleEvent(7, (hour, day) => {
5  console.log(`Dzień ${day}, ${hour}:00 - Karmienie mięsożerców`);
6}, { recurring: true, label: 'karmienie-miesozerce' });
7
8sim.scheduleEvent(17, (hour, day) => {
9  console.log(`Dzień ${day}, ${hour}:00 - Drugie karmienie mięsożerców`);
10}, { recurring: true, label: 'karmienie-miesozerce-2' });
11
12// Patrole bezpieczeństwa - co godzinę w nocy (22:00-5:00)
13for (const h of [22, 23, 0, 1, 2, 3, 4, 5]) {
14  sim.scheduleEvent(h, (hour, day) => {
15    console.log(`Dzień ${day}, ${hour}:00 - Patrol nocny`);
16  }, { recurring: true, label: `patrol-${h}` });
17}
18
19// Raport dzienny - o 8:00
20sim.scheduleEvent(8, (hour, day) => {
21  console.log(`Dzień ${day} - Generowanie raportu dziennego`);
22}, { recurring: true, label: 'raport-dzienny' });

Reakcja na pory dnia

Architektura event-driven pozwala różnym podsystemom reagować niezależnie na tę samą zmianę:

1// System oświetlenia
2sim.on('timeOfDay:changed', (timeOfDay) => {
3  const lightingProfiles = {
4    rano: { brightness: 70, color: 'warm-white' },
5    dzien: { brightness: 100, color: 'daylight' },
6    wieczor: { brightness: 40, color: 'amber' },
7    noc: { brightness: 10, color: 'red' } // Czerwone, by nie płoszyć dinozaurów
8  };
9  const profile = lightingProfiles[timeOfDay];
10  console.log(`Oświetlenie: ${profile.brightness}% (${profile.color})`);
11});
12
13// System bezpieczeństwa
14sim.on('timeOfDay:changed', (timeOfDay) => {
15  if (timeOfDay === 'noc') {
16    console.log('Aktywacja nocnych protokołów bezpieczeństwa');
17    console.log('Zwiększenie napięcia ogrodzeń o 20%');
18    console.log('Włączenie kamer termowizyjnych');
19  } else if (timeOfDay === 'rano') {
20    console.log('Dezaktywacja nocnych protokołów');
21    console.log('Przywrócenie standardowego napięcia ogrodzeń');
22  }
23});
24
25// System odwiedzających
26sim.on('hour:changed', (hour) => {
27  if (hour === 9) console.log('Park otwarty dla odwiedzających');
28  if (hour === 18) console.log('Ostatnie wejście dla odwiedzających');
29  if (hour === 20) console.log('Park zamknięty - ewakuacja gości');
30});

Łączenie z programowaniem funkcyjnym

Architekturę event-driven można wzbogacić o techniki funkcyjne, które poznaliśmy wcześniej w module:

1// Funkcje czyste do transformacji danych
2const calculateFeedingAmount = (species, weight, hour) => {
3  const baseAmount = weight * 0.02; // 2% masy ciała
4  const timeMultiplier = hour < 12 ? 1.2 : 0.8; // Więcej rano
5  return Math.round(baseAmount * timeMultiplier);
6};
7
8// Kompozycja funkcji do przetwarzania zdarzeń
9const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
10
11const processAlert = pipe(
12  (alert) => ({ ...alert, timestamp: Date.now() }),
13  (alert) => ({ ...alert, severity: alert.level > 7 ? 'critical' : 'warning' }),
14  (alert) => ({ ...alert, notified: true })
15);
16
17// Użycie z systemem zdarzeń
18sim.on('alert:triggered', (rawAlert) => {
19  const processedAlert = processAlert(rawAlert);
20  console.log(`Alert [${processedAlert.severity}]: ${processedAlert.message}`);
21});

Zarządzanie stanem symulacji

Łączymy wzorzec zarządzania stanem (exercise_9_4) z systemem zdarzeń:

1class SimulationState {
2  constructor(simulation) {
3    this.state = {
4      parkStatus: 'operational',
5      activeAlerts: 0,
6      dinosaursCount: 0,
7      visitorsCount: 0,
8      fencesStatus: 'all-operational'
9    };
10    this.history = [];
11
12    // Reaguj na zdarzenia symulacji
13    simulation.on('hour:changed', (hour) => {
14      this.updateState({ lastUpdate: hour });
15    });
16
17    simulation.on('alert:triggered', () => {
18      this.updateState({
19        activeAlerts: this.state.activeAlerts + 1
20      });
21    });
22  }
23
24  updateState(changes) {
25    // Niemutowalność - nowy obiekt stanu
26    const previousState = { ...this.state };
27    this.state = { ...this.state, ...changes };
28    this.history.push({
29      timestamp: Date.now(),
30      previous: previousState,
31      current: { ...this.state }
32    });
33  }
34
35  getState() {
36    return Object.freeze({ ...this.state });
37  }
38}

Testowanie systemów event-driven

Systemy oparte na zdarzeniach są łatwe do testowania dzięki luźnemu powiązaniu komponentów:

1// Test symulacji czasu
2function testTimeSimulation() {
3  const sim = new TimeSimulation();
4  const events = [];
5
6  sim.on('hour:changed', (hour) => events.push({ type: 'hour', hour }));
7  sim.on('day:changed', (day) => events.push({ type: 'day', day }));
8
9  // Symuluj 25 godzin (przejście przez cały dzień)
10  for (let i = 0; i < 25; i++) {
11    sim.advanceHour();
12  }
13
14  console.log(`Zarejestrowane zdarzenia: ${events.length}`);
15  console.log(`Zmiana dnia: ${events.filter(e => e.type === 'day').length}`);
16  console.log(`Aktualny dzień: ${sim.day}, godzina: ${sim.currentHour}`);
17}
18
19testTimeSimulation();

Podsumowanie

Architektura event-driven łączy w sobie wiele wzorców i zasad, które poznałeś w tym module:

  1. Observer (exercise 9_1) - fundament systemu zdarzeń
  2. SOLID (exercise 9_2) - luźne powiązanie, otwartość na rozszerzenia
  3. Programowanie funkcyjne (exercise 9_3) - czyste funkcje do przetwarzania zdarzeń
  4. Zarządzanie stanem (exercise 9_4) - reaktywne aktualizacje stanu
  5. Optymalizacja (exercise 9_5-9_7) - mały bundle i podsystemy ładowane dopiero wtedy, gdy są potrzebne

To przygotowanie do projektu końcowego, w którym zintegrujesz wszystkie te elementy w kompletny system zarządzania Parkiem Jurajskim.

Kod do tej lekcji: index.js
1// Symulacja czasu w parku
2// Ćwiczenie: Zaimplementuj symulację cyklu dzień/noc
3
4class TimeSimulation {
5  constructor() {
6    this.currentHour = 6;
7    this.day = 1;
8    this.events = [];
9    this.isRunning = false;
10  }
11
12  // Zaimplementuj metody:
13  // advanceHour() - przesuń czas o godzinę
14  // scheduleEvent(hour, callback) - zaplanuj zdarzenie
15  // getTimeOfDay() - zwróć porę dnia (rano/dzień/wieczór/noc)
16
17  advanceHour() {
18    // Twój kod tutaj
19  }
20
21  scheduleEvent(hour, callback) {
22    // Twój kod tutaj
23  }
24
25  getTimeOfDay() {
26    // Twój kod tutaj
27  }
28}
29
30const sim = new TimeSimulation();
31console.log("Symulacja: Dzień", sim.day, "Godzina:", sim.currentHour);

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym charakteryzuje się architektura event-driven?

  2. 2. Jakie trzy podstawowe metody powinien implementować EventEmitter?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż elementy wywołania EventEmitter.on w odpowiedniej kolejności:

  • Edytor kodu

    W pliku index.js jest EventEmitter z lekcji o architekturze event-driven i klasa ParkTimeSystem, która symuluje godziny w parku. Uzupełnij luki: ___BLANK1___ to metoda tablicy, która w off() usuwa z listy jeden listener pod znalezionym indeksem, ___BLANK2___ to klasa, po której dziedziczy ParkTimeSystem, żeby mieć metody on, off i emit, a ___BLANK3___ to metoda tablicy, która z zaplanowanych wydarzeń wybiera tylko te na bieżącą godzinę. Co godzinę advanceHour() emituje 'hour:changed' z godziną i dniem, po północy najpierw 'day:changed' z numerem dnia, a każde wydarzenie zaplanowane metodą schedule(hour, name) na tę godzinę wywołuje 'event:triggered' z nazwą i godziną. start(intervalMs) uruchamia pętlę setInterval, a stop() ją zatrzymuje.

  • Układanie w pionie

    Uporządkuj etapy budowania systemu event-driven:

  • Klikanie w kolejności

    Ułóż poziomy testowania od najniższego do najwyższego.

Przydatne artykuły