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.
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.
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.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.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' });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});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});Łą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}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();Architektura event-driven łączy w sobie wiele wzorców i zasad, które poznałeś w tym module:
To przygotowanie do projektu końcowego, w którym zintegrujesz wszystkie te elementy w kompletny system zarządzania Parkiem Jurajskim.