Leçon : Classes et Programmation Orientée Objet avec TypeScript
Introduction à la Programmation Orientée Objet (POO)
Bienvenue dans cette leçon fondamentale de notre parcours "Maîtriser TypeScript : Développez des Applications Web Robustes et Scalables". Aujourd'hui, nous allons plonger au cœur de la Programmation Orientée Objet (POO), un paradigme de programmation puissant et omniprésent, et découvrir comment TypeScript enrichit cette approche avec sa robustesse typée.
Qu'est-ce que la POO ?
La Programmation Orientée Objet est une méthodologie de conception logicielle qui organise le code autour de "objets" plutôt que d'actions et de logique. Au lieu de se concentrer sur les fonctions qui opèrent sur des données, la POO se concentre sur les objets qui contiennent à la fois des données et les fonctions qui manipulent ces données.
Pourquoi la POO est-elle importante ?
La POO offre de nombreux avantages qui la rendent indispensable dans le développement d'applications modernes :
- Modularité : Le code est organisé en entités autonomes (objets), ce qui facilite la compréhension et la maintenance.
- Réutilisabilité : Les classes et les objets peuvent être réutilisés dans différentes parties d'une application ou même dans d'autres projets, ce qui réduit le temps de développement et améliore la cohérence.
- Maintenabilité : Les changements dans une partie du système ont moins d'impact sur les autres parties, car les objets sont bien isolés.
- Scalabilité : Il est plus facile d'ajouter de nouvelles fonctionnalités ou d'étendre les capacités d'une application existante sans tout casser.
- Abstraction : La POO permet de modéliser des problèmes complexes du monde réel de manière plus intuitive, en se concentrant sur les aspects pertinents et en masquant les détails d'implémentation.
TypeScript, avec son système de types statique, améliore considérablement l'expérience de la POO en introduisant la sécurité des types, la détection précoce des erreurs et une meilleure lisibilité du code lors de la création et de la manipulation d'objets.
Les Fondamentaux de la POO avec TypeScript
La POO repose sur quatre piliers principaux, souvent appelés les "Quatre Piliers de la POO" : Encapsulation, Héritage, Polymorphisme et Abstraction. Avant de les explorer, comprenons les concepts de base : les classes et les objets.
1. Classes : Le Plan de Construction
Une classe est un plan ou un modèle pour créer des objets. Elle définit la structure (les propriétés) et le comportement (les méthodes) que tous les objets créés à partir de cette classe posséderont.
En TypeScript, une classe est déclarée en utilisant le mot-clé class.
Déclaration d'une Classe
// Déclaration d'une classe Personne
class Personne {
// Propriétés (Attributs) : Les données que chaque objet Personne aura
nom: string;
age: number;
email?: string; // Propriété optionnelle
// Constructeur : Méthode spéciale appelée lors de la création d'une nouvelle instance
// Il initialise les propriétés de l'objet.
constructor(nom: string, age: number, email?: string) {
this.nom = nom;
this.age = age;
this.email = email;
}
// Méthodes (Comportements) : Les fonctions que chaque objet Personne peut exécuter
saluer(): string {
return `Bonjour, je m'appelle ${this.nom} et j'ai ${this.age} ans.`;
}
// Une autre méthode
celebrerAnniversaire(): void {
this.age++;
console.log(`Joyeux anniversaire ${this.nom} ! Tu as maintenant ${this.age} ans.`);
}
}
Explication du code :
class Personne { ... }: Définit une nouvelle classe nomméePersonne.nom: string; age: number; email?: string;: Ce sont les propriétés (ou attributs). Elles définissent le type de données que chaque instance dePersonnestockera.email?indique une propriété optionnelle.constructor(nom: string, age: number, email?: string): C'est le constructeur. Il est appelé automatiquement lorsque vous créez un nouvel objetPersonne. Il prend des arguments et les utilise pour initialiser les propriétés de l'objet.this.nom = nom;: Le mot-cléthisfait référence à l'instance actuelle de la classe. Ici, il attribue la valeur passée au constructeur (nom) à la propriéténomde l'instance.saluer(): string { ... }: C'est une méthode. Elle définit un comportement que les objetsPersonnepeuvent effectuer. Elle retourne une chaîne de caractères.celebrerAnniversaire(): void { ... }: Une autre méthode.voidsignifie qu'elle ne retourne aucune valeur.
2. Objets (Instances) : Les Réalisations du Plan
Un objet est une instance concrète d'une classe. Une fois que vous avez un plan (la classe), vous pouvez construire autant d'objets que vous le souhaitez à partir de ce plan.
Création d'Objets
// Création d'instances (objets) de la classe Personne
const personne1 = new Personne("Alice", 30, "alice@exemple.com");
const personne2 = new Personne("Bob", 25); // Sans email, car c'est optionnel
// Accès aux propriétés des objets
console.log(personne1.nom); // Alice
console.log(personne2.age); // 25
console.log(personne2.email); // undefined
// Appel des méthodes des objets
console.log(personne1.saluer()); // Bonjour, je m'appelle Alice et j'ai 30 ans.
personne2.celebrerAnniversaire(); // Joyeux anniversaire Bob ! Tu as maintenant 26 ans.
console.log(personne2.saluer()); // Bonjour, je m'appelle Bob et j'ai 26 ans.
Explication du code :
const personne1 = new Personne("Alice", 30, "alice@exemple.com");: Le mot-clénewest utilisé pour créer une nouvelle instance de la classePersonne. Le constructeur est appelé avec les arguments fournis.personne1.nom: Permet d'accéder à la propriéténomde l'objetpersonne1.personne1.saluer(): Permet d'appeler la méthodesaluer()de l'objetpersonne1.
Les Quatre Piliers de la POO avec TypeScript
Maintenant que nous avons une bonne compréhension des classes et des objets, explorons les piliers fondamentaux de la POO.
1. Encapsulation : Protéger les Données
L'encapsulation est le principe de regrouper les données (propriétés) et les méthodes (fonctions) qui opèrent sur ces données au sein d'une seule unité (la classe), et de restreindre l'accès direct à certaines de ces données depuis l'extérieur de la classe. Cela permet de cacher les détails d'implémentation et de contrôler comment les données sont modifiées.
TypeScript fournit des modificateurs d'accès pour contrôler la visibilité des membres d'une classe :
public(par défaut) : Le membre est accessible de n'importe où.private: Le membre n'est accessible qu'à l'intérieur de la classe où il est défini.protected: Le membre est accessible à l'intérieur de la classe et par les classes dérivées (héritées).
Exemple d'Encapsulation avec private et readonly
class CompteBancaire {
private _solde: number; // Propriété privée pour le solde
readonly numeroCompte: string; // Propriété en lecture seule
constructor(numero: string, soldeInitial: number) {
this.numeroCompte = numero;
this._solde = soldeInitial;
}
// Getter pour accéder au solde (lecture seule de l'extérieur)
get solde(): number {
return this._solde;
}
// Setter pour modifier le solde (avec validation)
deposer(montant: number): void {
if (montant > 0) {
this._solde += montant;
console.log(`Dépôt de ${montant}€. Nouveau solde : ${this._solde}€`);
} else {
console.log("Le montant du dépôt doit être positif.");
}
}
retirer(montant: number): void {
if (montant > 0 && montant <= this._solde) {
this._solde -= montant;
console.log(`Retrait de ${montant}€. Nouveau solde : ${this._solde}€`);
} else {
console.log("Montant de retrait invalide ou solde insuffisant.");
}
}
}
const monCompte = new CompteBancaire("FR123456789", 1000);
// Accès au solde via le getter
console.log(`Solde initial : ${monCompte.solde}€`); // Solde initial : 1000€
// Tentative d'accès direct à la propriété privée (erreur TypeScript)
// monCompte._solde = 500; // Erreur: Property '_solde' is private and only accessible within class 'CompteBancaire'.
// Utilisation des méthodes publiques pour interagir avec le solde
monCompte.deposer(200); // Dépôt de 200€. Nouveau solde : 1200€
monCompte.retirer(500); // Retrait de 500€. Nouveau solde : 700€
monCompte.retirer(1000); // Montant de retrait invalide ou solde insuffisant.
// Propriété readonly : ne peut pas être modifiée après l'initialisation
// monCompte.numeroCompte = "FR987654321"; // Erreur: Cannot assign to 'numeroCompte' because it is a read-only property.
Explication du code :
private _solde: number;: La propriété_soldeest marquée commeprivate. Cela signifie qu'elle ne peut être lue ou modifiée que par les méthodes de la classeCompteBancaireelle-même.readonly numeroCompte: string;: La propriéténumeroCompteestreadonly. Elle peut être initialisée dans le constructeur, mais ne peut plus être modifiée par la suite.get solde(): number { return this._solde; }: C'est un getter. Il fournit un accès en lecture contrôlé à la propriété privée_solde. De l'extérieur, on l'appelle comme une propriété (monCompte.solde).- Les méthodes
deposeretretirer: Elles sont publiques et fournissent les seules façons de modifier le_solde. Elles incluent une logique de validation pour s'assurer que les opérations sont sûres et valides. - Tenter d'accéder ou de modifier
_soldedirectement depuis l'extérieur de la classe ou de modifiernumeroCompteaprès construction générera des erreurs TypeScript, renforçant l'encapsulation.
2. Héritage : Étendre les Fonctionnalités
L'héritage est un mécanisme qui permet à une nouvelle classe (la classe enfant ou classe dérivée) d'acquérir les propriétés et les méthodes d'une classe existante (la classe parent ou classe de base). Cela favorise la réutilisation du code et permet de créer une hiérarchie de classes qui représentent des relations "est un" (par exemple, un Employe est une Personne).
En TypeScript, le mot-clé extends est utilisé pour l'héritage. Le mot-clé super est utilisé pour appeler le constructeur ou les méthodes de la classe parent.
Exemple d'Héritage
// Réutilisation de la classe Personne définie précédemment
class Personne {
nom: string;
age: number;
constructor(nom: string, age: number) {
this.nom = nom;
this.age = age;
}
saluer(): string {
return `Bonjour, je m'appelle ${this.nom}.`;
}
}
// Classe Employe qui hérite de Personne
class Employe extends Personne {
poste: string;
salaire: number;
constructor(nom: string, age: number, poste: string, salaire: number) {
// Appelle le constructeur de la classe parent (Personne)
super(nom, age);
this.poste = poste;
this.salaire = salaire;
}
// Nouvelle méthode spécifique à Employe
decrirePoste(): string {
return `${this.nom} est un(e) ${this.poste} et gagne ${this.salaire}€ par an.`;
}
// Surcharge (override) de la méthode saluer() de la classe parent
saluer(): string {
return `Bonjour, je suis ${this.nom}, votre ${this.poste}.`;
}
}
const employe1 = new Employe("Carole", 35, "Développeuse Frontend", 60000);
const personne3 = new Personne("David", 40);
console.log(employe1.saluer()); // Bonjour, je suis Carole, votre Développeuse Frontend. (Méthode surchargée)
console.log(employe1.decrirePoste()); // Carole est un(e) Développeuse Frontend et gagne 60000€ par an.
console.log(personne3.saluer()); // Bonjour, je m'appelle David. (Méthode de la classe Personne)
// Accès à une propriété héritée
console.log(employe1.nom); // Carole
Explication du code :
class Employe extends Personne: Déclare queEmployeest une sous-classe dePersonne.Employehérite de toutes les propriétés et méthodespublicetprotecteddePersonne.super(nom, age);: Dans le constructeur deEmploye,super()est utilisé pour appeler le constructeur de la classePersonne. C'est obligatoire si la classe parent a un constructeur avec des paramètres.Employeajoute ses propres propriétés (poste,salaire) et méthodes (decrirePoste).- La méthode
saluer()est surchargée (ou overridden) dansEmploye. Cela signifie que lorsqu'elle est appelée sur une instance d'Employe, la version spécifique àEmployeest exécutée au lieu de celle dePersonne.
3. Polymorphisme : Une Interface, Plusieurs Formes
Le polymorphisme (du grec "poly" signifiant plusieurs, et "morph" signifiant formes) est la capacité d'un objet à prendre plusieurs formes. En POO, cela se manifeste généralement par la capacité d'une variable à faire référence à des objets de types différents (mais liés par héritage ou implémentation d'interface), et la capacité d'appeler une méthode sur ces objets qui produira des comportements différents selon le type réel de l'objet.
Le polymorphisme est étroitement lié à l'héritage et à la surcharge de méthodes (comme vu avec saluer() dans l'exemple précédent).
Exemple de Polymorphisme
// Réutilisons nos classes Personne et Employe
// class Personne { ... } (déjà définie)
// class Employe extends Personne { ... } (déjà définie)
function afficherSalutation(personne: Personne): void {
console.log(personne.saluer());
}
const personneGenerique = new Personne("Fabrice", 50);
const employeSpecialise = new Employe("Gilles", 45, "Chef de Projet", 75000);
afficherSalutation(personneGenerique); // Bonjour, je m'appelle Fabrice.
afficherSalutation(employeSpecialise); // Bonjour, je suis Gilles, votre Chef de Projet.
Explication du code :
- La fonction
afficherSalutationattend un paramètre de typePersonne. - Cependant, elle peut accepter à la fois une instance de
Personneet une instance d'Employe(car unEmployeest unePersonnegrâce à l'héritage). - Lorsque
personne.saluer()est appelé, le moteur d'exécution détermine dynamiquement quelle version de la méthodesaluerdoit être appelée : celle dePersonneou celle d'Employe(la version surchargée). C'est le cœur du polymorphisme.
4. Abstraction : Cacher la Complexité
L'abstraction consiste à montrer uniquement les informations essentielles et à masquer les détails d'implémentation complexes. En POO, cela peut être réalisé via les classes abstraites et les interfaces.
Classes Abstraites
Une classe abstraite est une classe qui ne peut pas être instanciée directement. Elle est conçue pour être héritée par d'autres classes. Elle peut contenir des méthodes concrètes (avec implémentation) et des méthodes abstraites (sans implémentation, qui doivent être implémentées par les classes enfants).
// Classe abstraite Forme
abstract class Forme {
// Propriété concrète
couleur: string;
constructor(couleur: string) {
this.couleur = couleur;
}
// Méthode abstraite : doit être implémentée par les classes enfants
abstract calculerAire(): number;
// Méthode concrète : peut être utilisée ou surchargée par les classes enfants
afficherCouleur(): void {
console.log(`La couleur de la forme est ${this.couleur}.`);
}
}
// Classe Cercle qui hérite de Forme
class Cercle extends Forme {
rayon: number;
constructor(couleur: string, rayon: number) {
super(couleur);
this.rayon = rayon;
}
// Implémentation de la méthode abstraite
calculerAire(): number {
return Math.PI * this.rayon * this.rayon;
}
}
// Classe Rectangle qui hérite de Forme
class Rectangle extends Forme {
longueur: number;
largeur: number;
constructor(couleur: string, longueur: number, largeur: number) {
super(couleur);
this.longueur = longueur;
this.largeur = largeur;
}
// Implémentation de la méthode abstraite
calculerAire(): number {
return this.longueur * this.largeur;
}
}
// const maForme = new Forme("rouge"); // Erreur: Cannot create an instance of an abstract class.
const monCercle = new Cercle("bleu", 5);
console.log(`Aire du cercle : ${monCercle.calculerAire()}`); // Aire du cercle : 78.539...
monCercle.afficherCouleur(); // La couleur de la forme est bleu.
const monRectangle = new Rectangle("vert", 10, 4);
console.log(`Aire du rectangle : ${monRectangle.calculerAire()}`); // Aire du rectangle : 40
monRectangle.afficherCouleur(); // La couleur de la forme est vert.
Explication du code :
abstract class Forme { ... }: Définit une classeFormequi ne peut pas être instanciée directement.abstract calculerAire(): number;: Une méthode abstraite n'a pas de corps d'implémentation. Les classes dérivées doivent implémenter cette méthode.CercleetRectangleétendentFormeet fournissent leurs propres implémentations decalculerAire(). Cela garantit que touteFormeaura une méthodecalculerAire, mais l'implémentation exacte dépendra du type de forme spécifique.
Interfaces (Brève Mention)
Les interfaces en TypeScript sont un autre moyen d'atteindre l'abstraction. Elles définissent un contrat sur la structure d'un objet (propriétés et signatures de méthodes), sans aucune implémentation. Une classe peut implement une interface, garantissant qu'elle respecte ce contrat. Les interfaces sont généralement utilisées pour définir des comportements que des classes non liées par héritage peuvent partager, ou pour la validation de la structure des données. Elles seront approfondies dans une leçon dédiée.
Membres Statiques
Jusqu'à présent, toutes les propriétés et méthodes que nous avons vues appartenaient à des instances d'une classe. Chaque objet Personne a son propre nom et age.
Cependant, il est parfois utile d'avoir des propriétés ou des méthodes qui appartiennent à la classe elle-même, plutôt qu'à ses instances. C'est là que les membres static entrent en jeu.
- Les membres
staticsont accessibles directement sur la classe, sans avoir besoin de créer une instance. - Ils sont partagés par toutes les instances de la classe (car ils ne sont liés à aucune instance particulière).
- Ils sont parfaits pour les utilitaires, les compteurs d'instances, ou les constantes.
class OutilMathematique {
// Propriété statique : Pi, une constante mathématique
static readonly PI: number = 3.14159;
// Propriété statique pour compter les opérations effectuées
static operationsCompteur: number = 0;
// Méthode statique : pour ajouter deux nombres
static ajouter(a: number, b: number): number {
OutilMathematique.operationsCompteur++; // Incrémente le compteur statique
return a + b;
}
// Méthode statique : pour soustraire deux nombres
static soustraire(a: number, b: number): number {
OutilMathematique.operationsCompteur++; // Incrémente le compteur statique
return a - b;
}
}
// Accès aux propriétés statiques via la classe elle-même
console.log(OutilMathematique.PI); // 3.14159
// Appel des méthodes statiques
console.log(OutilMathematique.ajouter(10, 5)); // 15
console.log(OutilMathematique.soustraire(10, 5)); // 5
// Accès au compteur statique
console.log(`Nombre d'opérations effectuées : ${OutilMathematique.operationsCompteur}`); // Nombre d'opérations effectuées : 2
// Vous ne pouvez pas appeler les méthodes statiques sur une instance de la classe
// const util = new OutilMathematique();
// util.ajouter(1, 2); // Erreur: Property 'ajouter' does not exist on type 'OutilMathematique'.
Explication du code :
static readonly PI: number = 3.14159;:PIest une propriété statique et en lecture seule. On y accède viaOutilMathematique.PI.static operationsCompteur: number = 0;: Un compteur statique, partagé par toutes les utilisations de la classe.static ajouter(...)etstatic soustraire(...): Ce sont des méthodes statiques. Elles sont appelées directement sur la classe (OutilMathematique.ajouter(...)). Elles peuvent accéder à d'autres membres statiques de la classe (commeOutilMathematique.operationsCompteur).
Conclusion
Félicitations ! Vous avez parcouru les concepts fondamentaux des classes et de la Programmation Orientée Objet avec TypeScript. Nous avons couvert :
- La définition des classes comme des plans pour créer des objets.
- La création d'objets (instances) à partir de classes.
- Les quatre piliers de la POO :
- Encapsulation pour protéger les données avec les modificateurs d'accès (
public,private,protected) et les accesseurs (get/set). - Héritage pour la réutilisabilité du code via
extendsetsuper. - Polymorphisme pour permettre aux objets de prendre plusieurs formes et d'avoir des comportements différents selon leur type réel.
- Abstraction pour masquer la complexité, illustrée par les classes abstraites.
- Encapsulation pour protéger les données avec les modificateurs d'accès (
- L'utilisation des membres statiques pour des propriétés et méthodes liées à la classe elle-même.
La POO, couplée à la puissance typée de TypeScript, vous permet de construire des applications plus modulaires, plus faciles à maintenir, plus robustes et plus scalables.
N'hésitez pas à expérimenter avec ces concepts. La meilleure façon de les maîtriser est de les pratiquer. Créez vos propres classes, définissez des relations d'héritage, et jouez avec les modificateurs d'accès pour bien comprendre leurs implications.