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
- Créer des fonctions asynchrones de base.
- Comprendre l'enchaînement d'opérations asynchrones.
- Mettre en évidence le problème du "Callback Hell" (la pyramide de l'enfer).
Instructions
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);
}
-
Configuration Créez des constantes pour définir les temps de simulation de chaque étape :
TEMPS_SELECTION: 500 msTEMPS_MOUTURE: 700 msTEMPS_CHAUFFAGE: 1000 msTEMPS_PREPARATION: 500 ms
-
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 desconsole.logpour 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 seranull.-
selectionnerCafe(nomCafe, callback)- Affiche
1. Sélection du café : [nomCafe]... - Après
TEMPS_SELECTION, affiche-> Café "[nomCafe]" sélectionné. - Appelle le
callbackavec(null, nomCafe).
- Affiche
-
moudreGrains(nomCafe, callback)- Affiche
2. Broyage des grains pour [nomCafe]... - Après
TEMPS_MOUTURE, affiche-> Grains moulus. - Appelle le
callbackavec(null, 'grains-moulus').
- Affiche
-
chaufferEau(temperature, callback)- Affiche
3. Chauffage de l'eau à [temperature]°C... - Après
TEMPS_CHAUFFAGE, affiche-> Eau chaude prête. - Appelle le
callbackavec(null, 'eau-chaude').
- Affiche
-
preparerCafe(typeCafe, grains, eau, callback)- Affiche
4. Préparation du café [typeCafe]... - Après
TEMPS_PREPARATION, affiche-> Café [typeCafe] préparé.. - Appelle le
callbackavec(null, 'café [typeCafe] préparé').
- Affiche
-
-
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
selectionnerCafedoit appelermoudreGrains, son callback doit appelerchaufferEau, 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.logtout à la fin de votre script pour montrer que le code principal continue de s'exécuter pendant la préparation du café.
- Pour garantir que les étapes se déroulent dans le bon ordre, vous devrez imbriquer les appels : le callback de
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
- Transformer des fonctions à base de callbacks en fonctions retournant des
Promises. - Utiliser
async/awaitpour orchestrer des tâches asynchrones de manière lisible. - Gérer les erreurs proprement avec des blocs
try...catch. - Optimiser les performances en exécutant des tâches indépendantes en parallèle avec
Promise.all.
Instructions
-
"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 unePromiseau lieu d'utiliser un callback.- Ajoutez une nouvelle fonction :
nettoyerTasse()qui prend600mset résout la promesse avec la valeur'tasse-propre'. - Dans
selectionnerCafe, ajoutez une gestion d'erreur : sicoffeeNameest nul ou vide, laPromisedoit être rejetée avec uneError("Aucun type de café n'a été spécifié.").
// 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 }; - Ajoutez une nouvelle fonction :
-
Scénario 1 : préparation séquentielle
Créez une fonctionasync function runSequentialPreparation().- À l'intérieur, utilisez
awaitpour préparer un café "Lungo" en suivant strictement cet ordre :- Sélectionner le café.
- Moudre les grains.
- Chauffer l'eau (à 95°C).
- Nettoyer la tasse.
- Préparer le café final.
- Encadrez votre logique dans un bloc
try...catchpour afficher un message d'erreur clair en cas d'échec. - Utilisez
console.time("Temps total")etconsole.timeEnd("Temps total")pour mesurer la durée totale de ce processus.
- À l'intérieur, utilisez
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]
-
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
awaitla 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,
awaitla préparation finale du café. - Utilisez également
try...catchetconsole.time()/console.timeEnd()pour la gestion des erreurs et la mesure du temps.
- Créez une nouvelle fonction
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]
- Point d'entrée
Créez une fonctionasync function main()qui appellerarunSequentialPreparation()puisrunOptimizedPreparation()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.allici ?