0%
JavaScript Moderno: Programação Orientada a Objetos

POO

Classes e Objetos Modernos

10-15 min

JavaScript Moderno: Programação Orientada a Objetos

JavaScript é uma linguagem flexível que suporta vários paradigmas de programação, entre eles a programação orientada a objetos (POO). Desde o ES6, o JavaScript introduziu uma sintaxe de classe mais intuitiva, embora o modelo subjacente continue baseado em protótipos. Neste tutorial, vamos explorar a POO em JavaScript moderno, incluindo classes, herança, mixins e os padrões de projeto mais utilizados.

O modelo de objetos em JavaScript

Antes de explorar a sintaxe moderna de classes, é importante entender como funciona o modelo de objetos em JavaScript.

Objetos e protótipos

Em JavaScript, quase tudo é um objeto, e a herança funciona por meio de protótipos:

// Criação de um objeto literal
const personne = {
    nom: "Alice",
    saluer() {
        return `Bonjour, je suis ${this.nom}`;
    }
};

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

// Criação de um novo objeto baseado no protótipo 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"

Funções construtoras (método antigo)

Antes do ES6, a criação de objetos semelhantes era geralmente feita com funções construtoras:

// Função construtora
function Personne(nom, age) {
    this.nom = nom;
    this.age = age;
}

// Método adicionado ao protótipo
Personne.prototype.saluer = function() {
    return `Bonjour, je suis ${this.nom} et j'ai ${this.age} ans`;
};

// Criação de instâncias
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 em JavaScript moderno (ES6+)

O ES6 introduziu uma sintaxe de classe mais intuitiva, que simplifica a criação de objetos e a implementação de herança.

Definição de classe

class Personne {
    // Construtor
    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`;
    }
}

// Criação de instâncias
const alice = new Personne("Alice", 30);
console.log(alice.saluer()); // "Bonjour, je suis Alice et j'ai 30 ans"

Propriedades e métodos estáticos

Os métodos e propriedades estáticos pertencem à própria classe, não às instâncias:

class MathUtils {
    // Propriedade 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

// As propriedades e métodos estáticos não são acessíveis nas instâncias
const utils = new MathUtils();
// console.log(utils.PI); // undefined

Acessores (getters e setters)

Os acessores permitem controlar o acesso às propriedades de uma classe:

class Compte {
    constructor(proprietaire) {
        this.proprietaire = proprietaire;
        this._solde = 0; // Convenção: _ indica uma propriedade "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 normais
    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"; // Erro: Le solde doit être un nombre

Campos de classe (Class Fields)

Os campos de classe, introduzidos em versões mais recentes do JavaScript, permitem declarar propriedades diretamente na classe:

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); // Erro: propriedade privada
// p1.#miseAJourInterne(); // Erro: método privado

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

Herança e polimorfismo

A herança permite que uma classe herde as propriedades e métodos de outra classe.

Herança com extends

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

class Chien extends Animal {
    constructor(nom, race) {
        super(nom); // Chamada do construtor pai
        this.race = race;
    }
    
    // Sobrescrita do 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"

A cadeia de protótipos

Mesmo com a sintaxe moderna de classes, o JavaScript continua usando protótipos nos 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

Verificação de tipo

Para verificar se um objeto é uma instância de uma classe, use o 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"

Composição e mixins

Às vezes, a herança sozinha não é suficiente para modelar relações complexas. A composição e os mixins oferecem alternativas flexíveis.

Composição

A composição consiste em combinar objetos mais simples para criar objetos mais complexos:

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

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

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

// Composição por mixin
class Canard extends Animal {
    constructor(nom) {
        super(nom);
        // Composição: adicionar capacidades
        Object.assign(this, peutNager, peutVoler);
    }
}

class Poisson extends Animal {
    constructor(nom) {
        super(nom);
        // Composição: apenas 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()); // Erro: nemo.voler n'est pas une fonction

Mixins funcionais

Os mixins funcionais são funções que adicionam propriedades e métodos a um 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}`);
        }
    };
}

// Aplicação dos mixins
class Utilisateur {
    constructor(nom) {
        this.nom = nom;
    }
}

// Combinação de mixins
const UtilisateurAvecExtensions = avecLogging(avecTimestamp(Utilisateur));

const user = new UtilisateurAvecExtensions("Alice");
console.log(user.nom); // "Alice"
console.log(user.createdAt); // Data de criação
user.log("Instance créée"); // "[UtilisateurAvecExtensions] Instance créée"

Padrões de projeto em JavaScript

Os padrões de projeto (design patterns) são soluções reutilizáveis para problemas recorrentes. Veja alguns padrões comuns em JavaScript.

Singleton

O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a essa instância:

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" (mesma instância)

Factory (Fábrica)

O padrão Factory cria objetos sem expor a lógica de criação:

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

// Utilização
const factory = new VoitureFactory();
const maVoiture = factory.createVoiture("luxe", { couleur: "bleu", portes: 4 });
console.log(maVoiture); // VoitureLuxe com opções personalizadas

Observer (Observador)

O padrão Observer permite que um objeto notifique outros objetos sobre mudanças 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);
    }
}

// Exemplo de uso: sistema de notificação 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`);
    }
}

// Utilização
const station = new StationMeteo();
const affichageConditions = new AffichageConditionsActuelles();
const affichageStats = new AffichageStatistiques();

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

// Simula mudanças meteorológicas
station.setMesures(20, 65, 1013);
station.setMesures(22, 70, 1014);
station.setMesures(19, 75, 1012);

