Skip to content

📅 SAISON B Semaine 5 Épisode 1 - DĂ©couverte de la Programmation OrientĂ©e Objet (POO) en JavaScript – Classes, Encapsulation et ContrĂŽle d’AccĂšs

📚 Notions du jour

Aujourd'hui nous avons aborbé un paradigme important en programmation : la Programmation Orientée Objet (ou POO pour les intimes). C'est un concept que je connaissais déjà avec le langage de programmation Python que je pratiquais avant de commencer ma formation. Cette fois-ci j'ai pu voir comment cela était organisé en JavaScript en commençant par les fondamentaux.

Voici les notions aborbées durant cette journée :

  • Les limites du code procĂ©dural et l’intĂ©rĂȘt de la POO.
  • La syntaxe class pour dĂ©finir un « moule » Ă  objets.
  • Le constructor pour initialiser une nouvelle instance.
  • L’encapsulation pour protĂ©ger les donnĂ©es.
  • Les attributs privĂ©s avec le prĂ©fixe #.
  • Les getters et setters pour contrĂŽler l’accĂšs aux donnĂ©es.

🔄 Pourquoi la POO ? Les Limites du Code ProcĂ©dural

Jusqu’à prĂ©sent, j’ai principalement Ă©crit du code procĂ©dural : des fonctions qui s’enchaĂźnent pour accomplir des tĂąches. Cependant, Ă  mesure que les projets grandissent, ce style de programmation montre ses limites :

  • Manque de structure : Le code devient difficile Ă  organiser et Ă  maintenir.
  • RĂ©pĂ©tition : Les donnĂ©es et les fonctions qui les manipulent sont souvent sĂ©parĂ©es, ce qui conduit Ă  dupliquer du code.
  • Faible rĂ©utilisabilitĂ© : Difficile de rĂ©utiliser des morceaux de logique dans d’autres parties du projet.

La Programmation Orientée Objet (POO) résout ces problÚmes en regroupant les données et les comportements dans des objets. Cela permet de :

✅ ModĂ©liser des entitĂ©s rĂ©elles (ex. : un Ă©tudiant, une promotion).
✅ Encapsuler les donnĂ©es pour les protĂ©ger et contrĂŽler leur accĂšs.
✅ RĂ©utiliser du code grĂące Ă  l’hĂ©ritage et la composition.


đŸ—ïž Les Classes : Des Moules pour CrĂ©er des Objets

En JavaScript, une classe est comme un moule qui définit la structure et le comportement des objets. Elle permet de créer des instances (objets concrets) avec des attributs (données) et des méthodes (fonctions).

1ïžâƒŁ DĂ©finir une Classe avec class

// Define a class to represent a Student
class Student {
  // The constructor initializes new instances
  constructor(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }

  // Method to display student info
  getInfo() {
    return `${this.firstName} ${this.lastName}, ${this.age} years old`;
  }
}

Explications :

  • class Student : DĂ©finit une classe nommĂ©e Student.
  • constructor : MĂ©thode spĂ©ciale appelĂ©e lors de la crĂ©ation d’une instance pour initialiser ses attributs.
  • this : Fait rĂ©fĂ©rence Ă  l’instance courante de la classe.

2ïžâƒŁ CrĂ©er une Instance avec new

// Create a new student instance
const student1 = new Student('John', 'Doe', 20);
console.log(student1.getInfo()); // Output: "John Doe, 20 years old"

Résultat :

Un objet student1 est créé avec les attributs firstName, lastName, et age, ainsi que la méthode getInfo().


🔒 L’Encapsulation : ProtĂ©ger les DonnĂ©es

L’encapsulation est un principe clĂ© de la POO qui consiste Ă  :

  • Cacher les dĂ©tails internes d’un objet.
  • ContrĂŽler l’accĂšs aux donnĂ©es via des mĂ©thodes (getters/setters).
  • Éviter les modifications accidentelles des attributs.

1ïžâƒŁ Attributs PrivĂ©s avec #

