0%
JavaScript Moderne : Programmation Orientée Objet

POO

Classes et Objets Modernes

10-15 min

JavaScript Moderne : Programmation Orientée Objet

JavaScript est un langage flexible qui supporte plusieurs paradigmes de programmation, dont la programmation orientée objet (POO). Depuis ES6, JavaScript a introduit une syntaxe de classe plus intuitive, même si le modèle sous-jacent reste basé sur les prototypes. Dans ce tutoriel, nous explorerons la POO en JavaScript moderne, y compris les classes, l’héritage, les mixins et les patterns de conception couramment utilisés.

Le modèle objet en JavaScript

Avant d’explorer la syntaxe moderne des classes, il est important de comprendre comment fonctionne le modèle objet en JavaScript.

Objets et prototypes

En JavaScript, presque tout est un objet, et l’héritage fonctionne via des prototypes :

// Création d'un objet littéral
const personne = {
    nom: "Alice",
    saluer() {
        return `Bonjour, je suis ${this.nom}`;
    }
};

console.log(personne.saluer()); // "Bonjour, je suis Alice"

// Création d'un nouvel objet basé sur le prototype de personne
const etudiant = Object.create(personne);
etudiant.nom = "Bob";
etudiant.niveau = "Master";

console.log(etudiant.saluer()); // "Bonjour, je suis Bob"
console.log(etudiant.niveau); // "Master"

Fonctions constructeur (ancienne méthode)

Avant ES6, la création d’objets similaires se faisait généralement à l’aide de fonctions constructeur :

// Fonction constructeur
function Personne(nom, age) {
    this.nom = nom;
    this.age = age;
}

// Méthode ajoutée au prototype
Personne.prototype.saluer = function() {
    return `Bonjour, je suis ${this.nom} et j'ai ${this.age} ans`;
};

// Création d'instances
const alice = new Personne("Alice", 30);
const bob = new Personne("Bob", 25);

console.log(alice.saluer()); // "Bonjour, je suis Alice et j'ai 30 ans"
console.log(bob.saluer()); // "Bonjour, je suis Bob et j'ai 25 ans"

Classes en JavaScript moderne (ES6+)

ES6 a introduit une syntaxe de classe plus intuitive qui simplifie la création d’objets et l’implémentation de l’héritage.

Définition de classe

class Personne {
    // Constructeur
    constructor(nom, age) {
        this.nom = nom;
        this.age = age;
    }
    
    // Méthodes
    saluer() {
        return `Bonjour, je suis ${this.nom} et j'ai ${this.age} ans`;
    }
    
    decrire() {
        return `${this.nom} est une personne de ${this.age} ans`;
    }
}

// Création d'instances
const alice = new Personne("Alice", 30);
console.log(alice.saluer()); // "Bonjour, je suis Alice et j'ai 30 ans"

Propriétés et méthodes statiques

Les méthodes et propriétés statiques appartiennent à la classe elle-même, pas aux instances :

class MathUtils {
    // Propriété statique (ES2022+)
    static PI = 3.14159;
    
    // Méthode statique
    static carre(x) {
        return x * x;
    }
    
    static aire_cercle(rayon) {
        return MathUtils.PI * MathUtils.carre(rayon);
    }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.carre(4)); // 16
console.log(MathUtils.aire_cercle(2)); // 12.56636

// Les propriétés et méthodes statiques ne sont pas accessibles sur les instances
const utils = new MathUtils();
// console.log(utils.PI); // undefined

Accesseurs (getters et setters)

Les accesseurs permettent de contrôler l’accès aux propriétés d’une classe :

class Compte {
    constructor(proprietaire) {
        this.proprietaire = proprietaire;
        this._solde = 0; // Convention : _ indique une propriété "privée"
    }
    
    // Getter
    get solde() {
        return `${this._solde} €`;
    }
    
    // Setter
    set solde(valeur) {
        if (isNaN(valeur)) {
            throw new Error("Le solde doit être un nombre");
        }
        this._solde = valeur;
    }
    
