đ 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
classpour définir un « moule » à objets. - Le
constructorpour 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Ă©eStudent.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#firstNamesi 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
- Les classes permettent de créer des objets structurés avec des attributs et des méthodes.
- Lâencapsulation protĂšge les donnĂ©es en les rendant privĂ©es et en contrĂŽlant leur accĂšs via des getters/setters.
- Les attributs privés (avec
#) empĂȘchent les modifications directes depuis lâextĂ©rieur de la classe. - Les getters/setters permettent de valider les donnĂ©es avant de les modifier, garantissant ainsi leur intĂ©gritĂ©.
- 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 :
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 :
// 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 :
// 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éthodemap()pour transformer chaque objet brut en une instance deCoffee.new Coffee(...): Crée une nouvelle instance deCoffeeavec 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 :
/**
* 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 :
/**
* 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() :
<!-- 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
- La POO permet dâencapsuler la logique mĂ©tier dans des classes, ce qui rend le code plus modulaire et rĂ©utilisable.
- Les méthodes comme
map()sont utiles pour transformer des donnĂ©es brutes en objets plus riches. - Les vues deviennent plus expressives quand elles utilisent des mĂ©thodes dâobjets (ex:
FormattedPrice()).