0%
JavaScript Moderno: Programación Orientada a Objetos

POO

Clases y Objetos Modernos

10-15 min

JavaScript Moderno: Programación Orientada a Objetos

JavaScript es un lenguaje flexible que soporta varios paradigmas de programación, entre ellos la programación orientada a objetos (POO). Desde ES6, JavaScript introdujo una sintaxis de clases más intuitiva, aunque el modelo subyacente sigue basándose en prototipos. En este tutorial exploraremos la POO en JavaScript moderno, incluyendo las clases, la herencia, los mixins y los patrones de diseño más utilizados.

El modelo de objetos en JavaScript

Antes de explorar la sintaxis moderna de las clases, es importante entender cómo funciona el modelo de objetos en JavaScript.

Objetos y prototipos

En JavaScript, casi todo es un objeto y la herencia funciona mediante prototipos:

// Creación de un objeto literal
const personne = {
    nom: "Alice",
    saluer() {
        return `Bonjour, je suis ${this.nom}`;
    }
};

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

// Creación de un nuevo objeto basado en el prototipo 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"

Funciones constructoras (método antiguo)

Antes de ES6, la creación de objetos similares se hacía generalmente mediante funciones constructoras:

// Función constructora
function Personne(nom, age) {
    this.nom = nom;
    this.age = age;
}

// Método añadido al prototipo
Personne.prototype.saluer = function() {
    return `Bonjour, je suis ${this.nom} et j'ai ${this.age} ans`;
};

// Creación de instancias
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"

Clases en JavaScript moderno (ES6+)

ES6 introdujo una sintaxis de clases más intuitiva que simplifica la creación de objetos y la implementación de la herencia.

Definición de una clase

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

// Creación de instancias
const alice = new Personne("Alice", 30);
console.log(alice.saluer()); // "Bonjour, je suis Alice et j'ai 30 ans"

Propiedades y métodos estáticos

Los métodos y propiedades estáticos pertenecen a la propia clase, no a las instancias:

class MathUtils {
    // Propiedad estática (ES2022+)
    static PI = 3.14159;
    
    // Método estático
    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

// Las propiedades y métodos estáticos no son accesibles desde las instancias
const utils = new MathUtils();
// console.log(utils.PI); // undefined

Accesores (getters y setters)

Los accesores permiten controlar el acceso a las propiedades de una clase:

class Compte {
    constructor(proprietaire) {
        this.proprietaire = proprietaire;
        this._solde = 0; // Convención: _ indica una propiedad "privada"
    }
    
    // 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étodos 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"; // Error: Le solde doit être un nombre

Campos de clase (Class Fields)

Los campos de clase, introducidos en las versiones más recientes de JavaScript, permiten declarar propiedades directamente en la clase:

class Produit {
    // Campos públicos
    nom;
    prix;
    
    // Campo privado (ECMAScript 2022+)
    #stock = 0;
    
    // Campo estático
    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étodo privado (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); // Error: propiedad privada
// p1.#miseAJourInterne(); // Error: método privado

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

Herencia y polimorfismo

La herencia permite a una clase heredar las propiedades y los métodos de otra clase.

Herencia con extends

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

class Chien extends Animal {
    constructor(nom, race) {
        super(nom); // Llamada al constructor padre
        this.race = race;
    }
    