    // Méthodes normales
    deposer(montant) {
        this.solde = this._solde + montant;
        return this._solde;
    }
    
    retirer(montant) {
        if (montant > this._solde) {
            throw new Error("Fonds insuffisants");
        }
        this.solde = this._solde - montant;
        return this._solde;
    }
}

const compte = new Compte("Alice");
compte.deposer(1000);
console.log(compte.solde); // "1000 €"

compte.retirer(500);
console.log(compte.solde); // "500 €"

// compte.solde = "beaucoup"; // Erreur: Le solde doit être un nombre

Champs de classe (Class Fields)

Les champs de classe, introduits dans les versions plus récentes de JavaScript, permettent de déclarer des propriétés directement dans la classe :

class Produit {
    // Champs publics
    nom;
    prix;
    
    // Champ privé (ECMAScript 2022+)
    #stock = 0;
    
    // Champ statique
    static nbProduits = 0;
    
    constructor(nom, prix, stock) {
        this.nom = nom;
        this.prix = prix;
        this.#stock = stock;
        Produit.nbProduits++;
    }
    
    get stock() {
        return this.#stock;
    }
    
    set stock(valeur) {
        if (valeur < 0) {
            throw new Error("Le stock ne peut pas être négatif");
        }
        this.#stock = valeur;
    }
    
    vendre(quantite) {
        if (quantite > this.#stock) {
            throw new Error("Stock insuffisant");
        }
        this.#stock -= quantite;
        return `${quantite} unité(s) de ${this.nom} vendue(s)`;
    }
    
    // Méthode privée (ECMAScript 2022+)
    #miseAJourInterne() {
        console.log("Mise à jour interne");
    }
}

const p1 = new Produit("Téléphone", 499, 10);
console.log(p1.nom); // "Téléphone"
console.log(p1.stock); // 10

p1.stock = 15;
console.log(p1.stock); // 15

console.log(p1.vendre(3)); // "3 unité(s) de Téléphone vendue(s)"
console.log(p1.stock); // 12

// console.log(p1.#stock); // Erreur: propriété privée
// p1.#miseAJourInterne(); // Erreur: méthode privée

console.log(Produit.nbProduits); // 1

Héritage et polymorphisme

L’héritage permet à une classe d’hériter des propriétés et méthodes d’une autre classe.

Héritage avec extends

class Animal {
    constructor(nom) {
        this.nom = nom;
    }
    
    parler() {
        return `${this.nom} fait du bruit`;
    }
}

class Chien extends Animal {
    constructor(nom, race) {
        super(nom); // Appel du constructeur parent
        this.race = race;
    }
    
    // Surcharge de la méthode parler
    parler() {
        return `${this.nom} aboie`;
    }
    
    decrire() {
        return `${this.nom} est un chien de race ${this.race}`;
    }
}

const animal = new Animal("Animal inconnu");
console.log(animal.parler()); // "Animal inconnu fait du bruit"

const rex = new Chien("Rex", "Berger Allemand");
console.log(rex.parler()); // "Rex aboie"
console.log(rex.decrire()); // "Rex est un chien de race Berger Allemand"

La chaîne de prototypes

Même avec la syntaxe moderne des classes, JavaScript utilise toujours des prototypes en coulisses :

const rex = new Chien("Rex", "Berger Allemand");

console.log(rex instanceof Chien); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

console.log(Object.getPrototypeOf(rex) === Chien.prototype); // true
console.log(Object.getPrototypeOf(Chien.prototype) === Animal.prototype); // true

Contrôle de type

Pour vérifier si un objet est une instance d’une classe, utilisez l’opérateur instanceof :

function traiterAnimal(animal) {
    if (animal instanceof Chien) {
        console.log(animal.parler()); // Méthode de Chien
    } else if (animal instanceof Animal) {
        console.log(animal.parler()); // Méthode d'Animal
    } else {
        console.log("Ceci n'est pas un animal");
    }
}

traiterAnimal(new Chien("Rex", "Berger Allemand")); // "Rex aboie"
traiterAnimal(new Animal("Lion")); // "Lion fait du bruit"
traiterAnimal({}); // "Ceci n'est pas un animal"

