Kurs JavaScript i TypeScript · Moduł 9: Wzorce projektowe i architektura
Architektura event-driven i systemy czasowe w JavaScript
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:
- Observer (exercise 9_1) - fundament systemu zdarzeń
- SOLID (exercise 9_2) - luźne powiązanie, otwartość na rozszerzenia
- Programowanie funkcyjne (exercise 9_3) - czyste funkcje do przetwarzania zdarzeń
- Zarządzanie stanem (exercise 9_4) - reaktywne aktualizacje stanu
- 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. Czym charakteryzuje się architektura event-driven?
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.