    // Sobrescritura del método 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 cadena de prototipos

Incluso con la sintaxis moderna de clases, JavaScript sigue utilizando prototipos entre bastidores:

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

Comprobación de tipo

Para comprobar si un objeto es una instancia de una clase, utiliza el operador instanceof:

function traiterAnimal(animal) {
    if (animal instanceof Chien) {
        console.log(animal.parler()); // Método de Chien
    } else if (animal instanceof Animal) {
        console.log(animal.parler()); // Método de 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"

Composición y mixins

A veces, la herencia por sí sola no basta para modelar relaciones complejas. La composición y los mixins ofrecen alternativas flexibles.

Composición

La composición consiste en combinar objetos más simples para crear objetos más complejos:

// Objetos de capacidades
const peutNager = {
    nager() {
        return `${this.nom} nage`;
    }
};

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

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

// Composición mediante mixin
class Canard extends Animal {
    constructor(nom) {
        super(nom);
        // Composición: añadir capacidades
        Object.assign(this, peutNager, peutVoler);
    }
}

class Poisson extends Animal {
    constructor(nom) {
        super(nom);
        // Composición: solo nadar
        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()); // Error: nemo.voler no es una función

Mixins funcionales

Los mixins funcionales son funciones que añaden propiedades y métodos a un objeto existente:

// Mixin funcional
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}`);
        }
    };
}

// Aplicación de los mixins
class Utilisateur {
    constructor(nom) {
        this.nom = nom;
    }
}

// Combinación de mixins
const UtilisateurAvecExtensions = avecLogging(avecTimestamp(Utilisateur));

const user = new UtilisateurAvecExtensions("Alice");
console.log(user.nom); // "Alice"
console.log(user.createdAt); // Fecha de creación
user.log("Instance créée"); // "[UtilisateurAvecExtensions] Instance créée"

Patrones de diseño en JavaScript

Los patrones de diseño son soluciones reutilizables a problemas recurrentes. Estos son algunos patrones habituales en JavaScript.

Singleton

El patrón Singleton garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella:

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" (misma instancia)

Factory (Fábrica)

El patrón Factory crea objetos sin exponer la lógica de creación:

// Clases concretas
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}`);
        }
    }
}

// Uso
const factory = new VoitureFactory();
const maVoiture = factory.createVoiture("luxe", { couleur: "bleu", portes: 4 });
console.log(maVoiture); // VoitureLuxe con opciones personalizadas

Observer (Observador)

El patrón Observer permite que un objeto notifique a otros objetos los cambios de estado:

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);
    }
}

// Ejemplo de uso: sistema de notificación meteorológica
class StationMeteo extends Observable {
    setMesures(temperature, humidite, pression) {
        this.etat = { temperature, humidite, pression, date: new Date() };
        this.notifier(this);
    }
}

// Observadores
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`);
    }
}

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

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

// Simula cambios meteorológicos
station.setMesures(20, 65, 1013);
station.setMesures(22, 70, 1014);
station.setMesures(19, 75, 1012);

Módulo

El patrón Module encapsula funcionalidades y expone una API pública manteniendo privados ciertos detalles:

// Módulo con IIFE (estilo antiguo)
const Compteur = (function() {
    // Variables privadas
    let compte = 0;
    
    // Métodos privados
    function valider(valeur) {
        return typeof valeur === 'number' && !isNaN(valeur);
    }
    
    // API pública
    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 (privado)

Decorador

El patrón Decorador permite añadir comportamientos a objetos individuales sin afectar al comportamiento de otros objetos de la misma clase:

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

// Decoradores
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`;
    }
}

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

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

// Añadir azúcar
monCafe = new SucreDecorator(monCafe);
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple avec du lait avec du sucre : 3.2€"

// Creación directa de un café personalizado
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€"

Gestión avanzada de objetos

JavaScript ofrece varias formas avanzadas de manipular y controlar objetos.

Object.defineProperty

Object.defineProperty permite definir propiedades con un control preciso sobre su comportamiento:

const personne = {};

Object.defineProperty(personne, 'nom', {
    value: 'Alice',
    writable: true,      // se puede modificar
    enumerable: true,    // aparece en los bucles for...in
    configurable: true   // se puede eliminar
});

Object.defineProperty(personne, 'age', {
    value: 30,
    writable: false      // no se puede modificar
});

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 (no se modifica porque writable: false)

Proxy

Los Proxy permiten personalizar el comportamiento de las operaciones fundamentales sobre los objetos:

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

const handler = {
    // Interceptación del acceso a las propiedades
    get(cible, prop, receiver) {
        console.log(`Accès à la propriété: ${prop}`);
        
        if (prop === 'prix') {
            return `${cible[prop]}€`;
        }
        
        return cible[prop];
    },
    
    // Interceptación de la modificación de propiedades
    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; // éxito
    }
};

const proxy = new Proxy(cible, handler);

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

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

Símbolos y propiedades Well-Known

Los símbolos pueden utilizarse como claves de propiedades únicas y no enumerables:

// Creación de un símbolo
const idSymbol = Symbol("id");

const utilisateur = {
    nom: "Alice",
    [idSymbol]: 12345  // propiedad basada en un símbolo
};

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

// Las propiedades basadas en símbolos no son enumerables por defecto
for (let key in utilisateur) {
    console.log(key); // Muestra solo "nom", no el símbolo
}

// Símbolos well-known para personalizar el comportamiento
const collectionPersonnalisee = {
    elements: [1, 2, 3, 4, 5],
    
    // Personalización de la iteración
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.elements.length) {
                    return { value: this.elements[index++] * 2, done: false };
                }
                return { done: true };
            }
        };
    }
};

// Uso con for...of, que utiliza el iterador
for (const item of collectionPersonnalisee) {
    console.log(item); // 2, 4, 6, 8, 10
}

Ejercicios prácticos

Para consolidar tus conocimientos, prueba estos ejercicios:

  1. Sistema de formas: crea una jerarquía de clases con una clase Forme como base y subclases como Cercle, Rectangle y Triangle, cada una con sus propios métodos para calcular el área y el perímetro.

  2. Gestión de biblioteca: implementa un sistema de gestión de biblioteca con clases para Livre, Auteur, Bibliothèque y Emprunteur. Utiliza el encapsulamiento para proteger los datos y los mixins para añadir funcionalidades como el seguimiento de los préstamos.

  3. Mini framework de UI: crea un pequeño framework de UI con una clase Component como base y componentes específicos como Button, Input y Panel que hereden de esa base. Utiliza métodos como render() y update().

  4. Implementación de patrones: implementa el patrón Singleton para un servicio de registro (logging) y el patrón Factory para crear distintos tipos de notificaciones (email, SMS, push).

  5. API de validación: crea una API de validación con decoradores para validar las propiedades de un objeto (por ejemplo, la longitud mínima de una cadena o el rango de valores de un número).

Ejemplo completo: sistema de gestión de productos

Este es un ejemplo que combina varios de los conceptos abordados en este tutorial:

// Clase 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étodos
    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}`;
    }
}

// Subclase especializada
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 para el seguimiento de las modificaciones
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];
    }
};

// Aplicación del mixin
class ProduitAvecHistorique extends avecHistorique(Produit) {
    constructor(...args) {
        super(...args);
        this.ajouterEvenement("creation", { prix: args[1], stock: args[2] || 0 });
    }
    
    // Sobrescritura para añadir eventos
    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 para crear distintos tipos de productos
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}`);
        }
    }
}

// Gestor de catálogo (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);
    }
}

// Demostración
function demoProduits() {
    // Creación de productos con 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");
    
    // Uso del singleton
    const catalogue = new CatalogueGestionnaire();
    catalogue.ajouterProduit(p1);
    catalogue.ajouterProduit(p2);
    catalogue.ajouterProduit(p3);
    
    // Operaciones
    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()}`);
}

// Ejecución
demoProduits();

Conclusión

En este tutorial hemos explorado la programación orientada a objetos en JavaScript moderno:

  • La sintaxis de clases moderna y sus funcionalidades (constructores, getters/setters, métodos estáticos, campos privados)
  • La herencia y el polimorfismo mediante extends y la sobrescritura de métodos
  • La composición y los mixins para una reutilización flexible del código
  • Patrones de diseño habituales como Singleton, Factory, Observer y Decorador
  • Técnicas avanzadas de manipulación de objetos con Object.defineProperty y Proxy

JavaScript ofrece un enfoque flexible de la POO que combina elementos de la programación basada en prototipos con una sintaxis más familiar, inspirada en los lenguajes orientados a objetos clásicos. Esta flexibilidad te permite elegir el estilo que mejor se adapte a tus necesidades, ya sea la herencia clásica, la composición o un enfoque más funcional.

En el próximo tutorial exploraremos las Promises y async/await para la gestión de la asincronía.


¿Necesitas repasar la desestructuración? Consulta nuestro tutorial anterior sobre destructuring.

¿Listo para continuar? Pasa al siguiente tutorial sobre Promises y async/await.

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!