Composition et mixins

Parfois, l’héritage seul n’est pas suffisant pour modéliser des relations complexes. La composition et les mixins offrent des alternatives flexibles.

Composition

La composition consiste à combiner des objets plus simples pour créer des objets plus complexes :

// Objets de capacités
const peutNager = {
    nager() {
        return `${this.nom} nage`;
    }
};

const peutVoler = {
    voler() {
        return `${this.nom} vole`;
    }
};

// Classe de base
class Animal {
    constructor(nom) {
        this.nom = nom;
    }
    
    manger() {
        return `${this.nom} mange`;
    }
}

// Composition par mixin
class Canard extends Animal {
    constructor(nom) {
        super(nom);
        // Composition: ajouter des capacités
        Object.assign(this, peutNager, peutVoler);
    }
}

class Poisson extends Animal {
    constructor(nom) {
        super(nom);
        // Composition: seulement nager
        Object.assign(this, peutNager);
    }
}

const donald = new Canard("Donald");
console.log(donald.manger()); // "Donald mange"
console.log(donald.nager()); // "Donald nage"
console.log(donald.voler()); // "Donald vole"

const nemo = new Poisson("Nemo");
console.log(nemo.nager()); // "Nemo nage"
// console.log(nemo.voler()); // Erreur: nemo.voler n'est pas une fonction

Mixins fonctionnels

Les mixins fonctionnels sont des fonctions qui ajoutent des propriétés et des méthodes à un objet existant :

// Mixin fonctionnel
function avecTimestamp(Base) {
    return class extends Base {
        constructor(...args) {
            super(...args);
            this.createdAt = new Date();
        }
        
        getAge() {
            return new Date() - this.createdAt;
        }
    };
}

function avecLogging(Base) {
    return class extends Base {
        log(message) {
            console.log(`[${this.constructor.name}] ${message}`);
        }
    };
}

// Application des mixins
class Utilisateur {
    constructor(nom) {
        this.nom = nom;
    }
}

// Combinaison de mixins
const UtilisateurAvecExtensions = avecLogging(avecTimestamp(Utilisateur));

const user = new UtilisateurAvecExtensions("Alice");
console.log(user.nom); // "Alice"
console.log(user.createdAt); // Date de création
user.log("Instance créée"); // "[UtilisateurAvecExtensions] Instance créée"

Patterns de conception en JavaScript

Les patterns de conception sont des solutions réutilisables à des problèmes récurrents. Voici quelques patterns courants en JavaScript.

Singleton

Le pattern Singleton garantit qu’une classe n’a qu’une seule instance et fournit un point d’accès global à cette instance :

class Configuration {
    constructor() {
        if (Configuration._instance) {
            return Configuration._instance;
        }
        
        this.theme = "clair";
        this.langue = "fr";
        this.notifications = true;
        
        Configuration._instance = this;
    }
    
    static getInstance() {
        return new Configuration();
    }
}

const config1 = Configuration.getInstance();
const config2 = Configuration.getInstance();

console.log(config1 === config2); // true

config1.theme = "sombre";
console.log(config2.theme); // "sombre" (même instance)

Factory (Fabrique)

Le pattern Factory crée des objets sans exposer la logique de création :

// Classes concrètes
class VoitureEconomique {
    constructor(options) {
        this.portes = options.portes || 4;
        this.couleur = options.couleur || "blanche";
        this.consommation = "faible";
    }
}

class VoitureLuxe {
    constructor(options) {
        this.portes = options.portes || 2;
        this.couleur = options.couleur || "noire";
        this.consommation = "élevée";
        this.options = ["cuir", "GPS", "toit ouvrant"];
    }
}

class VoitureSport {
    constructor(options) {
        this.portes = 2;
        this.couleur = options.couleur || "rouge";
        this.consommation = "très élevée";
        this.vitesseMax = 300;
    }
}