En JavaScript, on peut dĂ©clarer des attributs privĂ©s en utilisant le prĂ©fixe #. Ces attributs ne sont accessibles qu’à l’intĂ©rieur de la classe.

class Student {
  // Private attributes
  #firstName;
  #lastName;
  #age;

  constructor(firstName, lastName, age) {
    this.#firstName = firstName;
    this.#lastName = lastName;
    this.#age = age;
  }

  // Public method to access private data
  getInfo() {
    return `${this.#firstName} ${this.#lastName}, ${this.#age} years old`;
  }

  // Attempting to access #firstName outside the class will throw an error
  // console.log(student1.#firstName); // SyntaxError: Private field '#firstName' must be declared in an enclosing class
}

Avantages :

  • SĂ©curitĂ© : Les attributs privĂ©s ne peuvent pas ĂȘtre modifiĂ©s directement depuis l’extĂ©rieur.
  • ContrĂŽle : Vous dĂ©cidez comment et quand les donnĂ©es sont accessibles ou modifiĂ©es.

2ïžâƒŁ Getters et Setters pour ContrĂŽler l’AccĂšs

Les getters et setters permettent de lire et modifier les attributs privés de maniÚre contrÎlée.

class Student {
  #firstName;
  #lastName;
  #age;

  constructor(firstName, lastName, age) {
    this.#firstName = firstName;
    this.#lastName = lastName;
    this.#age = age;
  }

  // Getter for firstName
  get firstName() {
    return this.#firstName;
  }

  // Setter for firstName (with validation)
  set firstName(newFirstName) {
    if (typeof newFirstName === 'string' && newFirstName.length > 0) {
      this.#firstName = newFirstName;
    } else {
      console.error('First name must be a non-empty string');
    }
  }

  // Getter for age
  get age() {
    return this.#age;
  }

  // Setter for age (with validation)
  set age(newAge) {
    if (Number.isInteger(newAge) && newAge > 0) {
      this.#age = newAge;
    } else {
      console.error('Age must be a positive integer');
    }
  }
}

// Usage
const student1 = new Student('John', 'Doe', 20);
console.log(student1.firstName); // "John" (using getter)
student1.firstName = 'Jane';      // Valid update
student1.age = 25;                // Valid update
student1.age = -5;                // Error: "Age must be a positive integer"

Explications :

  • get firstName() : Permet d’accĂ©der Ă  l’attribut privĂ© #firstName.
  • set firstName(newFirstName) : Valide et met Ă  jour #firstName si la nouvelle valeur est valide.
  • Validation : Les setters peuvent inclure des rĂšgles pour garantir la cohĂ©rence des donnĂ©es.

💡 Ce Que J’ai Appris Aujourd’hui

  1. Les classes permettent de créer des objets structurés avec des attributs et des méthodes.
  2. L’encapsulation protĂšge les donnĂ©es en les rendant privĂ©es et en contrĂŽlant leur accĂšs via des getters/setters.
  3. Les attributs privĂ©s (avec #) empĂȘchent les modifications directes depuis l’extĂ©rieur de la classe.
  4. Les getters/setters permettent de valider les données avant de les modifier, garantissant ainsi leur intégrité.
  5. La POO rend le code plus modulaire et rĂ©utilisable, ce qui facilite la maintenance et l’évolution des projets.

⌚ Challenge du jour : "Projet oCoffee" – projet fil rouge


☕ PrĂ©sentation du Challenge

Cette semaine nous allons continuer à travailler sur notre projet oCoffee réalisé la semaine derniÚre en autonomie. L'idée est de refactoriser notre application en utilisant la POO.
Plus prĂ©cisĂ©ment, il s’agissait de remplacer les objets bruts retournĂ©s par notre datamapper par des instances de la classe Coffee créée en cours, afin de pouvoir utiliser ses mĂ©thodes directement dans nos vues EJS.


📁 Structure du Projet (bac Ă  sable) RĂ©cupĂ©rĂ© pour le Challenge

📁ocoffee/
│
├── 📁node_modules/          # Folder generated by npm (do not version)
│
├── 📁data/                            #Database folder
│   ├── create_database.sql
│   └── README.md               # How to connect to DB documentation
│
├── 📁database/             # Interactions with the database
│   ├── main-datamapper.js       
│   └── database-client.js   # Connection to database    
│
├── 📁docs/                  # Project specifications folder                  
│   ├── 📁damande-client/
│   ├── 📁integration/
│   └── 📁recommandations/
│
├── 📁public/                # Static files (CSS, JS, images)
│   ├── 📁css/               # Style sheets
│   ├── 📁images/            # Images for the site (logos, cafes, etc.)
│   └── 📁js/                # Client-side JavaScript scripts
│
├── 📁views/                # EJS views (templates)
│     ├── 📁partials/       # Reusable partials (header, footer)
│     └── 📁pages/          # App pages
│
├── index.js                # Express app entry point
├── router.js               # Express routes handler
├── package.json            # Dependencies and npm scripts
├── package-lock.json         
├── .env.example            # Public environnement variables
├── .gitignore
└── README.md               # Project documentation

📌 Étapes Ă  RĂ©aliser

1ïžâƒŁ Importer la Classe Coffee dans le Datamapper

Objectif : Importer la classe Coffee créée en cours pour pouvoir créer des instances à partir des données de la base.

Mon code :

J'ai créé un dossier src/models/ oĂč j'ai copiĂ© la classe Coffee créée en cours âŸč src/models/Coffee.js.

Ensuite je l'importe dans database/main-datamapper.js :

database/main-datamapper.js
import Coffee from "../src/models/Coffee.js";

2ïžâƒŁ Modifier la MĂ©thode getLatestsProducts()

Objectif : Transformer les objets bruts retournés par la base de données en instances de Coffee.

Mon code :

Fichier d'origine :

database/main-datamapper.js
// Import database client
import db from "./database-client.js";

const datamapper = {
  /**
   * Retrieves the 3 most recently added products (coffees) from the database.
   * @async
   * @function getLatestsProducts
   * @returns {Promise<Array>} A promise resolving to an array of objects representing the 3 most recently added coffees.
   */
  async getLatestsProducts() {
     // SQL query to select the 3 most recent coffees, ordered by creation date
    const result = await db.query(
      `SELECT * FROM coffee ORDER BY created_at DESC LIMIT 3`
    );
    const products = result.rows;
    return products;
  },
  // ... other methods...
};

export default datamapper;

Refactorisation :

database/main-datamapper.js
// Import Coffee Class
import Coffee from "../src/models/Coffee.js";

const datamapper = {
  /**
   * Retrieves the 3 most recently added products (coffees) from the database
   * and maps them to Coffee model instances.
   * @async
   * @function getLatestsProducts
   * @returns {Promise<Array<Coffee>>} A promise resolving to an array of Coffee instances representing the 3 most recently added coffees.
   */
  async function getLatestsProducts() {
    // SQL query to select the 3 most recent coffees, ordered by creation date
    const query = `
      SELECT * FROM coffee
      ORDER BY created_at DESC
      LIMIT 3;
    `;
    // Execute the query and retrieve the result
    const result = await client.query(query);

    const products = result.rows;

    // Map each product row to a Coffee model instance
    const coffees = products.map(product => new Coffee({
          id: product.id,
          name: product.name,
          description: product.description,
          reference: product.reference,
          pricePerKg: product.price_per_kg,
          available: product.available,
        }));

    return coffees;
  },
  // ... other methods...
}

Explications :

  • result.rows.map(...) : Utilise la mĂ©thode map() pour transformer chaque objet brut en une instance de Coffee.
  • new Coffee(...) : CrĂ©e une nouvelle instance de Coffee avec les donnĂ©es de la base.

3ïžâƒŁ Utiliser les MĂ©thodes de Coffee dans la Vue home.ejs

Objectif : Afficher le prix formaté en utilisant la méthode getFormattedPrice() de la classe Coffee.

La classe Coffee était fournie avec ses méthodes de classe. J'ai dû modifier la méthode de classe FormattedPrice() qui n'était pas fonctionnelle en l'état afin de pouvoir l'intégrer dans la vue home.ejs. En effet, le prix récupéré dans la base de données était de type string. Il fallait donc d'abord le convertir en nombre avant de pouvoir l'utiliser dans la vue et le formater grùce à la méthode FormattedPrice().

Mon code :

Méthode de classe fournie :

src/models/Coffee.js
/**
 * Represents a Coffee product with its properties and methods.
 * The class is responsible for handling coffee data, including price formatting.
 */
class Coffee {
  #id;
  #name;
  #reference;
  #description;
  #country;
  #pricePerKg;
  #categories;
  #available;

  constructor(data) {
    this.#id = data.id;
    this.#name = data.name;
    this.#reference = data.reference;
    this.#description = data.description;
    this.#country = data.country;
    this.#pricePerKg = data.price_per_kg;
    this.#categories = data.categories;
    this.#available = data.available;
  }

  // Getters and Setters
  get id() {
    return this.#id;
  }

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

  set name(value) {
    this.#name = value;
  }
  // ... And so on for other getters/setters ...

  /**
   * Formats the coffee price for display.
   * Converts the price to a fixed 2-decimal format and replaces the dot with a comma,
   * which is a common format in many European locales.
   * @returns {string} The formatted price as a string.
   */
  formattedPrice() {
    return this.#pricePerKg.toFixed(2).replace('.', ',');
  }
}

export default Coffee;

Refactorisation de la méthode :

src/models/Coffee.js
/**
 * Represents a Coffee product with its properties and methods.
 * The class is responsible for handling coffee data, including price formatting.
 */
class Coffee {
  #id;
  #name;
  #reference;
  #description;
  #country;
  #pricePerKg;
  #categories;
  #available;

  constructor(data) {
    this.#id = data.id;
    this.#name = data.name;
    this.#reference = data.reference;
    this.#description = data.description;
    this.#country = data.country;
    // Refectorisation here: convert the price from string to float to ensure proper formatting
    this.#pricePerKg = parseFloat(data.price_per_kg);
    this.#categories = data.categories;
    this.#available = data.available;
  }

  // Getters and Setters
  get id() {
    return this.#id;
  }

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

  set name(value) {
    this.#name = value;
  }
  // ... And so on...

  /**
   * Formats the coffee price for display.
   * Converts the price to a fixed 2-decimal format and replaces the dot with a comma,
   * which is a common format in many European locales.
   * @returns {string} The formatted price as a string.
   */
  formattedPrice() {
    return this.#pricePerKg.toFixed(2).replace('.', ',');
  }
}

export default Coffee;

Refactorisation de la vue home.ejs avec la méthode FormattedPrice() :

views/pages/home.ejs
<!-- header -->
<main>
  <!-- Other sections -->
  <section id="latest-products">
    <h2>Nouveautés</h2>

    <ul>
      <% articles.forEach(article => { %>
      <li>
        <article>
          <img
            src="/images/coffees/<%= article.reference %>.png"
            alt="<%= article.name %>"
          />
          <div>
            <h3><%= article.name %></h3>
            <!-- Paragraph added to display formatted price of an article -->
            <!-- Using formattedPrice() method from Coffee Class instance -->
            <p><%= article.formattedPrice() %></p>
            <a class="button-outline" href="/article/<%= article.id %>">
              Voir le détail
            </a>
          </div>
        </article>
      </li>
      <% }) %>
    </ul>
  </section>
</main>
<!-- footer -->

Résultat attendu :

Le prix s’affiche sous la forme 20,99 € (au lieu d’un nombre brut comme 20.99).


💡 Ce Que J’ai Appris avec ce Challenge

  1. La POO permet d’encapsuler la logique mĂ©tier dans des classes, ce qui rend le code plus modulaire et rĂ©utilisable.
  2. Les méthodes comme map() sont utiles pour transformer des données brutes en objets plus riches.
  3. Les vues deviennent plus expressives quand elles utilisent des mĂ©thodes d’objets (ex: FormattedPrice()).