Skip to content

📅 SAISON B Semaine 5 Épisode 2 -L’Héritage et l’Architecture Objet – Factoriser le Code avec extends et super

📚 Notions du jour

Aujourd'hui nous continuons à voir les fondamentaux de la programmation orientée objet. Ce deuxième épisode est dédié à l'Héritage et Architecture Objet.

voici les notions abordées aujourd’hui :

  • L’héritage pour factoriser le code entre les classes.
  • Le mot-clé extends pour définir une classe enfant.
  • L’opérateur super() pour appeler le constructeur parent.
  • Les membres static (méthodes et propriétés) qui appartiennent à la classe elle-même.
  • L’application de l’héritage à l’architecture des contrôleurs.

🧬 Pourquoi l’Héritage ?

L’héritage est un pilier de la POO qui permet de factoriser du code entre plusieurs classes. Au lieu de réécrire la même logique dans chaque classe, on peut définir une classe parente qui contient les attributs et méthodes communs, puis étendre cette classe pour ajouter des fonctionnalités spécifiques.

Avantages :

Évite la duplication de code (principe DRY : Don’t Repeat Yourself).
Facilite la maintenance : une modification dans la classe parente se répercute sur toutes les classes enfants.
Améliore la lisibilité en organisant le code de manière hiérarchique.


🏗️ Le Mot-Clé extends : Créer une Classe Enfant

Le mot-clé extends permet de créer une classe enfant qui hérite des attributs et méthodes d’une classe parente.

1️⃣ Exemple : Une Classe Parente BaseModel

BaseModel.js
class BaseModel {
  // This property is common to all models
  #id;

  constructor(id) {
    this.#id = id;
  }

  // That method is common to all models
  get id() {
    return this.#id;
  }
}

export default BaseModel;

Explications :

  • BaseModel est une classe générique qui contient des attributs communs (id) et des méthodes pour y accéder.

2️⃣ Étendre la Classe BaseModel avec extends et l’Opérateur super() pour Appeler le Constructeur Parent

Coffee.js
// First we import the parent Class to inherit from
import BaseModel from './Model.js';

// To inherit from (extend) a parent class,
// We use the keyword `extends` and reference the parent class.
class Coffee extends BaseModel {
  #name;
  #reference;
  #description;
  #country;
  #pricePerKg;
  #categories;
  #available;

  // `data` is an object that expects all the properties of the Coffee class
  constructor(data) {
    // The `super()` method allows you to pass data into the constructor of the parent class
    // Use only in the constructor
    super(data.id); // Call the parent constructor
    this.#name = data.name;
    this.#description = data.description;
    this.#country = data.country;
    this.#pricePerKg = data.price_per_kg;
    this.#categories = data.categories;

  }

  // Setters and Getters

  // Specific method for Coffee
  getFormattedPrice() {
    return this.#pricePerKg.toFixed(2).replace('.', ',');
  }
}

export default Coffee;

Explications :

  • extends BaseModel : Coffee hérite de BaseModel.
  • super(id) : Appelle le constructeur de la classe parente pour initialiser id.
  • Coffee ajoute ses propres attributs (name, description, etc.) et méthodes (getFormattedPrice()).

🏛️ Les Membres Statiques : Méthodes et Propriétés de Classe

Les membres statiques (méthodes ou propriétés) appartiennent à la classe elle-même, et non aux instances. Ils sont utiles pour :

  • Définir des constantes ou des configurations communes.
  • Créer des méthodes utilitaires qui n’ont pas besoin d’accéder aux attributs d’une instance.

1️⃣ Propriété Statique

Les propriétés statiques sont des attributs qui appartiennent à la classe elle-même, et non aux instances individuelles. Elles sont particulièrement utiles pour définir des constantes, des configurations globales, ou des valeurs partagées par toutes les instances de la classe.

Exemple avec la classe Coffee :

Imaginons que nous voulons définir une valeur de TVA commune à appliquer sur tous les produits. Plutôt que de répéter cette information dans chaque instance, nous pouvons la définir comme une propriété statique de la classe.

Coffee.js
import BaseModel from './Model.js';

class Coffee extends BaseModel {
  // Static property for VAT (20%)
  static VAT_RATE = 0.20;

