commit e12f64776665e05dccddd5701a900667d6e7a623 Author: Johan Date: Mon Dec 15 16:45:48 2025 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7262242 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# TP : préparation d'un café (JavaScript asynchrone) + +## Informations générales + +**Cours** : JavaScript Avancé > Programmation concurrente > Tâches IO-bound concurrentes avec async/await \ +**Objectifs pédagogiques** : +- JavaScript avancé : programmation asynchrone +- Prise en main de WebStorm +- Bonnes pratiques de l'entreprise + +--- + +## Prérequis + +### Connaissances préalables + +- Connaissances de base en programmation + +--- + +## Énoncé + +L'objectif de ce TP est de comprendre et de maîtriser les concepts de la programmation asynchrone en JavaScript. +Nous allons simuler la préparation d'un café en passant d'une approche "classique" basée sur les callbacks +à une implémentation moderne et optimisée avec les `Promises` et la syntaxe `async/await`. + +### Contexte + +La préparation d'un café est une séquence d'opérations qui prennent du temps (chauffer l'eau, moudre les grains, etc.). En JavaScript, de telles opérations (comme les requêtes réseau ou les accès aux fichiers) sont non-bloquantes. Nous allons simuler ce comportement avec `setTimeout`. + +### Partie 1 : la machine à café "Callback" + +Dans cette première partie, nous allons construire la logique de notre machine à café en utilisant des fonctions asynchrones qui prennent une **fonction de rappel (callback)** comme dernier argument. + +#### Objectifs + +1. Créer des fonctions asynchrones de base. +2. Comprendre l'enchaînement d'opérations asynchrones. +3. Mettre en évidence le problème du "Callback Hell" (la pyramide de l'enfer). + +#### Instructions + +```javascript +const TEMPS_SELECTION = 500; +const TEMPS_MOUTURE = 700; +const TEMPS_CHAUFFAGE = 1000; +const TEMPS_PREPARATION = 500; + +function selectionnerCafe(nomCafe, callback) { + console.log(`1. Sélection du café : ${nomCafe}...`); + setTimeout(() => { + console.log(` -> Café "${nomCafe}" sélectionné.`); + // Le premier argument est pour une erreur potentielle (ici, null). + // Le second est le résultat. + callback(null, nomCafe); + }, TEMPS_SELECTION); +} +``` + +1. **Configuration** + Créez des constantes pour définir les temps de simulation de chaque étape : + * `TEMPS_SELECTION`: 500 ms + * `TEMPS_MOUTURE`: 700 ms + * `TEMPS_CHAUFFAGE`: 1000 ms + * `TEMPS_PREPARATION`: 500 ms + +2. **Fonctions de base** + Implémentez les quatre fonctions suivantes. Chaque fonction doit simuler une tâche asynchrone avec `setTimeout` (cf exemple ci-dessus) et utiliser des `console.log` pour afficher le début et la fin de chaque étape. + + Le dernier argument de chaque fonction doit être un `callback`. Ce callback respectera la convention Node.js : `callback(erreur, resultat)`. S'il n'y a pas d'erreur, le premier argument sera `null`. + + * `selectionnerCafe(nomCafe, callback)` + * Affiche `1. Sélection du café : [nomCafe]...` + * Après `TEMPS_SELECTION`, affiche ` -> Café "[nomCafe]" sélectionné.` + * Appelle le `callback` avec `(null, nomCafe)`. + + * `moudreGrains(nomCafe, callback)` + * Affiche `2. Broyage des grains pour [nomCafe]...` + * Après `TEMPS_MOUTURE`, affiche ` -> Grains moulus.` + * Appelle le `callback` avec `(null, 'grains-moulus')`. + + * `chaufferEau(temperature, callback)` + * Affiche `3. Chauffage de l'eau à [temperature]°C...` + * Après `TEMPS_CHAUFFAGE`, affiche ` -> Eau chaude prête.` + * Appelle le `callback` avec `(null, 'eau-chaude')`. + + * `preparerCafe(typeCafe, grains, eau, callback)` + * Affiche `4. Préparation du café [typeCafe]...` + * Après `TEMPS_PREPARATION`, affiche ` -> Café [typeCafe] préparé.`. + * Appelle le `callback` avec `(null, 'café [typeCafe] préparé')`. + +3. **L'Enchaînement infernal** + Maintenant, utilisez les fonctions que vous venez de créer pour préparer un **"Expresso"** chauffé à **90°C**. + + * Pour garantir que les étapes se déroulent dans le bon ordre, vous devrez **imbriquer les appels** : le callback de `selectionnerCafe` doit appeler `moudreGrains`, son callback doit appeler `chaufferEau`, et ainsi de suite. + * À chaque étape, vérifiez la présence d'une erreur potentielle avant de continuer. + * Lorsque la préparation est terminée, affichez un message de succès : `SUCCÈS : Votre [café final] est servi !` + * Ajoutez un `console.log` tout à la fin de votre script pour montrer que le code principal continue de s'exécuter pendant la préparation du café. + + + +```mermaid +graph TD + subgraph "Pyramide des callbacks" + A[selectionnerCafe] --> B{Callback 1} + B --> C[moudreGrains] + C --> D{Callback 2} + D --> E[chaufferEau] + E --> F{Callback 3} + F --> G[preparerCafe] + G --> H{Callback 4} + H --> I[Succès] + end +``` + +> **Question** : Observez la structure de votre code (et le diagramme ci-dessus). Pourquoi cette imbrication est-elle appelée le "Callback Hell" ? Quels problèmes cela pose-t-il en termes de lisibilité et de maintenance ? + +----- + +### Partie 2 : modernisation avec `async/await` + +Nous allons maintenant refactoriser notre code pour utiliser les `Promises` et la syntaxe moderne `async/await`. +Cela nous permettra d'écrire un code asynchrone qui ressemble à du code synchrone, tout en optimisant les performances. + +#### Objectifs + +1. Transformer des fonctions à base de callbacks en fonctions retournant des `Promises`. +2. Utiliser `async/await` pour orchestrer des tâches asynchrones de manière lisible. +3. Gérer les erreurs proprement avec des blocs `try...catch`. +4. Optimiser les performances en exécutant des tâches indépendantes en parallèle avec `Promise.all`. + +#### Instructions + +1. **"Promisification" de l'API** + Regroupez la logique de la machine à café dans un objet `coffeeApi`. Transformez chaque fonction de la partie 1 pour qu'elle retourne une `Promise` au lieu d'utiliser un callback. + + * Ajoutez une nouvelle fonction : `nettoyerTasse()` qui prend `600ms` et résout la promesse avec la valeur `'tasse-propre'`. + * Dans `selectionnerCafe`, ajoutez une gestion d'erreur : si `coffeeName` est nul ou vide, la `Promise` doit être **rejetée** avec une `Error("Aucun type de café n'a été spécifié.")`. + + ```javascript + // exemple de transformation pour selectionnerCafe + const coffeeApi = { + selectionnerCafe(coffeeName) { + return new Promise((resolve, reject) => { + console.log(` - Sélection du café : ${coffeeName}...`); + setTimeout(() => { + if (!coffeeName) { + return reject(new Error("Aucun type de café n'a été spécifié.")); + } + console.log(` -> Café "${coffeeName}" sélectionné.`); + resolve(coffeeName); + }, config.TEMPS_SELECTION); + }); + }, + // autres fonctions ici + }; + ``` + +2. **Scénario 1 : préparation séquentielle**\ + Créez une fonction `async function runSequentialPreparation()`. + + * À l'intérieur, utilisez `await` pour préparer un café **"Lungo"** en suivant **strictement** cet ordre : + 1. Sélectionner le café. + 2. Moudre les grains. + 3. Chauffer l'eau (à 95°C). + 4. **Nettoyer la tasse.** + 5. Préparer le café final. + * Encadrez votre logique dans un bloc `try...catch` pour afficher un message d'erreur clair en cas d'échec. + * Utilisez `console.time("Temps total")` et `console.timeEnd("Temps total")` pour mesurer la durée totale de ce processus. + +```mermaid +graph TD + A[Début] --> B["await selectionnerCafe Lungo"] + B --> C["await moudreGrains"] + C --> D["await chaufferEau 95°C"] + D --> E["await nettoyerTasse"] + E --> F["await preparerCafe"] + F --> G[Fin] +``` + +3. **Scénario 2 : préparation optimisée** + Certaines tâches peuvent être effectuées en même temps \! Le broyage des grains, le chauffage de l'eau et le nettoyage de la tasse sont des opérations indépendantes. + + * Créez une nouvelle fonction `async function runOptimizedPreparation()`. + * Commencez par `await` la sélection d'un café **"Americano"**. + * Ensuite, lancez le broyage des grains, le chauffage de l'eau (à 92°C) et le nettoyage de la tasse de manière concurrente en utilisant `Promise.all()`. + * Une fois que *toutes* ces tâches sont terminées, `await` la préparation finale du café. + * Utilisez également `try...catch` et `console.time()` / `console.timeEnd()` pour la gestion des erreurs et la mesure du temps. + +```mermaid +graph TD + A[Début] --> B["await selectionnerCafe Americano"] + B --> C{Lancement parallèle Promise.all} + C --> D[moudreGrains] + C --> E["chaufferEau 92°C"] + C --> F[nettoyerTasse] + D --> G{Synchronisation} + E --> G + F --> G + G --> H[await preparerCafe] + H --> I[Fin] +``` + +4. **Point d'entrée** + Créez une fonction `async function main()` qui appellera `runSequentialPreparation()` puis `runOptimizedPreparation()` pour lancer les deux scénarios l'un après l'autre. + +> **Question** : Comparez les temps d'exécution des scénarios 1 et 2. Expliquez la différence de performance observée. Quel est l'avantage principal de `Promise.all` ici ? diff --git a/cafe_async_await.js b/cafe_async_await.js new file mode 100644 index 0000000..c88fc76 --- /dev/null +++ b/cafe_async_await.js @@ -0,0 +1,104 @@ +// =================================================================== +// Machine à café — version avec async/await +// =================================================================== + +/** + * @typedef {object} CoffeeMachineConfig + * @property {number} TEMPS_SELECTION - Temps en ms pour sélectionner le café. + * @property {number} TEMPS_MOUTURE - Temps en ms pour moudre les grains. + * @property {number} TEMPS_CHAUFFAGE - Temps en ms pour chauffer l'eau. + * @property {number} TEMPS_PREPARATION - Temps en ms pour la préparation finale. + * @property {number} CLEANING_TIME - Temps en ms pour laver la tasse. + */ + +/** @type {CoffeeMachineConfig} */ +const config = { + TEMPS_SELECTION: 500, + TEMPS_MOUTURE: 700, + TEMPS_CHAUFFAGE: 1000, + TEMPS_PREPARATION: 500, + TEMPS_NETTOYER: 600, +}; + +// --- API de la machine à café (fonctions "promisifiées") --- +// Chaque fonction retourne une Promise, simulant une opération asynchrone. +// Cette encapsulation sépare la "logique métier" de son orchestration. + +const coffeeApi = { + /** + * Simule la sélection d'un type de café. + * @param {string} coffeeName - le nom du café à sélectionner. + * @returns {Promise} une promesse qui se résout avec le nom du café. Rejette si aucun nom n'est fourni. + */ + selectionnerCafe(coffeeName) { + return new Promise((resolve, reject) => { + console.log(` - Sélection du café : ${coffeeName}...`); + setTimeout(() => { + if (!coffeeName) { + return reject(new Error("Aucun type de café n'a été spécifié.")); + } + console.log(` -> Café "${coffeeName}" sélectionné.`); + resolve(coffeeName); + }, config.TEMPS_SELECTION); + }); + }, + + // TODO : implémenter les autres fonctions de l'API de la machine à café. + +}; + + +// --- Orchestration des Scénarios --- + +/** + * Scénario 1 : préparation simple. + * Les tâches s'exécutent les unes après les autres. Simple mais plus lent. + */ +async function runSequentialPreparation() { + console.log("\n--- SCÉNARIO 1 : préparation simple ---"); + console.time("Temps total (séquentiel)"); + try { + // TODO : implémenter le scénario 1 en utilisant async/await. + + //console.log(`\nSUCCÈS : votre ${finalCoffee} est servi !`); + } catch (error) { + console.error("ERREUR : la préparation a échoué.", error.message); + } finally { + console.timeEnd("Temps total (séquentiel)"); + } +} + +/** + * Scénario 2 : préparation optimisée avec exécution concurrente de certaines tâches. + * Les tâches indépendantes (moudre, chauffer, laver) sont lancées en même temps. + */ +async function runOptimizedPreparation() { + console.log("\n--- SCÉNARIO 2 : préparation optimisée, avec exécution concurrente de certaines tâches ---"); + console.time("Temps total (optimisé)"); + try { + // TODO : implémenter le scénario 2 en utilisant async/await et Promise.all. + // Astuce : Promise.all retourne un tableau contenant les résultats des promesses dans le même ordre. + // Vous pouvez utiliser la déstructuration pour les récupérer facilement : const [resultat1, resultat2, ...] = await Promise.all(...) + + //console.log(`\nSUCCÈS (optimisé) : votre ${finalCoffee} est servi !`); + } catch (error) { + console.error("ERREUR (optimisé) : la préparation a échoué.", error.message); + } finally { + console.timeEnd("Temps total (optimisé)"); + } +} + +/** + * Point d'entrée principal pour lancer les démonstrations. + */ +async function main() { + console.log("=".repeat(50)); + console.log("Démonstration de la gestion asynchrone en JavaScript"); + console.log("=".repeat(50)); + await runSequentialPreparation(); + console.log("\n" + "-".repeat(50) + "\n"); + await runOptimizedPreparation(); +} + +// Lancement du script +main(); \ No newline at end of file diff --git a/cafe_callbacks.js b/cafe_callbacks.js new file mode 100644 index 0000000..b6c1e5b --- /dev/null +++ b/cafe_callbacks.js @@ -0,0 +1,83 @@ +// =================================================================== +// Machine à café — version avec callbacks +// =================================================================== + +// --- Configuration des temps de préparation (en millisecondes) --- +const TEMPS_SELECTION = 500; +const TEMPS_MOUTURE = 700; +const TEMPS_CHAUFFAGE = 1000; +const TEMPS_PREPARATION = 500; + +// --- PARTIE 1 : fonctions asynchrones de base (avec callbacks) --- +// Ces fonctions simulent des opérations asynchrones (ex: I/O, requête réseau) +// en utilisant un callback comme dernier argument. +// Convention : le callback est appelé avec (erreur, resultat). + +/** + * Simule la sélection d'un type de café. + * @param {string} nomCafe - le nom du café à sélectionner. + * @param {function(Error|null, string?)} callback - la fonction à appeler une fois l'opération terminée. + */ +function selectionnerCafe(nomCafe, callback) { + console.log(`1. Sélection du café : ${nomCafe}...`); + setTimeout(() => { + console.log(` -> Café "${nomCafe}" sélectionné.`); + // Le premier argument est pour une erreur potentielle (ici, null). + // Le second est le résultat. + callback(null, nomCafe); + }, TEMPS_SELECTION); +} + +/** + * Simule le broyage des grains de café. + * @param {string} nomCafe - le nom du café (reçu de l'étape précédente). + * @param {function(Error|null, string?)} callback - la fonction de rappel. + */ +function moudreGrains(nomCafe, callback) { + console.log(`2. Broyage des grains pour ${nomCafe}...`); + // TODO : implémenter la fonction moudreGrains +} + +/** + * Simule le chauffage de l'eau. + * @param {number} temperature - la température cible. + * @param {function(Error|null, string?)} callback - la fonction de rappel. + */ +function chaufferEau(temperature, callback) { + console.log(`3. Chauffage de l'eau à ${temperature}°C...`); + // TODO : implémenter la fonction chaufferEau +} + +/** + * Simule la préparation finale du café. + * @param {string} typeCafe - le type de café à préparer. + * @param {string} grains - l'ingrédient "grains-moulus". + * @param {string} eau - l'ingrédient "eau-chaude". + * @param {function(Error|null, string?)} callback - la fonction de rappel finale. + */ +function preparerCafe(typeCafe, grains, eau, callback) { + console.log(`4. Préparation du café ${typeCafe}...`); + // TODO : implémenter la fonction preparerCafe +} + + +// --- PARTIE 2 : le "Callback Hell" --- +// On imbrique les appels pour garantir l'ordre d'exécution. +// Cette structure en "pyramide" est difficile à lire, à débugger et à maintenir. +// C'est l'exemple parfait de ce que les Promises et async/await permettent d'éviter. + +console.log("\n--- Lancement de la préparation (avec Callback Hell) ---"); + +selectionnerCafe('Expresso', (errSelect, cafeSelectionne) => { + // Chaque étape doit vérifier s'il y a eu une erreur à l'étape précédente. + if (errSelect) { + console.error("Erreur de sélection :", errSelect); + return; // Arrête l'exécution en cas d'erreur. + } + + // TODO : appeler "moudreGrains" avec le café sélectionné, puis "chaufferEau", puis "preparerCafe". + +}); + +console.log("\nMessage affiché PENDANT que le café se prépare (non-bloquant)."); +