# 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 ?