  constructor(data) {
    super(data.id); // Call the parent constructor
    this.#name = data.name;
    this.#pricePerKg = data.price_per_kg;
    // Other properties...
  }

  // Method to calculate VAT
  getPriceWithVAT() {
    return this.#pricePerKg * (1 + Coffee.VAT_RATE); // Use of static property
  }
}

// Use
const espresso = new Coffee(1, 'Espresso', 2.50);
console.log(espresso.getPriceWithVAT()); // Display 3.00 (2.50 * 1.20)
console.log(Coffee.VAT_RATE); // Display 0.20 (direct access via the class)

Pourquoi utiliser des propriétés statiques ?

  • Éviter la duplication : La TVA n’a pas besoin d’être stockée dans chaque instance.
  • Accès direct via la classe : On peut y accéder avec Coffee.VAT_RATE sans créer d’instance.
  • Centralisation des configurations : Si la TVA change, il suffit de modifier la propriété statique, et toutes les instances utiliseront la nouvelle valeur.

Cas d’usage courants :

  • Constantes globales (ex: taux de TVA, catégories par défaut).
  • Configurations partagées (ex: nom de la table en base de données).
  • Compteurs ou registres (ex: compter le nombre total d’instances créées).

2️⃣ Méthode Statique pour Créer des Instances

Les méthodes statiques sont des fonctions qui appartiennent à la classe elle-même, et non aux instances. Elles sont idéales pour :

  • Créer des utilitaires liés à la classe (ex: formater des données, valider des entrées).
  • Fabriquer des instances à partir de données brutes (ex: convertir une ligne de base de données en objet).
  • Effectuer des opérations globales qui ne dépendent pas des attributs d’une instance spécifique.
Coffee.js
import datamapper from "../../database/main-datamapper.js";
import BaseModel from './Model.js';

class Coffee extends BaseModel {
  // Static property for VAT (20%)
  static VAT_RATE = 0.20;

  constructor(data) {
    super(data.id); // Call the parent constructor
    this.#name = data.name;
    this.#pricePerKg = data.price_per_kg;
    // Other properties...
  }

  // Method to calculate VAT
  getPriceWithVAT() {
    return this.#pricePerKg * (1 + Coffee.VAT_RATE); // Use of static property
  }

  // Static method via the keyword "static",
  // allows you to use the method without instantiating the Coffee class
  static async findAll() {
    const records = await datamapper.getAllAvailableProducts();

    return records.map(record => new Coffee({
      id: record.id,
      name: record.name,
      pricePerKg: record.price_per_kg,
    }));
  }
}

export default Coffee;
main-controller.js
import Coffee from "../src/models/Coffee.js";

const mainController = {
  async renderCatalogPage(req, res) {
    try {
      // If there is no static method, we would have to instantiate the class to use the method
      // const coffee = new Coffee();

      // Using the static findAll() method of the Coffee class without instantiating the class
      const articles = await Coffee.findAll();
      res.render("pages/catalog", { articles });
    } catch (error) {
      console.error(error);
      res.status(500).render("pages/error");
    }
  }
}

export default mainController;

Pourquoi Utiliser des Méthodes Statiques ?

  1. Pas besoin d’instance : On peut appeler Coffee.findAll() sans créer d’objet Coffee au préalable.
  2. Logique centralisée : Les validations ou transformations sont définies au niveau de la classe, ce qui évite la duplication de code.

Cas d’Usage Courants :

  • Validations : Vérifier la cohérence des données avant de créer un objet (ex: Email.isValid(email)).
  • Opérations globales : Calculs ou transformations qui ne dépendent pas d’une instance (ex: Math.max() en JavaScript natif).
  • Requêtes sur la classe : Récupérer toutes les instances d’un type donné (ex: Coffee.findAll()).

💡 Ce Que J’ai Appris Aujourd’hui

  1. L’héritage (extends) permet de factoriser le code entre plusieurs classes.
  2. super() appelle le constructeur de la classe parente pour initialiser les attributs hérités.
  3. Les membres statiques (static) appartiennent à la classe elle-même et sont utiles pour les constantes ou méthodes utilitaires.

⌨️ Challenge du jour : "Projet oCoffee" – projet fil rouge (suite)