// Factory
class VoitureFactory {
    createVoiture(type, options) {
        switch(type) {
            case "economique":
                return new VoitureEconomique(options);
            case "luxe":
                return new VoitureLuxe(options);
            case "sport":
                return new VoitureSport(options);
            default:
                throw new Error(`Type de voiture non reconnu: ${type}`);
        }
    }
}

// Utilisation
const factory = new VoitureFactory();
const maVoiture = factory.createVoiture("luxe", { couleur: "bleu", portes: 4 });
console.log(maVoiture); // VoitureLuxe avec options personnalisées

Observer (Observateur)

Le pattern Observer permet à un objet de notifier d’autres objets des changements d’état :

class Sujet {
    constructor() {
        this.observateurs = [];
    }
    
    souscrire(observateur) {
        this.observateurs.push(observateur);
    }
    
    desinscrire(observateur) {
        this.observateurs = this.observateurs.filter(obs => obs !== observateur);
    }
    
    notifier(donnees) {
        this.observateurs.forEach(observateur => observateur.update(donnees));
    }
}

class Observable extends Sujet {
    constructor() {
        super();
        this.etat = null;
    }
    
    getEtat() {
        return this.etat;
    }
    
    setEtat(etat) {
        this.etat = etat;
        this.notifier(this);
    }
}

// Exemple d'utilisation: système de notification météo
class StationMeteo extends Observable {
    setMesures(temperature, humidite, pression) {
        this.etat = { temperature, humidite, pression, date: new Date() };
        this.notifier(this);
    }
}

// Observateurs
class AffichageConditionsActuelles {
    update(station) {
        const { temperature, humidite } = station.getEtat();
        console.log(`Conditions actuelles: ${temperature}°C et ${humidite}% d'humidité`);
    }
}

class AffichageStatistiques {
    constructor() {
        this.temperatures = [];
    }
    
    update(station) {
        const { temperature } = station.getEtat();
        this.temperatures.push(temperature);
        
        const max = Math.max(...this.temperatures);
        const min = Math.min(...this.temperatures);
        const moy = this.temperatures.reduce((acc, val) => acc + val, 0) / this.temperatures.length;
        
        console.log(`Statistiques - Max: ${max}°C, Min: ${min}°C, Moyenne: ${moy.toFixed(1)}°C`);
    }
}

// Utilisation
const station = new StationMeteo();
const affichageConditions = new AffichageConditionsActuelles();
const affichageStats = new AffichageStatistiques();

station.souscrire(affichageConditions);
station.souscrire(affichageStats);

// Simule des changements météo
station.setMesures(20, 65, 1013);
station.setMesures(22, 70, 1014);
station.setMesures(19, 75, 1012);

Module

Le pattern Module encapsule des fonctionnalités et expose une API publique tout en gardant certains détails privés :

// Module avec IIFE (ancien style)
const Compteur = (function() {
    // Variables privées
    let compte = 0;
    
    // Méthodes privées
    function valider(valeur) {
        return typeof valeur === 'number' && !isNaN(valeur);
    }
    
    // API publique
    return {
        incrementer() {
            return ++compte;
        },
        decrementer() {
            return --compte;
        },
        ajouter(valeur) {
            if (!valider(valeur)) {
                throw new Error("Valeur invalide");
            }
            compte += valeur;
            return compte;
        },
        getCompte() {
            return compte;
        }
    };
})();

console.log(Compteur.getCompte()); // 0
Compteur.incrementer();
Compteur.incrementer();
console.log(Compteur.getCompte()); // 2
Compteur.ajouter(5);
console.log(Compteur.getCompte()); // 7
// console.log(Compteur.compte); // undefined (privé)

Décorateur

Le pattern Décorateur permet d’ajouter des comportements à des objets individuels sans affecter le comportement d’autres objets de la même classe :

// Composant de base
class Cafe {
    cout() {
        return 2.5;
    }
    
    description() {
        return "Café simple";
    }
}

// Décorateurs
class LaitDecorator {
    constructor(cafe) {
        this.cafe = cafe;
    }
    
    cout() {
        return this.cafe.cout() + 0.5;
    }
    
    description() {
        return `${this.cafe.description()} avec du lait`;
    }
}

