Topic outline

  • Présentation de la programmation orientée objet


    La Programmation Orientée Objet (POO, et OOP en anglais) est un des concepts les plus utilisés en programmation logiciel. Il permet d'organiser son code selon une certaine logique avec des règles précises, et permet de traduire plus facilement certains comportements en langage de programmation. 

    L'idée générale de la POO, c'est que vous allez regrouper une partie de votre code qui traite de la même chose dans un seul endroit. De la même façon que les fonctions vous permettent d'encapsuler une partie de votre code, vous allez séparer une partie de votre code de votre script principal grâce à la programmation objet.

    Il y a deux grands concepts en POO : les classes et les objets. On va définir la logique dans les classes, et l'utiliser au travers des objets. Les objets sont des sortes de "super variables" qui vont contenir des propriétés et des méthodes. 

    Dans les cours précédent, nous avons déjà utilisés de nombreux objets. Par exemple l'objet JS Date. Nous avons vu que nous pouvions créer un nouvel objet Date, et le manipuler grâce à certaines méthodes "encapsulées" dans cet objet. 

    Voici un exemple d'utilisation de la POO au travers de la classe JS native Date :

    1. let date = new Date(); : on créer un nouvel objet Date.
    2. date.toLocaleDateString() : on utilise la méthode toLocaleDateString() proposée par la classe Date


    Ces deux lignes cachent en réalité un code très complexe :

    1. Comment se fait il que l'instructions new Date() revoit une valeur qui correspond à la date actuelle ? (à la millisecondes près)
    2. Comment la méthode toLocaleDateString se débrouille-t-elle pour vous afficher une date au format local sur la base de l'objet instancié plus haut ?


    Vous imaginez bien qu'il doit y avoir du code assez complexe pour générer toutes ces valeurs. Vous l'avez utilisé, sans pour autant savoir ce qui se passait "en coulisse". Et bien la programmation objet c'est ça. On va déterminer une logique à un endroit, et on va l'exploiter à un autre. C'est très pratique, car ça vous permet de bien cloisonner votre code, et d'exprimer de nombreuses manières d'utiliser un même concept (ici le traitement des dates et du temps).


    L'encapsulation : le concept de la boite noire


    Un des aspects important de la POO, c'est l'encapsulation. Toutes les choses dont vous aurez besoin pour utiliser correctement vos objets seront "enfermées" (encapsulées) dans votre classe.

    Pour prendre un exemple concret regardons comment fonctionne un cockpit d'avion. Chaque bouton permet d'effectuer une action précise dans l'avion, telles que :

    • sortir ou entrer le train d'atterrissage
    • augmenter la puissance des moteurs
    • modifier l'assiette, le tangage, le roulis
    • allumer les phares
    • etc...


    Et bien, quand le pilote va actionner ces boutons, il ne sait pas forcément tout ce qui se passe derrière. Et il n'a pas à le savoir. Lui tout ce qu'il à savoir c'est : "si j'appuie sur le bouton pour sortir le train d'atterrissage, le train d'atterrissage doit sortir". Il ne sait pas qu'on va peut être actionner l'ouverture de la trappe, vérifier la pression du circuit hydraulique, actionner des moteurs pour faire sortir le train, désactiver le frein de parking,...

    Ce cloisonnement entre logique et comportement d'un côté, et utilisation de l'autre est très important. Cela vous permet de ne pas tout mélanger dans votre code, et d'avoir un comportement "normalisé" pour tout vos objets.


    Créer ces propres classes et objets

    De nombreux objets existent "nativement" dans les différents langages de programmation. Ils correspondent à des usages répandus, redondant et pratiques, et sont donc ajoutés nativement aux différents langages, selon les besoins. Mais en tant que développeur, vous allez devoir créer et manipuler des classes et objets conçus spécifiquement pour l'application sur laquelle vous travaillerez. Dans ce cours, nous allons découvrir la POO via la création et l'utilisation d'une classe Voiture.

  • Les classes et les instances de classes

    Les classes et les instances de classes sont les deux concepts de base de la POO. Ce n'est pas bien compliqué : les classes représentent un fonctionnement, et les instances de classe sont l'utilisation "pratique" de vos classes. On parle d'instance de classe car quand on créé un nouvel objet à partir d'une classe, on dit qu'on instancie la classe, tout simplement. 

    Donc un objet n'est pas une classe, et une classe n'est pas un objet.


    La classe va contenir toutes les propriétés de notre future voiture, tandis que l'objet voiture sera celui que vous utiliserez dans votre code et qui possédera "par nature" toutes les définitions d'une voiture. Votre objet voiture est donc une instance de votre classe voiture. Et vous pouvez avoir 100 voitures différentes qui sont les instances d'une seule classe voiture.

    Description minimaliste de notre classe Voiture

    On peux dire qu'une voiture "possède" :

    1. une marque
    2. une couleur

    => ce sont nos attributs de classe


    On peux dire qu'une voiture "peux" :

    1. accélérer
    2. freiner
    => ce sont nos méthodes de classe


    Implémentation de la classe Voiture


    exemple classe poo jsVoici un exemple d'implémentation de notre classe Voiture en Typescript :

    class Voiture {
    
      vitesseMax = 0;
      couleur = '';
    
      accelerer() {
        console.log('La voiture accélère !!');
      }
      freiner() {
        console.log('La voiture freine...');
      }
      
    }

    Dans cet extrait de code, on définit ce qu'est une voiture, et ce qu'elle peux faire. Remarquez l'utilisation du mot clé class pour définir une classe. On dit alors que vitesseMax et couleur sont des attributs de la classe Voiture, tandis que accelerer() et freiner() sont des méthodes de la classe Voiture.

    => Copiez ce code dans votre fichier voiture.ts.



    Instanciation d'un objet voiture

    classes et instances de classes poo
    Voici comment on pourrait manipuler un objet voiture :

    let voiture = new Voiture();
    voiture.vitesseMax = 130;
    voiture.couleur = 'rouge';
    console.log(voiture.vitesseMax);
    console.log(voiture.couleur);
    voiture.accelerer();
    voiture.freiner();

    Dans cet extrait de code, on instancie la classe voiture dans une variable, via l'utilisation du mot clé new, et on utilise l'objet par la suite. On peux voir ici que les notions de marque ou de couleur ne sont pas des variables "à part", mais sont bien des composantes de notre objet voiture. La seule variable déclarée est voiture.

    Quand on instancie un nouvel objet d'une classe, on hérite de toutes les propriétés de la classe. Donc dans notre exemple, notre objet voiture aura une vitesse maximum et une couleur, et on pourra accélérer et freiner.

    => Copiez ce code dans votre fichier script.js.


    Vérifiez que Typescript a bien compilé votre fichier voiture.ts dans voiture.js, puis regarder le résultat dans votre console. Sans rentrer dans le détails, on voit bien que l'on peut faire des manipulation sur notre objet voiture, définit par la classe Voiture.

    Ce qu'il faut retenir :
    1. les classes sont la description du comportement et de la logique d'une "chose", et se déclarent avec le mot clé class
    2. les objets sont la "matérialisation" d'une classe, et se déclarent avec le mot clé new. On dit alors qu'un objet est une instance de classe.
  • Les constructeurs

    poo constructeurLe constructeur est un élément très important des classes. C'est la toute première méthode qui est appelée quand vous instanciez un objet au travers de l'utilisation du mot clé new.

    On peux le voir ici :

    let voiture = new Voiture();

    Cette instruction veux dire : "construit moi un nouvel objet de la classe Voiture dans la variable voiture".

    Sauf que pour l'instant, notre classe Voiture n'a pas de constructeur défini. Javascript va alors construire l'objet automatiquement, car vous n'êtes pas obligé de définir un constructeur dans vos classes, mais c'est nécessaire la plupart du temps. Voyons comment en implémenter un.


    Rajoutez ce code dans votre classe Voiture :

    constructor(vitesseMax = 0, couleur = ''){
      this.vitesseMax = vitesseMax;
      this.couleur = couleur;
      console.log(`Nouvelle voiture ${this.couleur} créée. Elle va jusqu'à ${this.vitesseMax}km/h.`)
    }

    Ici, le mot clé constructor permet de définir notre constructeur. Ça marche de la même manière que les fonctions, on peux y définir des paramètres, avec des valeurs par défaut, puis exécuter une série d'instructions. On voit aussi l'apparition du mot clé this que l'on verra ensemble plus tard, il permet ici de donner des valeurs aux propriétés de votre objet. Vous n'avez pas besoin "d’appeler" votre constructeur, cela se fait automatiquement via l'utilisation du mot clé new.


    Copiez ce code dans votre fichier script :

    let voiture = new Voiture(130, 'jaune');

    Pour conclure, on peux dire que le constructeur va initialiser chaque nouvel objet selon la logique que vous aurez écrite. 

    => Modifiez les paramètres de création de l'objet voiture dans votre fichier script.js pour constater que le constructeur les prends bien en compte. 


    Pour bien comprendre la différence entre classe et objet, créez moi une deuxième voiture dans votre fichier script, qui a des propriétés différentes de la première. 

    Exemple :

    let voiture1 = new Voiture(100, 'rouge');
    let voiture2 = new Voiture(130, 'noire');


    Maintenant, changez les valeurs des paramètres par défaut dans votre constructeur afin qu'il initialise par défaut la propriété "vitesseMax" à "50", et la propriété "couleur" à "blanc".

    Exemple :

    constructor(vitesseMax = 50, couleur = 'blanc')

    Puis, initialisez une troisième voiture dans votre script, sans lui donner de paramètres de vitesseMax ou de couleur.

    Exemple :

    let voiture3 = new Voiture();

    Désormais, vous devriez avoir 3 messages de création d'une nouvelle voiture dans votre console :

    constructeur poo js

  • Les attributs et propriétés

    Les attributs sont les données qui représentent votre classe. Dans notre classe voiture, les attributs sont "vitesseMax" et "couleur". Vous pouvez déterminer autant d'attributs que vous le souhaitez. On parle d'attributs au niveau de la classe (attributs de classe) et de propriétés au niveau de l'objet. Les attributs sont la description de ce que vous souhaitez avoir dans vos classes, tandis que les propriétés sont les valeurs de chacun de vos objets.

    Dans cet exemple :

    let voiture1 = new Voiture(100, 'rouge');
    let voiture2 = new Voiture(130, 'noire');
    let voiture3 = new Voiture();

    On peux voir que toutes les voitures ont une vitesseMax et une couleur (ce sont les attributs de la classe Voiture), par contre chaque voiture aura des valeurs différentes :

    • 100 et rouge pour la voiture1
    • 130 et noire pour la voiture2
    • 50 et blanc pour la voiture3



    La différence de vocabulaire est importante car on parle bien de deux choses différentes : toutes les voitures ont une couleur, mais elles n'ont pas toute la même couleur. Donc attributs pour la classe (description) et propriétés pour les objets (instanciation). 

    Les attributs sont comme des variables classiques, et ils peuvent recevoir tout type de valeurs (nombres, chaînes, tableaux, objets, etc...). Il vous suffit simplement de les déclarer dans votre classe, puis de les utiliser comme bon vous semble par la suite.


    Rajoutons un attribut "quantité carburant" et un attribut "volume du réservoir" à notre classe Voiture :

    class Voiture {
      vitesseMax = 0;
      couleur = '';
      quantiteCarburant = 0;
      volumeReservoir = 0;



      constructor(vitesseMax = 50, couleur = 'blanc', qteCarburant = 0, volReservoir = 0){
        this.vitesseMax = vitesseMax;
        this.couleur = couleur;
        this.quantiteCarburant = qteCarburant;
        this.volumeReservoir = volReservoir;


        console.log(`Nouvelle voiture créé !`);
        console.log(`Couleur : ${this.couleur}`);
        console.log(`Vitesse max : ${this.vitesseMax}km/h`);
        console.log(`Volume du réservoir : ${this.volumeReservoir}L`);
        console.log(`Quantité de carburant : ${this.quantiteCarburant}/${this.volumeReservoir}L`);    
        console.log("\n");
      }
      accelerer() {
        console.log('La voiture accélère !!');
      }
      freiner() {
        console.log('La voiture freine...');
      }
    }


    Et ajoutons ces nouveaux attributs dans la création de nos objets :

    let voiture1 = new Voiture(100, 'rouge', 10, 60);
    let voiture2 = new Voiture(130, 'noir', 20, 50);
    let voiture3 = new Voiture();


    Vous devriez avoir quelque chose comme cela dans votre console :

    attributs poo js

  • Les méthodes

    Si les attributs d'une classe sont ses variables, alors les méthodes sont ses fonctions. Comme nous l'avons vu dans notre exemple, une voiture n'est pas  seulement représentée par des données brutes, elle peux aussi effectuer des actions telles que :

    • accélérer
    • freiner


    Ces méthodes, définies dans la classe, vont permettre de décrire un comportement et une logique qui va pouvoir être exploitée par vos objets. Les méthodes fonctionnent à peu près de la même manière que les fonctions standards, à la différence qu'elles ne peuvent être utilisées seulement au travers des objets de la classe. Vous allez pouvoir écrire des blocs d'instructions à l'intérieur, leur envoyer des paramètres, etc... Certaines seront très simples (par exemple : afficher le niveau de carburant), d'autres auront des instructions plus complexes. 


    Dans notre exemple précédent, les méthodes sont très simples car elles affichent un simple console.log :

    accelerer() {
      console.log('La voiture accélère !!');
    }
    freiner() {
      console.log('La voiture freine...');
    }


    Créons une méthode qui va afficher les propriétés de notre objet :

    class Voiture {
      vitesseMax = 0;
      couleur = '';
      quantiteCarburant = 0;
      volumeReservoir = 0;


      constructor(vitesseMax = 50, couleur = 'blanc', qteCarburant = 0, volReservoir = 0) {
        this.vitesseMax = vitesseMax;
        this.couleur = couleur;
        this.quantiteCarburant = qteCarburant;
        this.volumeReservoir = volReservoir;
      }
      info() {
        console.log(`Nouvelle voiture créé !`);
        console.log(`Couleur : ${this.couleur}`);
        console.log(`Vitesse max : ${this.vitesseMax}km/h`);
        console.log(`Volume du réservoir : ${this.volumeReservoir}L`);
        console.log(`Quantité de carburant : ${this.quantiteCarburant}/${this.volumeReservoir}L`);
        console.log("\n");
      }

      accelerer() {
        console.log('La voiture accélère !!');
      }
      freiner() {
        console.log('La voiture freine...');
      }
    }


    Ici, nous avons simplement créé une nouvelle méthode à notre classe Voiture, qui va afficher les propriétés de l'objet qui s'en sert.


    Puis appelons la dans notre script :

    let voiture1 = new Voiture(100, 'rouge', 10, 60);
    let voiture2 = new Voiture(130, 'noir', 20, 50);
    let voiture3 = new Voiture();
    voiture1.info();
    voiture2.info();
    voiture3.info();

    A présent, nous pouvons utiliser la méthode info() au travers de nos différents objets Voiture. Remarquez l'opérateur "point" (.), qui permet d'accéder aussi aux méthodes. On rajoutera les parenthèses afin "d'exécuter" la méthode souhaitée, comme pour les fonctions.


    On peux aussi appeler des méthodes depuis l'intérieur de la classe :

    constructor(vitesseMax = 50, couleur = 'blanc', qteCarburant = 0, volReservoir = 0) {
      this.vitesseMax = vitesseMax;
      this.couleur = couleur;
      this.quantiteCarburant = qteCarburant;
      this.volumeReservoir = volReservoir;
      console.log(`Nouvelle voiture créé !`);
      this.info();
    }

    => avec cette nouvelle écriture, vous pouvez supprimer les appels à la méthode info() dans votre fichier script.


    Ce qui est intéressant avec les méthodes, c'est que vous pouvez agir sur les propriétés de votre objet, mais en inscrivant une logique précise. Prenons l'exemple d'une méthode "rajouter du carburant".

    Rajouter cette méthode à votre classe Voiture :

    rajouteCarburant(qte = 1){
      if(qte + this.quantiteCarburant > this.volumeReservoir){
        console.error(`Impossible de mettre ${qte}L dans le réservoir.`);
      } else {
        console.warn(`OK, on peux rajouter ${qte}L dans ce véhicule`);
        this.quantiteCarburant = this.quantiteCarburant + qte;
      }
      console.log(`Quantité de carburant actuelle : ${this.quantiteCarburant}/${this.volumeReservoir}L`);
      console.log(`Volume disponible : ${this.volumeReservoir - this.quantiteCarburant}L`);
    }

    Et copiez ça dans votre fichier script :

    let voiture1 = new Voiture(100, 'rouge', 0, 60);
    let voiture2 = new Voiture(130, 'noir', 0, 50);
    let voiture3 = new Voiture();
    voiture1.rajouteCarburant();

    On peut constater que la méthode rajouteCarburant() influe sur la valeur de la propriété quantiteCarburant de l'objet courant. Maintenant, modifiez les différents paramètres dans votre fichier script.js pour voir ce qui se passe dans les différents cas de figure ou vous pouvez appeler votre méthode rajouteCarburant(). L'idée, c'est de voir le comportement de la méthode rajouteCarburant() quand le réservoir a encore de la place et quand le réservoir est plein.

    Voici les deux cas de figures que vous devez générer via l'utilisation de votre méthode rajouteCarburant() :


    poo méthodes
    poo méthodes
  • Le mot clé this

    Le mot-clé this mérite une section à lui tout seul. L'idée est simple : quand vous instanciez un objet, les attributs de la classe deviennent les propriétés de l'objet. Et si vous instanciez 100 objets d'une même classe, vous aurez 100 fois les mêmes attributs reportés sur chaque objet. Le mot-clé this permet de spécifier à vos méthodes que vous souhaitez accéder ou modifier la propriété de l'objet en cours.

    Si vous souhaitez changer la couleur d'une voiture rouge, pour la faire devenir verte, vous ne souhaitez pas que toutes les voitures instanciés deviennent verte. Vous souhaitez modifier seulement cette voiture. D’où le mot clé this.

    Le mot-clé this est utilisé ultra fréquemment en programmation objet, car c'est lui qui va pouvoir vous permettre de faire des actions sur les objets séparément les un des autres, alors qu'ils partagent les mêmes méthodes et attributs. Le mot clé this s'utilise à l'intérieur de votre classe.


    Voici un exemple très standard du rôle du mot clé this dans notre classe Voiture :

    Fichier voiture.ts :

    class Voiture {
      vitesseMax = 0;
      couleur = '';
      accelerer() {
        console.log('La voiture accélère !!');
        console.warn(`Attention : vitesse limitée à ${this.vitesseMax}km/h`);
      }
      freiner() {
        console.log('La voiture freine...');
      }
    }

    Fichier script.js :

    let voiture1 = new Voiture();
    voiture1.vitesseMax = 130;
    voiture1.couleur = 'rouge';
    let voiture2 = new Voiture();
    voiture2.vitesseMax = 180;
    voiture2.couleur = 'noir';
    voiture1.accelerer();
    voiture2.accelerer();

    => Copiez ce code dans votre sandbox poo

    Dans cet exemple, vous pouvez constater que le message contenu dans le console.warn() de la méthode accelerer() va afficher des valeurs différentes. Normal, la voiture1 peut rouler jusqu'a 130km/h, tandis que la voiture2 peut rouler jusqu'à 180km/h.

    Ce qu'il faut retenir : le mot clé this représente l'objet "courant", celui qui en cours d'utilisation lors d'une instruction. Il permettra d'accéder aux propriétés de l'objet depuis l'intérieur de la classe, et peux avoir des valeurs différentes pour chaque objet. This permet donc de contextualiser l'accès aux attributs d'une classe, le contexte étant l'objet en cours.

  • Les opérateurs de visibilité

    Un des aspects très importants de la POO c'est ce que l'on appel la "visibilité" des attributs et méthodes.

    Lorsque vous créez une classe vous pouvez décider que certains attributs ou méthodes sont accessibles depuis divers endroits de votre code. Vous aurez certains éléments accessibles "depuis partout", et d'autres que vous ne souhaitez pas rendre accessible, pour diverses raisons.

    Ce raisonnement vous permet de sécuriser l'accès et la modification de vos données, ou bien l'exécution de certaines méthodes.

    Prenons par exemple la propriété "niveau de carburant" de notre objet "voiture". Vous ne souhaitez pas que cette valeur soit changée "à la volée" par un développeur peu scrupuleux. Il n'y a que deux manières de modifier cette valeur :

    • soit on fait le plein
    • soit on roule

    Interdiction absolue de changer le niveau de carburant en dehors de ces deux cas de figure. Donc pour remédier à ça, on va donner à l’attribut "niveau de carburant" une visibilité restreinte, qui le rends inaccessible, sauf aux méthodes autorisées (ici : "faire le plein" et "rouler").

    Le développeur dispose de trois niveau de visibilité, qui s'appliquent aux attributs et aux méthodes :
    visibilité en poo

    public : c'est la visibilité par défaut. Cela veux dire que l'élément est accessible depuis l'intérieur de la classe, depuis les enfants de la classe (héritage), et dans les instances de la classe (objets).

    protected : cela veux dire que l'élément est accessible depuis les enfants de la classe, et depuis la classe elle même. Mais pas depuis les instances.

    private : cela veux dire que l'élément est strictement réservé à un usage depuis l'intérieur de la classe. Il n'est accessible ni aux classe enfants, ni aux instances de la classe.



    Exemple :

    class Voiture {
      public vitesseMax = 0;
      public couleur = '';
      private _volumeReservoir = 0;
      private _quantiteCarburant = 0;
      constructor(vitesseMax = 50, couleur = 'blanc') {
        this.vitesseMax = vitesseMax;
        this.couleur = couleur;
        console.log(`Nouvelle voiture créé !`);
      }
      public volumeReservoir(vol) {
        if (vol > 0 && vol < 200) {
          this._volumeReservoir = vol;
        } else {
          console.error('Valeur impossible pour le volume du réservoir.')
        }
      }
      public quantiteCarburant(qte) {
        if (this._volumeReservoir > 0) {
          this._quantiteCarburant = qte;
        } else {
          console.error(`Le volume du réservoir n'a pas été déterminé.`);
        }
      }
      public info() {
        console.log(`Couleur : ${this.couleur}`);
        console.log(`Vitesse max : ${this.vitesseMax}km/h`);
        console.log(`Volume du réservoir : ${this._volumeReservoir}L`);
        console.log(`Quantité de carburant : ${this._quantiteCarburant}/${this._volumeReservoir}L`);
        console.log("\n");
      }
      
    }

    Copiez cette classe dans votre fichier voiture.ts, puis créez une nouvelle voiture et modifiez les propriétés quantiteCarburant et volumeReservoir. Faites ça dans le même fichier voiture.ts, après votre classe, afin que Typescript vous détecte des erreurs si il y en a. Utilisez la console pour voir le comportement de votre classe Voiture.

    Pour la visibilité "protected", nous verrons ça avec la notion d'héritage.

  • Les accesseurs et mutateurs

    poo encapsulationLes accesseurs et mutateurs, couramment appelés getters et setters, sont des méthodes "génériques" que l'on utilise très souvent en programmation objet. Théoriquement, une bonne partie de vos attributs doivent avoir une visibilité "private". Ce qui veux dire qu'il ne vous sera pas possible de lire leur valeur ou de la modifier en accédant directement à la propriété via un objet de cette classe. Et c'est plutôt une bonne chose, car on ne veux pas (toujours en théorie) que ces propriétés soient modifiés "à la volée" (sous entendu : sans passer par la logique de la classe).

    On va donc créer deux méthodes par propriétés, elles ne vont s'occuper que de lire ou modifier la propriété en question. En général, le getter ne fait que retourner la propriété, tout simplement. Le setter, quant à lui, peux effectuer plusieurs opérations de vérification - par exemple - avant d'autoriser un changement de valeur. Si vous avez compris la section sur les visibilités, vous déduirez que ces méthodes sont de type "public", car elles doivent être accessibles depuis vos objets. Le résultat : les attributs sont protégés, mais accessibles et modifiables.

    Avec Typescript, et en ES6 désormais, vous pouvez utiliser les mot clé get et set pour définir vos méthodes getters et vos setters. Quand on fais ça, généralement on place un underscore devant le nom des propriétés "private", et on nomme ces getters et ses setters avec le même nom, mais sans le underscore.

    Important : avec les getters et les setters, vous n'avez plus a appeler vos méthodes en rajoutant des parenthèses. Si vous voulez lire, vous appelez la méthode comme un attribut (ex : voiture.quantiteCarburant), et si vous souhaitez la modifier, vous mettez simplement le signe égal (ex : voiture.volumeReservoir = 60).


    Cas concret : pour reprendre l'exemple de la voiture, on pourrai dire que la notion de "niveau de carburant" illustre bien cela. On a d'un coté le "niveau de carburant", et de l'autre la "capacité du réservoir". Si notre réservoir fais 50L, on ne doit pas dépasser cette valeur quand on fait le plein. Si l'attribut "niveau de carburant" est directement accessible depuis l'extérieur de la classe, un développeur peut potentiellement mettre la valeur "100L". Ce qui devrait être impossible. Si on passe par un setter, il vérifiera la quantité de carburant dans le réservoir, la capacité du réservoir, et interdira tout dépassement de la valeur maximum. Le setter va donc remplir son rôle en protégeant l'intégrité des données dictée par la logique de votre classe. Un autre exemple : pas de valeur négative pour l'attribut "niveau de carburant".

    Exemple :

    class Voiture {

      public vitesseMax = 0;
      public couleur = '';
      private _volumeReservoir = 0;
      private _quantiteCarburant = 0;

      constructor(vitesseMax = 50, couleur = 'blanc') {
        this.vitesseMax = vitesseMax;
        this.couleur = couleur;
        console.log(`Nouvelle voiture créé !`);
      }
      get volumeReservoir() {
        return this._volumeReservoir;
      }
      set volumeReservoir(vol) {
        if (vol > 0 && vol < 200) {
          this._volumeReservoir = vol;
        } else {
          console.error('Valeur impossible pour le volume du réservoir.')
        }
      }
      get quantiteCarburant() {
        return this._quantiteCarburant;
      }
      set quantiteCarburant(qte) {
        if (this._volumeReservoir > 0 && (this._quantiteCarburant + qte <= this._volumeReservoir)) {
          this._quantiteCarburant = qte;
        } else {
          console.error(`Erreur sur la quantité de carburant.`);
        }
      }
      public info() {
        console.log(`Couleur : ${this.couleur}`);
        console.log(`Vitesse max : ${this.vitesseMax}km/h`);
        console.log(`Volume du réservoir : ${this._volumeReservoir}L`);
        console.log(`Quantité de carburant : ${this._quantiteCarburant}/${this._volumeReservoir}L`);
        console.log("\n");
      }
    }


    => Copiez la classe dans votre fichier voiture.ts, instanciez un nouvel objet Voiture, puis faites des test sur les valeurs volumeReservoir et quantiteCarburant.

  • Les attributs et méthodes statiques

    Comme nous l'avons vu auparavant, les attributs et méthodes d'une classe sont accessibles via les objets instanciés. Et chaque objet peux avoir des valeurs différentes (une voiture rouge, une voiture bleue, une voiture verte). Chaque valeur est propre à chaque objet (rouge, bleu, vert), et non à la classe elle même.

    Maintenant, imaginons que nous souhaitons stocker dans notre classe voiture le nombre d'objet voiture instancié par notre programme. Par exemple lors d'une course automobile, on veux savoir le nombre de voiture participant à cette course. Nous sommes d'accord que l’attribut "nombre de participant", n'est pas lié à chaque voiture, c'est un attribut "général" à la classe, et non spécifique à une voiture en particulier. Et bien la programmation objet a prévu ce cas de figure via l'utilisation des attributs et méthodes statiques grâce au mot-clé static.

    Un attribut statique aura une valeur partagé par tous les objets de la classe contrairement aux attributs standard :

    • chaque voiture a sa propre couleur (rouge, bleu, vert)
    • toutes les voitures ont accès au nombre de participants à la course (par exemple : 8)

    Et si on change la valeur de l'attribut, la valeur change pour tous les objets instanciés.

    La même logique est appliquée aux méthodes, à la différence qu'une méthode statique peut être appelée en dehors du contexte "objet". C'est à dire que vous pouvez appeler une méthode statique même si vous n'avez pas encore instancier d'objet sur cette classe. La méthode statique étant une méthode "de la classe", et non "de l'objet", elle sera  tout de même accessible.

    Important : pour accéder à un attribut ou une méthode statique, on utilisera le nom de la classe au lieu du this.

    Exemple :

    class Voiture {
      public vitesseMax = 0;
      public couleur = '';
      static _nombreDeVoiture = 0;
      constructor(vitesseMax = 50, couleur = 'blanc') {
        this.vitesseMax = vitesseMax;
        this.couleur = couleur;
        Voiture._nombreDeVoiture++;
        console.log(`Nouvelle voiture créé !`);
      }
      public static nombreDeVoiture() {
        console.log(`Il y a ${Voiture._nombreDeVoiture} dans notre programme.`)
      }
      public info() {
        console.log(`Couleur : ${this.couleur}`);
        console.log(`Vitesse max : ${this.vitesseMax}km/h`);
        console.log("\n");
      }
    }

    => Copiez ce code dans votre fichier, créez plusieurs objets voitures, puis afficher le nombre de voitures créées.

  • L'héritage


    L'héritage est un concept très intéressant qu'apporte la POO. Il vous permet de définir un lien de "parenté" entre deux classes, et donc de lier très fortement ces deux classes entre elles qui sont - à priori - différentes (sinon on aurait qu'une seule classe). Pour prendre l'exemple de la voiture, si je vous dit qu'une voiture est un véhicule, cela vous semble correct. Et malgré le fait qu'une voiture ne soit ni une moto, ni un camion, elle partage de nombreuses choses en communs avec eux. Dans le cas de la POO, on écrirait d'abord une classe "mère", la classe "Véhicule", puis on écrirait les trois classes "filles" : une classe voiture, une classe moto, et une classe camion.

    Dans cet exemple, la classe véhicule comprendrait des éléments du type :

    • nombre de roues
    • type de carburant
    • année de mise en service
    • démarrer le moteur

    Ces éléments sont en effet partagés par tous les véhicules. Donc toutes classes "fille" (voiture, camion, moto) va hériter des éléments (attributs ou méthodes) de la classe mère. En revanche, certaines propriétés de chaque classe "fille" vont être propres à chacune :

    • une voiture à un coffre, mais pas la moto ni le camion
    • une moto à un guidon, mais pas la voiture ni le camion
    • un camion a une remorque, mais pas la moto ni la voiture

    On va donc placer ces propriétés dans les classes filles. Au final, quand on instanciera une nouvelle voiture, on bénéficiera des éléments de la classe voiture, mais aussi ceux de la classe véhicule.

    Ce concept est très puisant car il vous permet une certaines justesse dans votre conception (de l'idée à la réalisation pratique), et offre une certaines granularité, sans pour autant avoir à redéfinir les éléments communs à toutes vos classes filles.

    Mots-clés :
    • extends : vous permet de définir une classe comme étant la fille d'une autre. (ex : class Voiture extends Vehicule)
    • super : ce mot-clé vous permet d'accéder aux attributs et méthodes d'une classe mère depuis votre classe fille

    On peux faire de l'héritage sur autant de "niveaux" que l'on souhaite. Par exemple on pourrait avoir des classes berline, citadine, 4x4, et monospace, qui héritent toutes de la classes voiture, qui hérite elle même de la classe véhicule. Donc pour garder l'image "familiale", vous pouvez créer autant de générations que vous le souhaitez.

    Vous pouvez aussi redéfinir ou surcharger certaines méthodes provenant des classes mères. Cela veux dire que vous aller modifier le comportement par défaut d'une méthode de votre classe mère à l'intérieur de votre classe fille, en rajoutant des instructions particulières. On pourrait imager ça de la façon suivante pour les véhicule : tous les véhicules ont une méthode "rouler", mais chaque type de véhicule à des restrictions :

    • la moto peux rouler sur des chemins, des routes ou des autoroutes
    • la voiture sur des routes ou des autoroutes
    • le camion seulement sur des autoroutes
    Et bien les méthodes "rouler" de chaque classe fille (héritées de la classe mère) vont être redéfinies différemment dans chaque classes, car elle doivent vérifier que le véhicule est sur le bon type de route avant de rouler.


  • Exercices

  • Notes

    https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Classes

    https://developer.mozilla.org/fr/docs/Learn/JavaScript/Objects/JS_orient%C3%A9-objet