Cours JavaScript et TypeScript · Module 9: Design patterns et architecture
Architecture événementielle et systèmes temporels en JavaScript
Dans cette leçon9
Après avoir maîtrisé les patterns de conception, les principes SOLID, la programmation fonctionnelle et l'optimisation du bundle, il est temps de combiner ces connaissances en pratique. Dans ce chapitre, tu vas construire un système événementiel avec simulation du temps - un pattern couramment utilisé dans les jeux, les simulations et les systèmes de monitoring.
Qu'est-ce que l'architecture événementielle ?
L'architecture événementielle est une approche dans laquelle le déroulement du programme est piloté par des événements - actions de l'utilisateur, signaux de capteurs, écoulement du temps ou messages provenant d'autres systèmes. Au lieu d'une exécution linéaire du code, le système réagit aux événements à mesure qu'ils surviennent.
1// Traditional approach - linear
2function checkPark() {
3 checkFences();
4 feedDinosaurs();
5 updateVisitors();
6 generateReport();
7}
8
9// Event-driven - reactive
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);Dans l'approche événementielle, chaque composant fonctionne de façon indépendante et ne réagit qu'aux événements qui le concernent. Cela conduit à un couplage faible - un objectif que servent aussi les principes SOLID vus plus tôt, en particulier l'inversion des dépendances.
Le pattern EventEmitter
Au cœur de l'architecture événementielle se trouve l'EventEmitter - une implémentation du pattern Observer que nous avons vu dans la première leçon de ce module.
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 // Copy the array, because once() removes a listener during the loop
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); // unregister first, then call
34 callback(...args);
35 };
36 return this.on(event, wrapper);
37 }
38}La méthode once est un exemple élégant du pattern Decorator - elle enveloppe le callback d'origine dans un wrapper qui se désinscrit automatiquement après le premier appel.
Simulation du temps - cycle jour/nuit
Dans les systèmes de simulation, le temps est un élément clé. À Jurassic Park, le cycle jour/nuit influence le comportement des dinosaures, les horaires de nourrissage, les heures de visite et les protocoles de sécurité.
1class TimeSimulation extends EventEmitter {
2 constructor() {
3 super();
4 this.currentHour = 6; // Start at 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 // Check scheduled events
22 this.checkScheduledEvents();
23
24 // Emit only when the time of day changes
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 'morning';
34 if (this.currentHour >= 12 && this.currentHour < 18) return 'day';
35 if (this.currentHour >= 18 && this.currentHour < 22) return 'evening';
36 return 'night';
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; // Keep only recurring ones
55 }
56 return true;
57 });
58 }
59}Remarque comment la classe TimeSimulation hérite d'EventEmitter - c'est une application du principe Open/Closed de SOLID. On peut étendre le comportement de la simulation en ajoutant de nouveaux listeners, sans modifier la classe elle-même.
Planifier des événements récurrents
Dans un système réel, de nombreux événements se répètent de façon cyclique - nourrir les dinosaures, les patrouilles de sécurité, les rapports d'état.
1const sim = new TimeSimulation();
2
3// Feeding carnivores - daily at 7:00 and 17:00
4sim.scheduleEvent(7, (hour, day) => {
5 console.log(`Day ${day}, ${hour}:00 - Feeding carnivores`);
6}, { recurring: true, label: 'feeding-carnivores' });
7
8sim.scheduleEvent(17, (hour, day) => {
9 console.log(`Day ${day}, ${hour}:00 - Second feeding for carnivores`);
10}, { recurring: true, label: 'feeding-carnivores-2' });
11
12// Security patrols - every hour at night (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(`Day ${day}, ${hour}:00 - Night patrol`);
16 }, { recurring: true, label: `patrol-${h}` });
17}
18
19// Daily report - at 8:00
20sim.scheduleEvent(8, (hour, day) => {
21 console.log(`Day ${day} - Generating daily report`);
22}, { recurring: true, label: 'daily-report' });Réagir aux moments de la journée
L'architecture événementielle permet à différents sous-systèmes de réagir indépendamment au même changement :
1// Lighting system
2sim.on('timeOfDay:changed', (timeOfDay) => {
3 const lightingProfiles = {
4 morning: { brightness: 70, color: 'warm-white' },
5 day: { brightness: 100, color: 'daylight' },
6 evening: { brightness: 40, color: 'amber' },
7 night: { brightness: 10, color: 'red' } // Red so as not to startle dinosaurs
8 };
9 const profile = lightingProfiles[timeOfDay];
10 console.log(`Lighting: ${profile.brightness}% (${profile.color})`);
11});
12
13// Security system
14sim.on('timeOfDay:changed', (timeOfDay) => {
15 if (timeOfDay === 'night') {
16 console.log('Activating night security protocols');
17 console.log('Increasing fence voltage by 20%');
18 console.log('Enabling thermal cameras');
19 } else if (timeOfDay === 'morning') {
20 console.log('Deactivating night protocols');
21 console.log('Restoring standard fence voltage');
22 }
23});
24
25// Visitor system
26sim.on('hour:changed', (hour) => {
27 if (hour === 9) console.log('Park open for visitors');
28 if (hour === 18) console.log('Last visitor admission');
29 if (hour === 20) console.log('Park closed - guest evacuation');
30});Combiner avec la programmation fonctionnelle
L'architecture événementielle peut être enrichie par les techniques fonctionnelles vues plus tôt dans le module :
1// Pure functions for data transformation
2const calculateFeedingAmount = (species, weight, hour) => {
3 const baseAmount = weight * 0.02; // 2% of body mass
4 const timeMultiplier = hour < 12 ? 1.2 : 0.8; // More in the morning
5 return Math.round(baseAmount * timeMultiplier);
6};
7
8// Function composition for event processing
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// Usage with event system
18sim.on('alert:triggered', (rawAlert) => {
19 const processedAlert = processAlert(rawAlert);
20 console.log(`Alert [${processedAlert.severity}]: ${processedAlert.message}`);
21});Gérer l'état de la simulation
On combine le pattern de gestion d'état (exercise_9_4) avec le système d'événements :
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 // React to simulation events
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 // Immutability - new state object
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}Tester les systèmes événementiels
Les systèmes événementiels sont faciles à tester grâce au couplage faible de leurs composants :
1// Time simulation test
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 // Simulate 25 hours (going through an entire day)
10 for (let i = 0; i < 25; i++) {
11 sim.advanceHour();
12 }
13
14 console.log(`Recorded events: ${events.length}`);
15 console.log(`Day changes: ${events.filter(e => e.type === 'day').length}`);
16 console.log(`Current day: ${sim.day}, hour: ${sim.currentHour}`);
17}
18
19testTimeSimulation();Résumé
L'architecture événementielle combine de nombreux patterns et principes que tu as appris dans ce module :
- Observer (exercise 9_1) - la base du système d'événements
- SOLID (exercise 9_2) - couplage faible, ouverture à l'extension
- Programmation fonctionnelle (exercise 9_3) - fonctions pures pour traiter les événements
- Gestion d'état (exercise 9_4) - mises à jour réactives de l'état
- Optimisation (exercise 9_5-9_7) - un bundle léger et des sous-systèmes chargés seulement quand ils sont nécessaires
C'est une préparation au projet final, où tu intégreras tous ces éléments dans un système complet de gestion de Jurassic Park.
Code de cette leçon : index.js
1// Simulation du temps dans le parc
2// Exercice : implémente la simulation du cycle jour/nuit
3
4class TimeSimulation {
5 constructor() {
6 this.currentHour = 6;
7 this.day = 1;
8 this.events = [];
9 this.isRunning = false;
10 }
11
12 // Implémente les méthodes :
13 // advanceHour() - avance le temps d'une heure
14 // scheduleEvent(hour, callback) - planifie un événement
15 // getTimeOfDay() - retourne le moment de la journée (matin/jour/soir/nuit)
16
17 advanceHour() {
18 // Ton code ici
19 }
20
21 scheduleEvent(hour, callback) {
22 // Ton code ici
23 }
24
25 getTimeOfDay() {
26 // Ton code ici
27 }
28}
29
30const sim = new TimeSimulation();
31console.log("Simulation : jour", sim.day, "Heure :", sim.currentHour);Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce qui caractérise l'architecture événementielle ?
2. Quelles sont les trois méthodes de base que devrait implémenter un EventEmitter ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Range les éléments d'un appel EventEmitter.on dans le bon ordre :
- Éditeur de code
Le fichier index.js contient l'EventEmitter de la leçon sur l'architecture événementielle et une classe ParkTimeSystem qui simule les heures dans le parc. Complète les blancs : ___BLANK1___ est la méthode de tableau qui, dans off(), retire un écouteur à l'indice trouvé, ___BLANK2___ est la classe dont hérite ParkTimeSystem pour avoir les méthodes on, off et emit, et ___BLANK3___ est la méthode de tableau qui ne garde, parmi les événements planifiés, que ceux de l'heure actuelle. Chaque heure, advanceHour() émet 'hour:changed' avec l'heure et le jour, après minuit d'abord 'day:changed' avec le numéro du jour, et chaque événement planifié avec schedule(hour, name) pour cette heure émet 'event:triggered' avec le nom et l'heure. start(intervalMs) lance une boucle setInterval, et stop() l'arrête.
- Tri vertical
Range les étapes de construction d'un système événementiel :
- Clique dans l'ordre
Range les niveaux de test du plus bas au plus haut.