class SucreDecorator {
    constructor(cafe) {
        this.cafe = cafe;
    }
    
    cout() {
        return this.cafe.cout() + 0.2;
    }
    
    description() {
        return `${this.cafe.description()} avec du sucre`;
    }
}

class CannelleDecorator {
    constructor(cafe) {
        this.cafe = cafe;
    }
    
    cout() {
        return this.cafe.cout() + 0.3;
    }
    
    description() {
        return `${this.cafe.description()} avec de la cannelle`;
    }
}

// Utilisation
let monCafe = new Cafe();
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple : 2.5€"

// Ajout de lait
monCafe = new LaitDecorator(monCafe);
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple avec du lait : 3€"

// Ajout de sucre
monCafe = new SucreDecorator(monCafe);
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple avec du lait avec du sucre : 3.2€"

// Création d'un café personnalisé directement
const cafeComplet = new CannelleDecorator(new SucreDecorator(new LaitDecorator(new Cafe())));
console.log(`${cafeComplet.description()} : ${cafeComplet.cout()}€`);
// "Café simple avec du lait avec du sucre avec de la cannelle : 3.5€"

Gestion avancée des objets

JavaScript offre plusieurs façons avancées de manipuler et contrôler les objets.

Object.defineProperty

Object.defineProperty permet de définir des propriétés avec un contrôle précis sur leur comportement :

const personne = {};

Object.defineProperty(personne, 'nom', {
    value: 'Alice',
    writable: true,      // peut être modifié
    enumerable: true,    // apparaît dans les boucles for...in
    configurable: true   // peut être supprimé
});

Object.defineProperty(personne, 'age', {
    value: 30,
    writable: false      // ne peut pas être modifié
});

console.log(personne.nom); // "Alice"
personne.nom = "Bob";
console.log(personne.nom); // "Bob"

console.log(personne.age); // 30
personne.age = 40;
console.log(personne.age); // 30 (non modifié car writable: false)

Proxy

Les Proxys permettent de personnaliser le comportement des opérations fondamentales sur les objets :

const cible = {
    message: "Hello",
    prix: 100
};

const handler = {
    // Interception de l'accès aux propriétés
    get(cible, prop, receiver) {
        console.log(`Accès à la propriété: ${prop}`);
        
        if (prop === 'prix') {
            return `${cible[prop]}€`;
        }
        
        return cible[prop];
    },
    
    // Interception de la modification de propriétés
    set(cible, prop, valeur, receiver) {
        console.log(`Modification de ${prop}: ${valeur}`);
        
        if (prop === 'prix' && valeur < 0) {
            throw new Error("Le prix ne peut pas être négatif");
        }
        
        cible[prop] = valeur;
        return true; // succès
    }
};

const proxy = new Proxy(cible, handler);

console.log(proxy.message); // Log: "Accès à la propriété: message" puis "Hello"
console.log(proxy.prix); // Log: "Accès à la propriété: prix" puis "100€"

proxy.message = "Bonjour"; // Log: "Modification de message: Bonjour"
// proxy.prix = -50; // Erreur: Le prix ne peut pas être négatif

Symboles et propriétés Well-Known

Les symboles peuvent être utilisés comme clés de propriétés uniques et non-énumérables :

// Création d'un symbole
const idSymbol = Symbol("id");

const utilisateur = {
    nom: "Alice",
    [idSymbol]: 12345  // propriété basée sur un symbole
};

console.log(utilisateur.nom); // "Alice"
console.log(utilisateur[idSymbol]); // 12345

// Les propriétés basées sur des symboles ne sont pas énumérables par défaut
for (let key in utilisateur) {
    console.log(key); // Affiche seulement "nom", pas le symbole
}

// Symboles well-known pour personnaliser le comportement
const collectionPersonnalisee = {
    elements: [1, 2, 3, 4, 5],
    
    // Personnalisation de l'itération
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.elements.length) {
                    return { value: this.elements[index++] * 2, done: false };
                }
                return { done: true };
            }
        };
    }
};

