Découvrir Modèles d'Architecture en JavaScript
Introduction
Ce cours explore deux modèles d'architecture clés utilisés en JavaScript pour structurer des applications robustes et maintenables : le modèle MVC (Model-View-Controller) et le modèle Flux/Redux. Chaque modèle est décrit avec des exemples pratiques pour illustrer leur mise en œuvre.
1. Modèle MVC (Model-View-Controller)
Présentation du MVC


Le modèle MVC divise une application en trois composants interconnectés, permettant ainsi une séparation efficace des préoccupations :
- Model : Gère les données et la logique métier.
- View : Présente les données (le modèle) à l'utilisateur.
- Controller : Interprète les entrées de l'utilisateur, modifiant le modèle et la vue en conséquence.
Exemple Commenté
// Définition du modèle 'Book' avec deux propriétés : titre et auteur.
class Book {
constructor(title, author) {
this.title = title; // Titre du livre
this.author = author; // Auteur du livre
}
}
// Définition de la vue qui affiche les informations du livre.
class BookView {
constructor(model) {
this.model = model; // Stocke le modèle de livre.
this.controller = new BookController(this); // Crée un contrôleur pour cette vue.
this.init(); // Initialise l'affichage du livre.
}
init() {
// Affiche les détails du livre dans la console.
console.log(`Book: ${this.model.title} by ${this.model.author}`);
}
update(model) {
// Met à jour le modèle de la vue et rafraîchit l'affichage.
this.model = model;
this.init();
}
}
// Définition du contrôleur qui manipule la vue en fonction des interactions.
class BookController {
constructor(view) {
this.view = view; // Référence à la vue associée.
}
changeAuthor(author) {
// Change l'auteur dans le modèle et met à jour la vue.
this.view.model.author = author;
this.view.update(this.view.model);
}
}
// Création d'une instance de modèle de livre.
const book = new Book("1984", "George Orwell");
// Création d'une vue pour ce livre, qui initialise aussi un contrôleur.
const view = new BookView(book);
// Utilisation du contrôleur pour changer l'auteur et mettre à jour la vue.
view.controller.changeAuthor("G. Orwell");
Ce code montre la mise en œuvre du modèle MVC en JavaScript pour séparer clairement les responsabilités entre les données (modèle), l'affichage (vue) et les actions (contrôleur). Cette architecture favorise la maintenabilité et la testabilité de l'application.
Le code présenté illustre l'implémentation du modèle MVC (Model-View-Controller) en JavaScript, une architecture populaire pour structurer des applications robustes et maintenables. Le modèle MVC divise l'application en trois composantes principales :
-
Modèle (
Book) : Représente les données et la logique métier. Le modèleBookstocke les informations sur un livre, notamment le titre et l'auteur. -
Vue (
BookView) : Responsable de l'affichage des données (modèle) à l'utilisateur. La vue écoute les modifications du modèle et met à jour l'interface utilisateur en conséquence. Elle initialise également son affichage par la méthodeinit()et se met à jour viaupdate()après modifications. -
Contrôleur (
BookController) : Fait le lien entre l'utilisateur et le système. Il manipule le modèle et la vue en fonction des actions de l'utilisateur, comme le montre la méthodechangeAuthor()qui permet de modifier l'auteur du livre et de rafraîchir la vue.
Utilisation Pratique : Le MVC est idéal pour les applications avec une logique métier complexe, où la gestion des données, la représentation des données et les interactions utilisateur doivent être clairement séparées.
Pour structurer une application JavaScript suivant le modèle MVC (Model-View-Controller) de manière modulaire, voici une approche en plusieurs fichiers qui sépare clairement les responsabilités de chaque composant de l'application :
Architecture Proposée
-
Modèle (Model)
- Fichier :
Book.js- Contient la définition de la classe
Book, qui gère les données et la logique métier associées à un livre.
- Contient la définition de la classe
- Fichier :
-
Vue (View)
- Fichier :
BookView.js- Contient la définition de la classe
BookView, responsable de l'affichage des informations du livre à l'utilisateur. Cette classe se met à jour en réponse aux modifications du modèle.
- Contient la définition de la classe
- Fichier :
-
Contrôleur (Controller)
- Fichier :
BookController.js- Contient la définition de la classe
BookController, qui manipule les données du modèle et les mises à jour de la vue en réponse aux interactions de l'utilisateur.
- Contient la définition de la classe
- Fichier :
-
Point d'Entrée (Entry Point)
- Fichier :
app.js- Initialise les composants et les relie. Instancie le modèle, la vue, et le contrôleur. Gère les interactions de haut niveau et l'initialisation de l'application.
- Fichier :
Exemple de Contenu de Chaque Fichier
Book.js
export class Book {
constructor(title, author) {
this.title = title;
this.author = author;
}
}
BookView.js
export class BookView {
constructor(model) {
this.model = model;
this.controller = null;
}
init(controller) {
this.controller = controller;
console.log(`Book: ${this.model.title} by ${this.model.author}`);
}
update(model) {
this.model = model;
this.init(this.controller);
}
}
BookController.js
import { Book } from './Book.js';
import { BookView } from './BookView.js';
export class BookController {
constructor(model, view) {
this.model = model;
this.view = view;
this.view.init(this);
}
changeAuthor(newAuthor) {
this.model.author = newAuthor;
this.view.update(this.model);
}
}
app.js
import { Book } from './Book.js';
import { BookView } from './BookView.js';
import { BookController } from './BookController.js';
const book = new Book("1984", "George Orwell");
const view = new BookView(book);
const controller = new BookController(book, view);
controller.changeAuthor("G. Orwell");
Cette structure modulaire non seulement clarifie les responsabilités de chaque composant, mais facilite également la maintenance et le test de l'application. Chaque fichier peut être modifié, testé et réutilisé indépendamment, ce qui renforce la modularité et la réutilisabilité du code.
On peut faire encore mieux et c'est fortement conseillé
Architecture de Structuration du code
/my-mvc-app
|-- /src
| |-- /model
| | |-- Book.js # Modèle représentant l'entité "Book"
| |
| |-- /view
| | |-- BookView.js # Vue gérant l'affichage des livres
| |
| |-- /controller
| | |-- BookController.js # Contrôleur pour manipuler les livres
| |
| |-- app.js # Point d'entrée de l'application, initialise le MVC
|
|-- /node_modules # Dossiers pour les dépendances (si utilisé)
|
|-- package.json # Fichier de configuration npm (si utilisé)
|
|-- /public
| |-- index.html # Fichier HTML principal
| |-- /css
| | |-- style.css # Fichiers CSS pour l'esthétique
| |
| |-- /js
| |-- bundle.js # JavaScript compilé/bundled si nécessaire
|
|-- /test
|-- /model
| |-- Book.test.js # Tests pour le modèle Book
|
|-- /view
| |-- BookView.test.js # Tests pour la vue BookView
|
|-- /controller
|-- BookController.test.js # Tests pour le contrôleur BookController
Détails de la structure :
- src/ : Contient le code source de l'application, divisé en sous-dossiers pour les modèles, les vues, et les contrôleurs, reflétant l'architecture MVC.
- node_modules/ : Dossier pour les modules externes installés via npm, si vous utilisez Node.js.
- package.json : Fichier de configuration pour gérer les dépendances et scripts npm.
- public/ : Contient les fichiers statiques comme HTML, CSS, et le JavaScript compilé. Ceci est particulièrement utile pour séparer clairement le code du serveur du code client.
- test/ : Contient les tests pour chaque partie de l'architecture MVC, ce qui est essentiel pour assurer la fiabilité et la maintenabilité du code.
Cette structure permet de maintenir une séparation claire des préoccupations tout en rendant le projet extensible et facile à gérer.
Exercice
Créer la structure de répertoires dans un projet Visual Studio Code et faite fonctionner le code.
Il y aura peut être des modifications mineures à faire dans le code pour gérer le déplacement des fichiers dans les répertoires.
import { Book } from './Book.js';
import { BookView } from './BookView.js';
import { BookController } from './BookController.js';
A vous de jouer !
2. Modèle Flux/Redux
Présentation de Flux/Redux
Flux est une architecture qui favorise un flux de données unidirectionnel, utilisée principalement avec React. Redux en est une variante qui centralise l'état de l'application dans un store unique pour faciliter sa gestion.
Flux
Flux est une architecture de gestion de l'état pour les applications React qui utilise un flux de données unidirectionnel, où chaque donnée a un cycle de vie précis qui facilite le suivi des changements d'état. Le processus commence par les actions envoyées aux dispatchers, qui mettent à jour les stores où l'état est conservé. Les vues réagissent ensuite à ces changements d'état, créant un cycle cohérent et prévisible.
- dispatchers:
-
Dans le contexte des architectures Flux ou Redux utilisées principalement avec React, les dispatchers jouent un rôle clé. Ils agissent comme des centres de coordination pour toutes les actions qui affectent l'état de l'application. Lorsqu'une action est déclenchée, que ce soit par une interaction utilisateur ou par un autre processus, elle est envoyée au dispatcher. Le dispatcher prend ensuite cette action et la transmet au store approprié ou aux reducers dans le cas de Redux.
Dans Flux, chaque dispatcher gère le flux de données en s'assurant que les stores sont mis à jour de manière ordonnée. Cela est essentiel pour maintenir la cohérence de l'état à travers l'application. Redux simplifie ce concept en utilisant un seul store avec plusieurs reducers, réduisant le besoin d'avoir plusieurs dispatchers. Dans Redux, le dispatcher est plus intégré et moins distinct comme concept, car les actions sont directement dispatchées au store et traitées par les reducers qui gèrent les changements d'état.
-
- store:
-
Dans les architectures Flux et Redux utilisées pour la gestion d'état dans les applications JavaScript (notamment avec React), le store est le composant central qui détient l'état global de l'application. Il sert de dépôt unique pour tout l'état, rendant cet état accessible à n'importe quelle partie de l'application qui en a besoin.
-
Fonctionnalité du Store :
- Stockage de l'état : Le store conserve l'état de l'application dans une structure d'objet unique. Cela inclut les données utilisateur, les préférences, les caches, et d'autres informations d'état.
- Gestion des mises à jour : Le store reçoit des actions envoyées par les composants de l'application. Ces actions décrivent les changements d'état souhaités. En réponse à ces actions, le store exécute des fonctions réductrices (reducers) qui prennent l'état actuel et une action, et retournent un nouvel état modifié.
- Notification des changements : Lorsque l'état change, le store notifie les composants qui y sont abonnés. Ces notifications permettent aux composants de se mettre à jour en réponse aux changements d'état, assurant ainsi une interface utilisateur réactive et à jour.
-
Exemple avec Redux : Dans Redux, le store est créé par la fonction
createStore(reducer). Les reducers sont des fonctions pures qui déterminent comment l'état est mis à jour en réponse aux actions. Par exemple, un reducer peut gérer des actions de connexion utilisateur en mettant à jour l'état pour refléter les informations de l'utilisateur connecté.
Le store est donc un élément crucial pour la gestion d'état dans les applications complexes, facilitant la maintenance, le débogage, et la prévisibilité du comportement de l'application.
-
-
- changement d'état
- Dans le contexte des applications logicielles, notamment celles utilisant des architectures comme Flux ou Redux, les changements d'état se réfèrent à la modification de l'état global de l'application en réponse à des actions des utilisateurs ou du système. Ces changements sont gérés de manière centralisée dans un store, qui est l'unique source de vérité pour l'état de l'application. Lorsqu'une action est dispatchée (envoyée), elle est traitée par des reducers qui décident comment l'état doit être modifié. Les reducers produisent un nouvel état à partir de l'état précédent et de l'action reçue, garantissant que les modifications sont prévisibles et cohérentes. Cela permet une mise à jour systématique et contrôlée de l'interface utilisateur en fonction des changements d'état.
Exemple
Pour illustrer le fonctionnement du modèle d'architecture Flux, voici un exemple simple qui montre comment gérer l'état dans une application utilisant Flux avec des actions, un dispatcher, des stores et des vues.
Exemple Commenté de Flux
1. Actions
Les actions sont des paquets d'informations qui envoient des données du point d'application à votre store.
// Action pour ajouter un utilisateur
function addUserAction(user) {
return {
type: 'ADD_USER',
user
};
}
2. Dispatcher
Le dispatcher reçoit des actions et les diffuse à tous les stores inscrits.
// Création du dispatcher avec la bibliothèque Flux
const dispatcher = new Flux.Dispatcher();
// Fonction pour dispatcher une action
function dispatchAction(action) {
dispatcher.dispatch(action);
}
3. Store
Le store contient l'état de l'application, traite les actions reçues et met à jour l'état.
class UserStore extends EventEmitter {
constructor() {
super(); // Appelle le constructeur de la classe parente 'EventEmitter'
this.users = []; // Initialise un tableau pour stocker les objets utilisateur
this.dispatchToken = dispatcher.register(this.handleAction.bind(this)); // Enregistre la méthode 'handleAction' auprès du dispatcher, en liant 'this' à cette méthode
}
handleAction(action) {
switch (action.type) { // Vérifie le type d'action reçue
case 'ADD_USER': // Si le type d'action est 'ADD_USER'
this.addUser(action.user); // Appelle la méthode 'addUser' avec l'utilisateur fourni par l'action
this.emit('change'); // Émet un événement 'change' indiquant que le store a été mis à jour
break;
default:
// Gérer le cas par défaut
}
}
addUser(user) {
this.users.push(user); // Ajoute l'utilisateur au tableau 'users'
}
getUsers() {
return this.users; // Retourne le tableau des utilisateurs
}
}
const userStore = new UserStore(); // Crée une instance de UserStore
Ce code met en place un gestionnaire d'état pour une application, où UserStore gère une liste d'utilisateurs et réagit aux actions pour ajouter des utilisateurs et émettre des notifications de changement.
4. Vue
La vue écoute les changements dans le store et se met à jour en conséquence.
userStore.on('change', () => {
console.log(userStore.getUsers());
});
// Simuler l'ajout d'un utilisateur
dispatchAction(addUserAction({ name: 'John Doe', age: 30 }));
Utilisations Pratiques
Dans cet exemple, une action pour ajouter un utilisateur est créée et dispatchée à travers un dispatcher central. Le store inscrit écoute ce dispatcher, traite l'action reçue, et met à jour son état interne (ici, la liste des utilisateurs). Enfin, une vue (dans ce cas, un simple log dans la console) est mise à jour chaque fois que le store émet un événement de changement.
Cet exemple montre comment les différentes parties de l'architecture Flux travaillent ensemble pour créer un flux de données unidirectionnel, facilitant la gestion de l'état dans des applications complexes.
L'implémentation de la classe UserStore dans une architecture Flux peut être utilisée dans diverses applications pratiques pour améliorer la gestion de l'état et la réactivité des interfaces utilisateur. Voici quelques utilisations pratiques :
-
Applications de réseaux sociaux : Gérer les listes d'utilisateurs, comme les abonnés ou les amis, permettant de mettre à jour l'interface utilisateur en temps réel lorsque de nouveaux utilisateurs sont ajoutés ou supprimés.
-
Systèmes de gestion de contenu (CMS) : Suivre les modifications apportées par plusieurs utilisateurs dans un environnement collaboratif et mettre à jour les vues pour tous les utilisateurs en temps réel.
-
Applications de commerce électronique : Gérer les comptes utilisateurs, y compris les informations de profil et les préférences, assurant une expérience utilisateur cohérente et personnalisée à travers différentes sessions et appareils.
-
Jeux en ligne : Suivre les joueurs et leurs statistiques pour mettre à jour les classements et les profils de manière dynamique en fonction des actions des joueurs.
Ces utilisations démontrent la flexibilité et l'efficacité de UserStore dans la gestion de l'état d'applications complexes, facilitant ainsi une programmation plus structurée et maintenable.
Redux
Redux est une variante de Flux qui simplifie encore plus la gestion de l'état en utilisant un seul store centralisé pour tout l'état de l'application, ce qui rend l'état plus prévisible et plus facile à gérer. Dans Redux, tout changement d'état passe par un reducer, une fonction qui reçoit l'état actuel et une action, et retourne un nouvel état. Cela centralise la logique de modification de l'état et offre une grande facilité pour opérer avec des états immuables, simplifiant le débogage et les tests.
Exemple Commenté
import { createStore } from 'redux';
// Reducer
function bookReducer(state = { title: "1984", author: "George Orwell" }, action) {
switch (action.type) {
case 'CHANGE_AUTHOR':
return { ...state, author: action.payload };
default:
return state;
}
}
// Store
const store = createStore(bookReducer);
// Dispatching an action
store.dispatch({ type: 'CHANGE_AUTHOR', payload: 'G. Orwell' });
// Subscribe to changes
store.subscribe(() => console.log(store.getState()));
// Getting initial state
console.log(store.getState());
Utilisation Pratique : Redux est extrêmement utile dans les applications où l'état global doit être partagé entre plusieurs composants ou où l'état est complexe et nécessite une gestion fine.
Pour illustrer l'utilisation de Redux avec l'exemple du livre, nous allons créer une application simple qui permet de gérer les informations d'un livre. Voici comment les éléments clés de Redux peuvent être mis en œuvre :
1. Actions Redux
Définissons une action pour changer l'auteur d'un livre.
// Action pour changer l'auteur
const changeAuthor = (author) => {
return {
type: 'CHANGE_AUTHOR',
payload: author
};
};
2. Reducer Redux
Le reducer gère les changements d'état du livre en fonction des actions reçues.
const initialState = {
title: "1984",
author: "George Orwell"
};
const bookReducer = (state = initialState, action) => {
switch (action.type) {
case 'CHANGE_AUTHOR':
return {
...state,
author: action.payload
};
default:
return state;
}
};
3. Store Redux
Le store Redux est créé à partir du reducer et sert de lieu central pour gérer l'état de l'application.
import { createStore } from 'redux';
const store = createStore(bookReducer);
// Souscrire au store pour loguer les changements d'état
store.subscribe(() => console.log(store.getState()));
// Dispatch de l'action pour changer l'auteur
store.dispatch(changeAuthor("G. Orwell"));
Explication
Dans cet exemple, le store de Redux contient l'état initial du livre, qui comprend un titre et un auteur. L'action changeAuthor est utilisée pour créer une demande de changement de l'auteur du livre. Le reducer écoute cette action et met à jour l'état en conséquence. Enfin, le store centralise la gestion de cet état et permet des mises à jour prévisibles et faciles à suivre grâce aux subscriptions et dispatchs.
Conclusion
La compréhension de ces modèles d'architecture est essentielle pour développer des applications front-end efficaces et bien organisées. Chaque modèle a ses forces et convient à différents types de projets, offrant des outils puissants pour les développeurs front-end.
Flux vs. Redux : Comparaison, avantages et inconvénients
Flux :
- Architecture : Utilise plusieurs stores et un dispatcher central pour gérer le flux de données.
- Avantages : Flexibilité dans la structuration des stores ; convivial pour les grandes applications avec de nombreux types de données dynamiques.
- Inconvénients : Peut devenir complexe avec plusieurs stores ; gestion des dépendances entre stores peut être difficile.
Redux :
- Architecture : Utilise un unique store centralisé et des reducers pour gérer les modifications d'état.
- Avantages : Simplifie le flux de données avec un store unique, facilitant le débogage et le test ; très intégré avec React via des bindings comme
react-redux. - Inconvénients : Peut imposer une certaine rigidité, surtout dans les petites applications ou celles qui n'ont pas besoin d'un store unique ; nécessite la compréhension des immutabilités.
Différences Clés :
- Flux permet une plus grande séparation et modularité grâce à ses multiples stores, tandis que Redux centralise l'état, rendant les flux de données plus prévisibles mais potentiellement plus restrictifs dans la gestion des états locaux.
Ces différences rendent Flux plus adapté aux applications très segmentées et Redux idéal pour les applications où un état global unifié est crucial.
L'immutabilité
L'immutabilité en programmation, notamment en JavaScript avec des bibliothèques comme Redux, se réfère à la pratique de ne jamais modifier directement l'état ou les objets. Au lieu de cela, chaque fois qu'une modification est nécessaire, une copie est créée et modifiée, puis retournée en tant que nouvel objet ou état.
Cette approche présente plusieurs avantages :
- Prévisibilité : Les changements d'état sont plus faciles à suivre et à prévoir car chaque état est un snapshot immuable.
- Sécurité des threads : Importante dans les environnements multi-threadés (même si moins directement applicable en JavaScript traditionnel), l'immutabilité évite les conflits de modification d'état.
- Historique des États : Facilite l'implémentation de fonctionnalités comme l'annulation, la répétition des actions ou le débogage, puisque les anciens états sont préservés intacts.
L'inconvénient majeur est la surcharge de performance et de mémoire due à la création répétée d'objets, bien que des bibliothèques comme Immutable.js ou des techniques spécifiques à Redux puissent aider à minimiser ces impacts.