☕️ Présentation du Challenge

Aujourd’hui, l’objectif était de refactoriser notre contrôleur principal (mainController) en utilisant la Programmation Orientée Objet (POO). Plus précisément, nous devions :

  • Créer une classe de base CoreController pour centraliser la logique commune (comme la gestion des erreurs 404).
  • Transformer notre contrôleur existant (mainController) en une classe ProductController qui hérite de CoreController.
  • Adapter les exports et les imports pour que tout fonctionne correctement avec Express.

Pourquoi ?

  • Éviter la duplication de code (ex: gestion des erreurs 404 dans chaque méthode).
  • Centraliser la logique commune pour faciliter la maintenance.
  • Améliorer la structure du code en utilisant l’héritage.

📌 Étapes à Réaliser

1️⃣ Créer la Classe CoreController

Objectif : Définir une classe de base avec une méthode pour gérer les erreurs 404.

Mon code :

src/controllers/CoreController.js
class CoreController {
  handleError404 = (res) => {
    res.status(404).render('pages/error', {status: 404});
  };
}

export default CoreController;

Explications :

  • handleError404() : Méthode pour afficher une page 404 personnalisée avec un message indiquant le statut 404.

2️⃣ Transformer mainController en ProductController

Objectif : Refactoriser l’objet littéral mainController en une classe ProductController qui hérite de CoreController.

Mon code :

src/controllers/ProductController.js
// Import the CoreController to inherit the `handleError404()` method.
import CoreController from "./CoreController.js";
import Coffee from "../models/Coffee.js";

// Inherit the CoreController class
class ProductController extends CoreController {
  renderCoffeeDetailsPage = async (req, res) => {
    try {
      const articleId = parseInt(req.params.id);
      // Return 404 error if the id got from the route to coffee details params is not a number
      if (isNaN(articleId)) {
        // Use `handleError404()` method from CoreController
        return this.handleError404(res);
      }

      const article = await Coffee.findById(articleId);
      // Return 404 error if no Coffee is found in the database
      if (!article) {
        // Use `handleError404()` method from CoreController
        return this.handleError404(res);
      }

      // Render coffee details page
      res.render("pages/article", { article });
    } catch (error) {
      console.error(error);
      res.status(500).render("pages/error");
    }
  };
};

Explications :

  • Fonctions fléchées (renderCoffeeDetailsPage = async (...) => { ... }) : Garantissent que this reste lié à l’instance de la classe (nécessaire pour accéder à this.handleError404).
  • Héritage : ProductController hérite de CoreController et utilise sa méthode handleError404.

3️⃣ Exporter une Instance de ProductController

Objectif : Exporter une instance de ProductController pour que le router puisse l’utiliser.

Mon code :

src/controllers/ProductController.js
// src/controllers/ProductController.js
// ... (previous code)

export default new ProductController(); // Export an instance

Explications :

  • export default new ProductController() : Exporte une instance de la classe, ce qui permet à Express d’appeler les méthodes directement (ex: productController.renderCoffeeDetailsPage).

4️⃣ Mettre à Jour le Router

Objectif : Importer et utiliser l’instance de ProductController dans le router.

Mon code :

src/routers/main-router.js
import { Router } from "express";
// Import an instance of the ProductController
import productController from '../controllers/ProductController.js';

const router = Router();

router.get("/", ProductController.renderHomePage);
router.get("/catalog", ProductController.renderCatalogPage);
router.get("/article/:id", ProductController.renderCoffeeDetailsPage);

export default router;

Explications :

productController.renderHomePage, ProductController.renderCatalogPage, ProductController.renderCoffeeDetailsPage : Appellent les méthodes de l’instance de ProductController exportée.


💡 Ce Que J’ai Appris avec ce Challenge

  1. L’héritage permet de factoriser la logique commune (ex: gestion des erreurs) dans une classe parente.
  2. Les fonctions fléchées dans les classes préservent le contexte de this, ce qui est crucial pour accéder aux méthodes héritées.
  3. Exporter une instance de la classe permet à Express d’appeler les méthodes directement.
  4. La POO rend le code plus modulaire et maintenable, surtout dans les projets de taille moyenne/grande.