// Utilisation avec for...of qui utilise l'itérateur
for (const item of collectionPersonnalisee) {
    console.log(item); // 2, 4, 6, 8, 10
}

Exercices pratiques

Pour solidifier vos connaissances, essayez ces exercices :

  1. Système de formes : Créez une hiérarchie de classes avec une classe Forme comme base, et des sous-classes comme Cercle, Rectangle et Triangle, chacune avec ses propres méthodes pour calculer l’aire et le périmètre.

  2. Gestion de bibliothèque : Implémentez un système de gestion de bibliothèque avec des classes pour Livre, Auteur, Bibliothèque et Emprunteur. Utilisez l’encapsulation pour protéger les données et les mixins pour ajouter des fonctionnalités comme le suivi des emprunts.

  3. Mini framework UI : Créez un petit framework UI avec une classe Component comme base et des composants spécifiques comme Button, Input et Panel qui héritent de cette base. Utilisez des méthodes comme render() et update().

  4. Implémentation de patterns : Implémentez le pattern Singleton pour un service de journalisation et le pattern Factory pour créer différents types de notifications (email, SMS, push).

  5. API de validation : Créez une API de validation avec des décorateurs pour valider les propriétés d’un objet (par exemple, longueur minimale d’une chaîne, plage de valeurs pour un nombre).

Exemple complet : Système de gestion de produits

Voici un exemple qui combine plusieurs concepts abordés dans ce tutoriel :

// Classe de base
class Produit {
    #id;
    #nom;
    #prix;
    #stock;
    static #compteur = 0;
    
    constructor(nom, prix, stock = 0) {
        this.#id = ++Produit.#compteur;
        this.#nom = nom;
        this.#prix = prix;
        this.#stock = stock;
    }
    
