• 10 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 10/02/2022

Gérez l'accès et l'encapsulation

Tout d'abord, un petit rappel. En POO, il y a deux parties bien distinctes :

  1. On crée des classes pour définir le fonctionnement des objets. C'est ce qu'on fait dans ce chapitre.

  2. On utilise des objets. C'est ce qu'on a fait au chapitre précédent.

Il faut bien distinguer ces deux parties car cela devient ici très important.

1. Création de la classe :

class Personnage
{
    // Méthodes
    void recevoirDegats(int nbDegats)
    {
 
    }
 
    void attaquer(Personnage &cible)
    {
 
    }
 
    void boirePotionDeVie(int quantitePotion)
    {
 
    }
 
    void changerArme(string nomNouvelleArme, int degatsNouvelleArme)
    {
 
    }
 
    bool estVivant()
    {
 
    }
 
    // Attributs
    int m_vie;
    int m_mana;
    string m_nomArme;
    int m_degatsArme;
};

2. Utilisation de l'objet :

int main()
{
    Personnage david, goliath;
    //Création de 2 objets de type Personnage : david et goliath
 
    goliath.attaquer(david); //goliath attaque david
    david.boirePotionDeVie(20); //david récupère 20 de vie en buvant une potion
    goliath.attaquer(david); //goliath attaque david
    david.attaquer(goliath); //david contre-attaque... c'est assez clair non ?
    
    goliath.changerArme("Double hache tranchante veneneuse de la mort", 40);
    goliath.attaquer(david);
 
 
    return 0;
}

Tenez, pourquoi n'essaierait-on pas ce code ? Allez, on met tout dans un même fichier, en prenant soin de définir la classe avant le main() :

#include <iostream>
#include <string>
 
using namespace std;
 
class Personnage
{
    // Méthodes
    void recevoirDegats(int nbDegats)
    {
 
    }
 
    void attaquer(Personnage &cible)
    {
 
    }
 
    void boirePotionDeVie(int quantitePotion)
    {
 
    }
 
    void changerArme(string nomNouvelleArme, int degatsNouvelleArme)
    {
 
    }
 
    bool estVivant()
    {
 
    }
 
    // Attributs
    int m_vie;
    int m_mana;
    string m_nomArme;
    int m_degatsArme;
};
 
int main()
{
    Personnage david, goliath;
    //Création de 2 objets de type Personnage : david et goliath
 
    goliath.attaquer(david);    //goliath attaque david
    david.boirePotionDeVie(20); //david récupère 20 de vie en buvant une potion
    goliath.attaquer(david);    //goliath attaque david
    david.attaquer(goliath);    //david contre-attaque... c'est assez clair non ?
    
    goliath.changerArme("Double hache tranchante veneneuse de la mort", 40);
    goliath.attaquer(david);
 
 
    return 0;
}

Compilez et admirez... la belle erreur de compilation !

Error : void Personnage::attaquer(Personnage&) is private within this context

Encore une insulte de la part du compilateur !

On en arrive justement au problème qui nous intéresse : celui des droits d'accès (oui, j'ai fait exprès de provoquer cette erreur de compilation ; vous ne pensiez tout de même pas que ce n'était pas prévu ?).

Gérez les droits d'accès

Il existe grosso modo deux droits d'accès différents :

  1. public  : l'attribut ou la méthode peut être appelé depuis l'extérieur de l'objet.

  2. private  : l'attribut ou la méthode ne peut pas être appelé depuis l'extérieur de l'objet. Par défaut, tous les éléments d'une classe sont private  .

Concrètement, qu'est-ce que cela signifie ? Qu'est-ce que "l'extérieur" de l'objet ?

Eh bien, dans notre exemple, "l'extérieur" c'est le main()  , là où on utilise l'objet.

On fait appel à des méthodes mais, comme elles sont par défaut privées, on ne peut pas les appeler depuis le main()  !

On va mettre en public toutes les méthodes, et en privé tous les attributs :

class Personnage
{
    // Tout ce qui suit est public (accessible depuis l'extérieur)
    public:
    
    void recevoirDegats(int nbDegats)
    {
 
    }
 
    void attaquer(Personnage &cible)
    {
 
    }
 
    void boirePotionDeVie(int quantitePotion)
    {
 
    }
 
    void changerArme(string nomNouvelleArme, int degatsNouvelleArme)
    {
 
    }
 
    bool estVivant()
    {
 
    }
 
    // Tout ce qui suit est prive (inaccessible depuis l'extérieur)
    private:
    
    int m_vie;
    int m_mana;
    string m_nomArme;
    int m_degatsArme;
};
  • Tout ce qui suit le mot-clé public:  est public, donc toutes nos méthodes sont publiques.

  • Tout ce qui suit le mot-clé private: est privé, donc tous nos attributs sont privés.

Voilà, vous pouvez maintenant compiler ce code, et vous verrez qu'il n'y a pas de problème (même si le code ne fait rien pour l'instant).

On appelle des méthodes depuis le main()  : comme elles sont publiques, on a le droit de le faire.

En revanche, nos attributs sont privés, ce qui veut dire qu'on n'a pas le droit de les modifier depuis le main()  . En clair, on ne peut pas écrire dans le main()  :

goliath.m_vie = 90;

Essayez, vous verrez que le compilateur vous ressort la même erreur que tout à l'heure.

Mais alors, on ne peut pas modifier la vie du personnage depuis le main()  ?

C'est ce qu'on appelle l'encapsulation.

Respectez le principe d'encapsulation

Si on mettait tout en public ? Les méthodes et les attributs, comme cela on peut tout modifier depuis le main()  et plus aucun problème ! Non ? Quoi j'ai dit une bêtise ?

Oh, trois fois rien. Vous venez juste de vous faire autant d'ennemis qu'il y a de programmeurs qui font de la POO dans le monde.

Vous vous souvenez de la métaphore du cube pour un objet ?

L'utilisation du code est simplifiée grâce à l'utilisation d'un objet

Le code à l'intérieur, ce sont les attributs.
Les boutons sur la façade avant, ce sont les méthodes.

Et là, pif paf pouf, vous devriez avoir tout compris d'un coup. En effet, le but du modèle objet est justement de masquer à l'utilisateur les informations complexes (les attributs) pour éviter qu'il ne fasse des bêtises avec.

Imaginez par exemple que l'utilisateur puisse modifier la vie... qu'est-ce qui l'empêcherait de mettre 150 de vie alors que la limite maximale est 100 ?

C'est pour cela qu'il faut toujours passer par des méthodes (des fonctions) qui vont d'abord vérifier qu'on fait les choses correctement avant de modifier les attributs. Cela garantit que le contenu de l'objet reste une "boîte noire". On ne sait pas comment cela fonctionne à l'intérieur quand on l'utilise, et c'est très bien ainsi. C'est une sécurité, cela permet d'éviter de faire péter tout le bazar à l'intérieur.

Séparez les prototypes et les définitions

Bon, on avance mais on n'a pas fini ! Voici ce que je voudrais qu'on fasse :

  1. Séparer les méthodes en prototypes et définitions dans deux fichiers différents, pour avoir un code plus modulaire.

  2. Implémenter les méthodes de la classe Personnage  (c'est-à-dire écrire le code à l'intérieur parce que, pour le moment, il n'y a rien).

À ce stade, notre classe figure dans le fichier main.cpp  , juste au-dessus du main()  . Et les méthodes sont directement écrites dans la définition de la classe. Cela fonctionne, mais c'est un peu bourrin.

Pour améliorer cela, il faut tout d'abord clairement séparer le main()  (qui se trouve dans main.cpp  ) des classes.

Pour chaque classe, on va créer :

  1. Un "header" (fichier *.hpp  ) qui contiendra les attributs et les prototypes de la classe.

  2. Un fichier source (fichier*.cpp  ) qui contiendra la définition des méthodes et leur implémentation.

Je vous propose d'ajouter à votre projet deux fichiers nommés très exactement :

  1. Personnage.hpp  .

  2. Personnage.cpp  .

1. Le fichier  Personnage.hpp

Le fichier .hpp va donc contenir la déclaration de la classe avec les attributs et les prototypes des méthodes. Dans notre cas, pour la classe Personnage  , nous obtenons :

#ifndef DEF_PERSONNAGE
#define DEF_PERSONNAGE

#include <string>

class Personnage
{
    public:

    void recevoirDegats(int nbDegats);
    void attaquer(Personnage &cible);
    void boirePotionDeVie(int quantitePotion);
    void changerArme(std::string nomNouvelleArme, int degatsNouvelleArme);
    bool estVivant();

    private:

    int m_vie;
    int m_mana;
    std::string m_nomArme; //Pas de using namespace std, il faut donc mettre std:: devant string
    int m_degatsArme;
};

#endif

Seuls les prototypes des méthodes figurent dans le .hpp  . C'est déjà beaucoup plus clair.

2. Le fichier  Personnage.cpp

Nous allons voir dans ce screencast comment définir l’ensemble des méthodes de la classe en respectant toutes les bonnes pratiques de développement. Vous êtes prêt ?

En résumé, voici le code complet de Personnage.cpp  :

#include "Personnage.hpp"

using namespace std;

void Personnage::recevoirDegats(int nbDegats)
{
    m_vie -= nbDegats;
    //On enlève le nombre de dégâts reçus à la vie du personnage

    if (m_vie < 0) //Pour éviter d'avoir une vie négative
    {
        m_vie = 0; //On met la vie à 0 (cela veut dire mort)
    }
}

void Personnage::attaquer(Personnage &cible)
{
    cible.recevoirDegats(m_degatsArme);
    //On inflige à la cible les dégâts que cause notre arme
}

void Personnage::boirePotionDeVie(int quantitePotion)
{
    m_vie += quantitePotion;

    if (m_vie > 100) //Interdiction de dépasser 100 de vie
    {
        m_vie = 100;
    }
}

void Personnage::changerArme(string nomNouvelleArme, int degatsNouvelleArme)
{
    m_nomArme = nomNouvelleArme;
    m_degatsArme = degatsNouvelleArme;
}

bool Personnage::estVivant()
{
    return m_vie > 0;
}

3. Le fichier  main.cpp

Retour au main()  . Première chose à ne pas oublier : inclure Personnage.hpp  pour pouvoir créer des objets de type Personnage  .

#include "Personnage.hpp" //Ne pas oublier

Le main() reste le même que tout à l'heure, on n'a pas besoin de le modifier. Au final, le code est donc très court et le fichier main.cpp ne fait qu'utiliser les objets :

#include <iostream>
#include "Personnage.hpp" //Ne pas oublier

using namespace std;

int main()
{
    Personnage david, goliath;
    //Création de 2 objets de type Personnage : david et goliath

    goliath.attaquer(david); //goliath attaque david
    david.boirePotionDeVie(20); //david récupère 20 de vie en buvant une potion
    goliath.attaquer(david); //goliath attaque david
    david.attaquer(goliath); //david contre-attaque... c'est assez clair non ? 
    goliath.changerArme("Double hache tranchante veneneuse de la mort", 40);
    goliath.attaquer(david);

    return 0;
}

Pour le moment, il faudra donc vous contenter de votre imagination. Essayez d'imaginer que David et Goliath sont bien en train de combattre (je ne veux pas vous gâcher la chute mais, normalement, c'est David qui gagne à la fin) !

En résumé

  • Les éléments qui constituent la classe peuvent être publics ou privés. S'ils sont publics, tout le monde peut les utiliser n'importe où dans le code. S'ils sont privés, seule la classe peut les utiliser.

  • En programmation orientée objet, on suit la règle d'encapsulation : on rend les attributs privés, afin d'obliger les autres développeurs à utiliser uniquement les méthodes.

Pourquoi initialiser un objet ? C’est justement ce qu’on va voir dans le chapitre suivant : les constructeurs et les destructeurs.

Exemple de certificat de réussite
Exemple de certificat de réussite