Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Architektura event-driven i systemy czasowe w JavaScript

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) - jednej z kluczowych zasad SOLID, którą poznałeś wcześniej.

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    this.listeners.get(event).forEach(callback => {
25      callback(...args);
26    });
27    return true;
28  }
29
30  once(event, callback) {
31    const wrapper = (...args) => {
32      callback(...args);
33      this.off(event, wrapper);
34    };
35    return this.on(event, wrapper);
36  }
37}

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 zdarzenia pory dnia
25    const timeOfDay = this.getTimeOfDay();
26    this.emit('timeOfDay:changed', timeOfDay);
27  }
28
29  getTimeOfDay() {
30    if (this.currentHour >= 6 && this.currentHour < 12) return 'rano';
31    if (this.currentHour >= 12 && this.currentHour < 18) return 'dzien';
32    if (this.currentHour >= 18 && this.currentHour < 22) return 'wieczor';
33    return 'noc';
34  }
35
36  scheduleEvent(hour, callback, options = {}) {
37    const event = {
38      hour,
39      callback,
40      recurring: options.recurring || false,
41      label: options.label || 'unnamed'
42    };
43    this.scheduledEvents.push(event);
44    return event;
45  }
46
47  checkScheduledEvents() {
48    this.scheduledEvents = this.scheduledEvents.filter(event => {
49      if (event.hour === this.currentHour) {
50        event.callback(this.currentHour, this.day);
51        return event.recurring; // Zachowaj tylko cykliczne
52      }
53      return true;
54    });
55  }
56}

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
13for (let h = 22; h <= 23; h++) {
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('Ostatni wpust 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(`Rejestrowane 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) - wydajne przetwarzanie dużej liczby zdarzeń

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

Vai a CodeWorlds