09

Patterns & Architecture JS

Observer, Factory, Singleton, State Machine, Immutabilité

← Accueil
Pourquoi les patterns avant les frameworks
React = Observer + Immutabilité. Vue = Proxy + Observer. Redux = Singleton + Observer. Apprendre les patterns, c'est comprendre CE QUE font les frameworks, pas juste HOW.

1 Observer / EventEmitter

Le pattern le plus utilisé en JS. Un objet notifie des "abonnés" quand il change. C'est exactement addEventListener — mais personnalisé.

class EventEmitter {
  #listeners = {};

  on(event, fn) {
    if (!this.#listeners[event]) this.#listeners[event] = [];
    this.#listeners[event].push(fn);
    return this; // chaînable
  }

  off(event, fn) {
    if (!this.#listeners[event]) return;
    this.#listeners[event] = this.#listeners[event].filter(l => l !== fn);
  }

  emit(event, ...args) {
    this.#listeners[event]?.forEach(fn => fn(...args));
  }
}

// Usage
const bus = new EventEmitter();

bus.on('user:login', user => console.log(`Bienvenue ${user.nom}`));
bus.on('user:login', user => localStorage.setItem('user', JSON.stringify(user)));

bus.emit('user:login', { nom: "Alice", role: "admin" });
// Bienvenue Alice  +  sauvegarde dans localStorage
React : useState notifie les composants qui re-rendent.
Vue : watch() observe les changements réactifs.

2 Module Pattern (encapsulation)

Créer un espace de noms avec des données privées — sans class.

const CartManager = (() => {
  // Privé — inaccessible de l'extérieur
  let articles = [];

  function calculerTotal() {
    return articles.reduce((acc, a) => acc + a.prix * a.qte, 0);
  }

  // Public — ce qu'on expose
  return {
    ajouter(article) {
      const existant = articles.find(a => a.id === article.id);
      if (existant) existant.qte++;
      else articles.push({ ...article, qte: 1 });
    },
    supprimer(id) {
      articles = articles.filter(a => a.id !== id);
    },
    get total() { return calculerTotal(); },
    get items() { return [...articles]; }, // copie — pas la ref interne
  };
})(); // IIFE — s'exécute immédiatement

CartManager.ajouter({ id: 1, nom: "Laptop", prix: 899 });
console.log(CartManager.total); // 899
// articles → inaccessible directement

3 Factory Pattern

Une fonction qui crée et retourne des objets — sans new.

function creerUtilisateur(nom, role = 'user') {
  if (!nom || nom.length < 2) throw new Error("Nom invalide");

  let _tentativesConnexion = 0;

  return {
    nom,
    role,
    connecter(mdp) {
      _tentativesConnexion++;
      if (_tentativesConnexion > 3) throw new Error("Compte bloqué");
      return mdp === "secret";
    },
    get tentatives() { return _tentativesConnexion; },
  };
}

const alice = creerUtilisateur("Alice", "admin");
alice.connecter("mauvais"); // false, tentatives=1
alice.connecter("secret");  // true,  tentatives=2
console.log(alice._tentativesConnexion); // undefined — privé !

4 Singleton

Une seule instance dans toute l'application.

class ConfigApp {
  static #instance = null;

  #config = {
    theme: 'dark',
    langue: 'fr',
    apiUrl: 'https://api.example.com',
  };

  static getInstance() {
    if (!ConfigApp.#instance) {
      ConfigApp.#instance = new ConfigApp();
    }
    return ConfigApp.#instance;
  }

  get(cle)      { return this.#config[cle]; }
  set(cle, val) { this.#config[cle] = val; }
  getAll()      { return { ...this.#config }; }
}

const config1 = ConfigApp.getInstance();
const config2 = ConfigApp.getInstance();
config1.set('theme', 'light');
console.log(config2.get('theme')); // "light" — même instance !
console.log(config1 === config2);  // true

5 State Machine (machine à états)

Gère les transitions d'état de façon explicite. Essentiel pour les formulaires, les loaders, etc.

const etats = {
  IDLE:       { label: 'Prêt',       suivants: ['CHARGEMENT'] },
  CHARGEMENT: { label: 'Chargement', suivants: ['SUCCES', 'ERREUR'] },
  SUCCES:     { label: 'Succès',     suivants: ['IDLE'] },
  ERREUR:     { label: 'Erreur',     suivants: ['IDLE', 'CHARGEMENT'] },
};

class StateMachine {
  #etat = 'IDLE';

  get etat() { return this.#etat; }

  transition(nouvelEtat) {
    const autorise = etats[this.#etat].suivants;
    if (!autorise.includes(nouvelEtat)) {
      throw new Error(`Transition ${this.#etat} → ${nouvelEtat} interdite`);
    }
    this.#etat = nouvelEtat;
    return this;
  }
}

const machine = new StateMachine();
machine.transition('CHARGEMENT'); // OK
machine.transition('SUCCES');     // OK
machine.transition('SUCCES');     // Error ! SUCCES → SUCCES non autorisé

6 Immutabilité

Principe fondamental de React et Redux : ne jamais muter l'état, toujours créer du nouveau.

// ❌ Mutation — React ne voit pas le changement
const etat = { user: null, items: [] };
etat.items.push("nouveau"); // modifie en place

// ✅ Immutable — crée un nouvel objet
const nouvelEtat = {
  ...etat,
  items: [...etat.items, "nouveau"],
};

// ✅ Mise à jour d'un item dans un tableau
const items = [{ id: 1, val: "a" }, { id: 2, val: "b" }];
const updated = items.map(item =>
  item.id === 1 ? { ...item, val: "z" } : item
);
// items non modifié, updated contient la nouvelle version

Résumé rapide

PatternQuand l'utiliser
ObserverNotifier plusieurs parties d'un changement
ModuleEncapsuler des données privées dans un namespace
FactoryCréer des objets avec logique de construction
SingletonUne seule config / store global
State MachineContrôler les transitions d'état
ImmutabilitéToujours avec React/Redux/Vue
🧩

Mini-Projet

Implémente Observer, Factory et State Machine dans une mini-application architecturée.

Exercices & Solutions

Télécharge les fichiers, ouvre-les dans ton éditeur et travaille directement dedans.

📝 exercices.js

4 exercices sur EventEmitter, store réactif, Factory avec validation et State Machine.

🟢 ×1 Facile 🟡 ×2 Moyen 🔴 ×1 Difficile
⬇ Télécharger
solutions.js

Corrections complètes. À consulter après avoir essayé.

Corrigé complet Best practices
⬇ Télécharger
🧠 Tester mes connaissances