Module

O padrão Module encapsula funcionalidades e expõe uma API pública, mantendo alguns detalhes privados:

// Módulo com IIFE (estilo antigo)
const Compteur = (function() {
    // Variáveis 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)

Decorator

O padrão Decorator permite adicionar comportamentos a objetos individuais sem afetar o comportamento de outros objetos da mesma classe:

// 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€"

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

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

// Criando diretamente um 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€"

Gerenciamento avançado de objetos

O JavaScript oferece várias formas avançadas de manipular e controlar objetos.

Object.defineProperty

Object.defineProperty permite definir propriedades com controle preciso sobre o comportamento delas:

const personne = {};

Object.defineProperty(personne, 'nom', {
    value: 'Alice',
    writable: true,      // pode ser modificada
    enumerable: true,    // aparece nos laços for...in
    configurable: true   // pode ser removida
});

Object.defineProperty(personne, 'age', {
    value: 30,
    writable: false      // não pode ser modificada
});

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 (não modificada porque writable: false)

Proxy

Os Proxies permitem personalizar o comportamento das operações fundamentais sobre objetos:

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

const handler = {
    // Interceptação do acesso às propriedades
    get(cible, prop, receiver) {
        console.log(`Accès à la propriété: ${prop}`);
        
        if (prop === 'prix') {
            return `${cible[prop]}€`;
        }
        
        return cible[prop];
    },
    
    // Interceptação da modificação de propriedades
    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; // sucesso
    }
};

const proxy = new Proxy(cible, handler);

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

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

Símbolos e propriedades Well-Known

Os símbolos podem ser usados como chaves de propriedades únicas e não enumeráveis:

// Criação de um símbolo
const idSymbol = Symbol("id");

const utilisateur = {
    nom: "Alice",
    [idSymbol]: 12345  // propriedade baseada em um símbolo
};

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

// As propriedades baseadas em símbolos não são enumeráveis por padrão
for (let key in utilisateur) {
    console.log(key); // Exibe apenas "nom", não o símbolo
}

// Símbolos well-known para personalizar o comportamento
const collectionPersonnalisee = {
    elements: [1, 2, 3, 4, 5],
    
    // Personalização da iteração
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.elements.length) {
                    return { value: this.elements[index++] * 2, done: false };
                }
                return { done: true };
            }
        };
    }
};

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

Exercícios práticos

Para solidificar seus conhecimentos, tente estes exercícios:

  1. Sistema de formas: crie uma hierarquia de classes com uma classe Forme como base e subclasses como Cercle, Rectangle e Triangle, cada uma com seus próprios métodos para calcular a área e o perímetro.

  2. Gerenciamento de biblioteca: implemente um sistema de gerenciamento de biblioteca com classes para Livre, Auteur, Bibliothèque e Emprunteur. Use o encapsulamento para proteger os dados e os mixins para adicionar funcionalidades como o acompanhamento de empréstimos.

  3. Mini framework de UI: crie um pequeno framework de UI com uma classe Component como base e componentes específicos como Button, Input e Panel que herdam dessa base. Use métodos como render() e update().

  4. Implementação de padrões: implemente o padrão Singleton para um serviço de log e o padrão Factory para criar diferentes tipos de notificações (e-mail, SMS, push).

  5. API de validação: crie uma API de validação com decoradores para validar as propriedades de um objeto (por exemplo, comprimento mínimo de uma string, intervalo de valores para um número).

Exemplo completo: sistema de gerenciamento de produtos

Aqui está um exemplo que combina vários conceitos abordados neste tutorial:

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

// Subclasse 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 o acompanhamento de modificações
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];
    }
};

// Aplicação do mixin
class ProduitAvecHistorique extends avecHistorique(Produit) {
    constructor(...args) {
        super(...args);
        this.ajouterEvenement("creation", { prix: args[1], stock: args[2] || 0 });
    }
    
    // Sobrescrita para adicionar 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 criar diferentes tipos de produtos
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}`);
        }
    }
}

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

// Demonstração
function demoProduits() {
    // Criação de produtos com 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 do singleton
    const catalogue = new CatalogueGestionnaire();
    catalogue.ajouterProduit(p1);
    catalogue.ajouterProduit(p2);
    catalogue.ajouterProduit(p3);
    
    // Operações
    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()}`);
}

// Execução
demoProduits();

Conclusão

Neste tutorial, exploramos a programação orientada a objetos no JavaScript moderno:

  • A sintaxe moderna de classes e seus recursos (construtores, getters/setters, métodos estáticos, campos privados)
  • A herança e o polimorfismo por meio de extends e da sobrescrita de métodos
  • A composição e os mixins para uma reutilização flexível de código
  • Padrões de projeto comuns, como Singleton, Factory, Observer e Decorator
  • Técnicas avançadas de manipulação de objetos com Object.defineProperty e Proxy

O JavaScript oferece uma abordagem flexível de POO, que combina elementos de programação baseada em protótipos com uma sintaxe mais familiar, inspirada nas linguagens orientadas a objetos clássicas. Essa flexibilidade permite escolher o estilo que melhor atende às suas necessidades, seja a herança clássica, a composição ou uma abordagem mais funcional.

No próximo tutorial, vamos explorar as Promises e o async/await para o gerenciamento de código assíncrono.


Precisa revisar o destructuring? Confira nosso tutorial anterior sobre destructuring.

Pronto para continuar? Passe para o próximo tutorial sobre Promises e async/await.

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!