    // Getters/Setters
    get id() { return this.#id; }
    
    get nom() { return this.#nom; }
    set nom(value) { this.#nom = value; }
    
    get prix() { return this.#prix; }
    set prix(value) {
        if (value < 0) throw new Error("Le prix ne peut pas être négatif");
        this.#prix = value;
    }
    
    get stock() { return this.#stock; }
    set stock(value) {
        if (value < 0) throw new Error("Le stock ne peut pas être négatif");
        this.#stock = value;
    }
    
    // Méthodes
    ajouter_stock(quantite) {
        this.stock += quantite;
        return this.stock;
    }
    
    retirer_stock(quantite) {
        if (quantite > this.stock) throw new Error("Stock insuffisant");
        this.stock -= quantite;
        return this.stock;
    }
    
    toString() {
        return `Produit #${this.id}: ${this.nom} - Prix: ${this.prix}€ - Stock: ${this.stock}`;
    }
}

// Sous-classe spécialisée
class ProduitPerissable extends Produit {
    #dateExpiration;
    
    constructor(nom, prix, stock, dateExpiration) {
        super(nom, prix, stock);
        this.#dateExpiration = new Date(dateExpiration);
    }
    
    get dateExpiration() { return this.#dateExpiration; }
    
    estPerime() {
        return new Date() > this.#dateExpiration;
    }
    
    toString() {
        return `${super.toString()} - Expire le: ${this.#dateExpiration.toLocaleDateString()}`;
    }
}

// Mixin pour le suivi des modifications
const avecHistorique = Base => class extends Base {
    #historique = [];
    
    constructor(...args) {
        super(...args);
    }
    
    ajouterEvenement(type, details) {
        const evenement = {
            type,
            details,
            date: new Date()
        };
        this.#historique.push(evenement);
        return evenement;
    }
    
    get historique() {
        return [...this.#historique];
    }
};

// Application du mixin
class ProduitAvecHistorique extends avecHistorique(Produit) {
    constructor(...args) {
        super(...args);
        this.ajouterEvenement("creation", { prix: args[1], stock: args[2] || 0 });
    }
    
    // Surcharge pour ajouter des événements
    ajouter_stock(quantite) {
        const stockAvant = this.stock;
        const nouveauStock = super.ajouter_stock(quantite);
        this.ajouterEvenement("ajout_stock", { 
            avant: stockAvant,
            ajout: quantite,
            apres: nouveauStock
        });
        return nouveauStock;
    }
    
    retirer_stock(quantite) {
        const stockAvant = this.stock;
        const nouveauStock = super.retirer_stock(quantite);
        this.ajouterEvenement("retrait_stock", { 
            avant: stockAvant,
            retrait: quantite,
            apres: nouveauStock
        });
        return nouveauStock;
    }
    
    set prix(value) {
        const ancienPrix = this.prix;
        super.prix = value;
        this.ajouterEvenement("changement_prix", { 
            avant: ancienPrix,
            apres: value
        });
    }
}

// Factory pour créer différents types de produits
class ProduitFactory {
    creerProduit(type, ...args) {
        switch(type) {
            case "standard":
                return new ProduitAvecHistorique(...args);
            case "perissable":
                return new ProduitPerissable(...args);
            default:
                throw new Error(`Type de produit inconnu: ${type}`);
        }
    }
}

// Gestionnaire de catalogue (Singleton)
class CatalogueGestionnaire {
    #produits = new Map();
    static #instance;
    
    constructor() {
        if (CatalogueGestionnaire.#instance) {
            return CatalogueGestionnaire.#instance;
        }
        CatalogueGestionnaire.#instance = this;
    }
    
    ajouterProduit(produit) {
        this.#produits.set(produit.id, produit);
        return produit;
    }
    
    getProduit(id) {
        return this.#produits.get(id);
    }
    
    listerProduits() {
        return [...this.#produits.values()];
    }
    
    listerProduitsEnStock() {
        return this.listerProduits().filter(p => p.stock > 0);
    }
}

// Démonstration
function demoProduits() {
    // Création de produits avec Factory
    const factory = new ProduitFactory();
    const p1 = factory.creerProduit("standard", "Clavier", 49.99, 10);
    const p2 = factory.creerProduit("standard", "Souris", 29.99, 15);
    const p3 = factory.creerProduit("perissable", "Yaourt", 2.99, 50, "2023-12-31");
    
    // Utilisation du singleton
    const catalogue = new CatalogueGestionnaire();
    catalogue.ajouterProduit(p1);
    catalogue.ajouterProduit(p2);
    catalogue.ajouterProduit(p3);
    
    // Opérations
    console.log("Produits en catalogue:");
    catalogue.listerProduits().forEach(p => console.log(` - ${p}`));
    
    p1.ajouter_stock(5);
    p2.retirer_stock(3);
    p1.prix = 45.99;
    
    console.log("\nHistorique du clavier:");
    p1.historique.forEach((e, i) => {
        console.log(`${i+1}. [${e.date.toLocaleTimeString()}] ${e.type}: `, e.details);
    });
    
    console.log("\nTest d'un produit périssable:");
    console.log(p3.toString());
    console.log(`Est périmé: ${p3.estPerime()}`);
}

// Exécution
demoProduits();

Conclusion

Dans ce tutoriel, nous avons exploré la programmation orientée objet en JavaScript moderne :

  • La syntaxe de classe moderne et ses fonctionnalités (constructeurs, getters/setters, méthodes statiques, champs privés)
  • L’héritage et le polymorphisme via extends et la surcharge de méthodes
  • La composition et les mixins pour une réutilisation flexible du code
  • Des patterns de conception courants comme Singleton, Factory, Observer et Décorateur
  • Des techniques avancées de manipulation d’objets avec Object.defineProperty et Proxy

JavaScript offre une approche flexible de la POO qui combine des éléments de programmation basée sur les prototypes avec une syntaxe plus familière inspirée des langages orientés objet classiques. Cette flexibilité vous permet de choisir le style qui convient le mieux à vos besoins, que ce soit l’héritage classique, la composition, ou une approche plus fonctionnelle.

Dans le prochain tutoriel, nous explorerons les Promises et async/await pour la gestion de l’asynchrone.


Besoin de revoir le destructuring ? Consultez notre tutoriel précédent sur le destructuring.

Prêt à continuer ? Passez au prochain tutoriel sur les Promises et async/await.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !