Maîtriser TypeScript : Développez des Applications Web Robustes et Scalables
Maîtriser TypeScript : Développez des Applications Web Robustes et Scalables

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ée Personne.
  • 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 de Personne stockera. 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 objet Personne. Il prend des arguments et les utilise pour initialiser les propriétés de l'objet.
  • this.nom = nom; : Le mot-clé this fait référence à l'instance actuelle de la classe. Ici, il attribue la valeur passée au constructeur (nom) à la propriété nom de l'instance.
  • saluer(): string { ... } : C'est une méthode. Elle définit un comportement que les objets Personne peuvent effectuer. Elle retourne une chaîne de caractères.
  • celebrerAnniversaire(): void { ... } : Une autre méthode. void signifie 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é new est utilisé pour créer une nouvelle instance de la classe Personne. Le constructeur est appelé avec les arguments fournis.
  • personne1.nom : Permet d'accéder à la propriété nom de l'objet personne1.
  • personne1.saluer() : Permet d'appeler la méthode saluer() de l'objet personne1.

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é _solde est marquée comme private. Cela signifie qu'elle ne peut être lue ou modifiée que par les méthodes de la classe CompteBancaire elle-même.
  • readonly numeroCompte: string; : La propriété numeroCompte est readonly. 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 deposer et retirer : 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 _solde directement depuis l'extérieur de la classe ou de modifier numeroCompte aprè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 que Employe est une sous-classe de Personne. Employe hérite de toutes les propriétés et méthodes public et protected de Personne.
  • super(nom, age); : Dans le constructeur de Employe, super() est utilisé pour appeler le constructeur de la classe Personne. C'est obligatoire si la classe parent a un constructeur avec des paramètres.
  • Employe ajoute ses propres propriétés (poste, salaire) et méthodes (decrirePoste).
  • La méthode saluer() est surchargée (ou overridden) dans Employe. Cela signifie que lorsqu'elle est appelée sur une instance d'Employe, la version spécifique à Employe est exécutée au lieu de celle de Personne.

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 afficherSalutation attend un paramètre de type Personne.
  • Cependant, elle peut accepter à la fois une instance de Personne et une instance d'Employe (car un Employe est une Personne grâce à l'héritage).
  • Lorsque personne.saluer() est appelé, le moteur d'exécution détermine dynamiquement quelle version de la méthode saluer doit être appelée : celle de Personne ou 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 classe Forme qui 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.
  • Cercle et Rectangle étendent Forme et fournissent leurs propres implémentations de calculerAire(). Cela garantit que toute Forme aura une méthode calculerAire, 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 static sont 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; : PI est une propriété statique et en lecture seule. On y accède via OutilMathematique.PI.
  • static operationsCompteur: number = 0; : Un compteur statique, partagé par toutes les utilisations de la classe.
  • static ajouter(...) et static 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 (comme OutilMathematique.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 extends et super.
    • 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.
  • 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.