LOCODUINO

Le monde des objets

Le monde des objets (3)

Une sombre histoire d’héritage...

.
Par : Thierry

DIFFICULTÉ :

Attention : ce chapitre est long, ardu et demande de la concentration.
Ha ? Vous êtes encore là ? Alors lisez lentement, n’hésitez pas à revenir sur vos pas. Chaque mot est important, en particulier dans les lignes de code. Laissez les principes entrer et faire leur chemin, quitte à en rester là pour le moment et revenir plus tard reprendre la lecture...

Nous avons parlé des classes et de leur capacité à rendre un code souple simple à créer et à maintenir. Voyons maintenant les avantages du paradigme objet qui l’ont rendu incontournable.

L’héritage sans soucis

Depuis le deuxième article, nous avons vu une jolie classe Led. Mais comment ferions nous pour coder une classe LedBicouleur qui s’occuperait d’une diode à deux couleurs dotée de trois pins ? Ce type de diode est le plus souvent un assemblage de deux leds simples dans un seul boitier que l’on peut activer seules ou ensembles. Seule la cathode est partagée.

La première idée, c’est du simple copié/collé avec une petite adaptation.

#define HIGH2   2       // Pour allumer la pin2
#define HIGH12  3       // Pour allumer les deux pins !
 
class LedBicouleur 
{
private:
  byte pin1;
  byte pin2;
  byte etat;  // HIGH, LOW, HIGH2 et même HIGH12 !
  bool montageInverse;
  void Rafraichir();
  
public:
  LedBicouleur (byte aPin1, byte apin2, aMontageInverse = false);
  void Setup();
  void Allumer();
  void Allumer2();
  void Allumer12();
  void Eteindre();
};

Qu’est ce qui a changé ici ? Une seconde broche pin2 a été ajoutée. La variable etat peut prendre quatre valeurs. HIGH, LOW, et deux inventions locales, HIGH2 pour allumer la pin 2, HIGH12 pour tout allumer ! Le constructeur reflète cette nouvelle organisation en ajoutant la seconde broche. Enfin, les méthodes Allumer2() et Allumer12() ont été ajoutées. Allumer() allume la première diode, Allumer2() la seconde, Allumer12() allume les deux, et Eteindre() éteint le tout.

On sent bien que c’est quand même très proche de la classe Led vue dans l’article précédent... Nous allons exploiter une nouvelle et très importante notion du C++ : l’héritage (on parle aussi de dérivation). Ce principe permet de dire qu’un objet est une sorte de un autre objet... Ici, la led bi-couleur est bien une sorte de led. On parle également de dérivation LedBicouleur serait la classe dérivée de Led. Voici comment exprimer l’héritage en code C++ :

class Led // l'originale, la classe de base
{
...
};
 
class LedBicouleur : public Led // LedBicouleur dérive de Led
{
...
};

LedBicouleur hérite ou dérive de Led. Le mot clé public devant le nom de la classe de base, celle dont dérive LedBicouleur, est là pour lui permettre de voir le contenu de sa classe de base Led, en tout cas la partie que Led a jugé bon de ne pas déclarer privé.

Ce qui est fait, est fait !

Mais pourquoi faire une classe dérivée ? Est ce que la classe LedBicouleur ne pouvait pas exister sans dérivation ? Si, bien sûr, mais l’héritage permet de donner des comportements communs à un ensemble de classes, de capitaliser sur ce qui est déjà fait et fiabilisé, de diminuer le volume de code tout en rationalisant le fonctionnement et la maintenance.

class LedBicouleur : public Led
{
private
  byte pin2;
 
public:
  LedBicouleur(byte aPin1, byte aPin2, bool aMontageInverse = false);
  void Allumer2();
  void Allumer12();
};

Cette led particulière ayant deux pins différentes à piloter, il faut en ajouter une à celle déjà présente dans la classe de base, c’est pin2, et l’initialiser dans le constructeur :

LedBicouleur::LedBicouleur(byte aPin1, byte aPin2, bool aMontageInverse) : Led(aPin1, aMontageInverse)
{
  pin2 = aPin2;
  pinMode(pin2, OUTPUT);
 
  Eteindre();
}

Le C++ permet de faire appel au constructeur de sa classe de base Led en lui redonnant les bons arguments, et se contente de ne faire que la partie supplémentaire autour de pin2.

Pourtant ce constructeur ne va pas fonctionner correctement. Voyez vous ce qui ne va pas ? Eteindre() n’est présent que dans Led, et pas dans LedBicouleur, ce qui fait que cette méthode ne connait pas pin2 et donc n’éteindra pas cette broche... Nous devons faire en sorte qu’Eteindre() marche pour les deux classes.

Dans le code de la méthode Led::Eteindre(), on a :

void Led::Eteindre()
[
  etat = LOW;
  Rafraichir();
}

Dans la nouvelle classe, on a dit dès le départ que etat = LOW éteindrait les deux broches. C’est donc Rafraichir() qui doit faire ce travail. Mais cette méthode existe déjà dans Led, comment en faire une nouvelle dans LedBicouleur qui mette à jour les deux broches ? Pour cela, il y a la surcharge de méthode.

class Led
{
private:
  byte pin;
  byte etat;
  bool montageInverse;
  virtual void Rafraichir();
 
public:
  Led(byte aPin, bool aMontageInverse = false);
  void Allumer();
  void Eteindre();
};

class LedBicouleur : public Led
{
private:
  byte pin2;
  void Rafraichir();
 
public:
  LedBicouleur(byte aPin1, byte aPin2, bool aMontageInverse = false);
  void Allumer2();
};

Dans la classe Led, est apparu le mot clé virtual qui déclare la fonction Rafraichir() virtuelle. Ce terme désigne une fonction qui dépend du type réel de la classe qui l’applique. Si c’est une Led, on utilisera Led::Rafraichir(), si c’est une LebBicouleur on utilisera LedBicouleur::Rafraichir() si elle existe, sinon on retrouvera la méthode de sa classe de base : Led::Rafraichir(). En effet, virtual ouvre une porte et permet aux classes dérivées de fournir un comportement alternatif, mais ce n’est pas une obligation ! Dans LedBicouleur, j’ai effectivement redéfini, surchargé Rafraichir(). Attention, le nom, le type de retour et les arguments doivent être exactement les mêmes, sinon le compilateur considérera qu’il s’agit d’une autre méthode, mais sans prévenir. Codons la nouvelle fonction :

void LedBicouleur::Rafraichir()
{
  byte etatLed1 = LOW;
  byte etatLed2 = LOW;
 
  switch(etat)
  {
    case HIGH:
      etatLed1 = HIGH;
      break;
      
    case HIGH2:
      etatLed2 = HIGH;
      break;
      
    case HIGH12:
      etatLed1 = HIGH;
      etatLed2 = HIGH;
      break;
  }
 
  if (montageInverse == true)  // si c'est un montage inversé
  {
    etatLed1 = ! etatLed1; // on inverse
    etatLed2 = ! etatLed2; // on inverse
  }
 
  digitalWrite(pin, etatLed1);
  digitalWrite(pin2, etatLed2);
}

Dans cette fonction, on crée deux états etatLed1 et etatLed2, un pour chaque couleur de la led. Les deux sont initialisés à LOW. Puis avec le switch, chaque état est mis à HIGH en fonction de ce qui est demandé. On inverse si nécessaire en fonction du flag montageInverse, et on fini par appliquer l’état sur chaque led. Cette fonction remaniée va nous permettre de coder Allumer2() et Allumer12() assez facilement :

void LedBicouleur::Allumer2()
{
  etat = HIGH2;
  Rafraichir();
}

void LedBicouleur::Allumer12()
{
  etat = HIGH12;
  Rafraichir();
}

Et là, nouveau problème. Les données pin et etat de la classe Led sont privées (private). LedBicouleur n’a pas le droit de les lire et encore moins de les changer ! Pour que ce code puisse fonctionner, elles peuvent rester cachées au yeux de tous, mais doivent devenir visibles pour les classes dérivées. C’est le rôle du modificateur protected (protégé) qui remplace private et ne laisse les données et les fonctions visibles que pour les classes dérivées.

class Led
{
protected:
  byte pin;
  byte etat;
  bool montageInverse;
  virtual void Rafraichir();
 
public:
  Led(byte aPin, bool aMontageInverse);
  void Allumer();
  void Eteindre();
};

Utiliser cette nouvelle classe est à peine plus compliqué qu’une led simple :

LedBicouleur maLed(10, 11);

void setup()
{
  maLed.Setup();
}

void loop()
{
  maLed.Allumer();
  delay(1000);
  maled.Eteindre();
  maLed.Allumer2();
  delay(1000);
  maLed.Eteindre();
  delay(1000);
}

Les plus perspicaces d’entre vous auront remarqué que, pour l’instant, rien ne justifie l’emploi de l’héritage par rapport à un objet LedBicouleur qui contiendrait brutalement deux Led ! C’est d’ailleurs un excellent exercice que je vous enjoins de coder pour le plaisir...

Nous verrons dans le prochain article quel immenses avantages l’on peut retirer de l’héritage comme méthode de programmation, plutôt que la force brute...

5 Messages

  • Le monde des objets (3) 8 mars 2015 17:25, par SavignyExpress

    Bonjour à tous,

    Je rebondis sur le dernier paragraphe de cet article, par ailleurs excellent !

    Je partage le point de vue d’une classe LedBicouleur codée comme une agrégation de 2 objets de la classe Led plutôt que par héritage.

    De mon point de vue, l’héritage ne devrait s’appliquer que si lorsque l’on manipule un objet de la classe Led, on pouvait sans autre lui substituer un objet de la classe dérivée LedBicouleur (principe de substitution de Liskov).

    Répondre

    • Le monde des objets (3) 9 mars 2015 22:03, par Jean-Luc

      Bonsoir,

      mais on peut tout à fait substituer un objet de la classe LedBicouleur à un objet de la classe Led. Seule une des deux DEL sera manipulée via Allumer et Eteindre.

      Répondre

  • Le monde des objets (3) 24 mars 2015 13:26, par ffayolle

    Bonjour,
    Merci pour ces articles très intéressants sur les objets
    Une question : comment intégrer dans une boucle une phase de test faisant appel à x objets identiques ?
    Je m’explique :
    if (L1.Change()) je fais quelque chose à L1
    if (L2.Change()) je fais la même chose à L2
    ....
    if (L10.Change()) je fais la même chose à L10
    J’espère avoir été clair !!!
    Fabrice

    Répondre

    • Le monde des objets (3) 24 mars 2015 15:54, par Jean-Luc

      Bonjour,

      On peut faire ça avec un tableau d’objets :

      Led mesLEDS[10]; // 10 Leds

      Le constructeur ne peut pas avoir d’argument. Il faudra initialiser chaque objet séparément.

      On peut ensuite écrire :

      for (byte numero = 0; numero < 10; numero++) {
        if (mesLEDS[numero].Change()) {
          ...
        }
      }

      Il existe un autre moyen en utilisant une liste chaînée qui est remplie au fur et à mesure que l’on instancie les objets. On peut ensuite exploiter cette liste. C’est un peu plus compliqué à expliquer ici mais c’est plus élégant.

      Répondre

  • Le monde des objets (3) 24 mars 2021 10:20, par fred

    Bonjour,

    Un grand MERCI pour cet article en quatre volumes sur les objets du C++.
    Je commence à y voir plus clair : il me reste à mettre la main à la pâte pour tenter de saisir toutes les subtilités.

    Pour la petite histoire, ce "monde de l’Objet en programmation", je m’y étais plongé avec le turbo pascal (oui, ça fait loin derrière : windows XP n’existait pas encore !) et je n’y avais pas compris grand chose.
    Mais avec Arduino et Teensy, je m’y suis remis à grand peine pour tenter de comprendre comment utiliser les librairies Adafruit et PJRC et éventuellement pour en créer, car certains programmes refusent de fonctionner tel celui qui est censé afficher sur le LCD une image stockée sur la carte SD (embarquée sur le dit LCD) le tout piloté par un Teensy 4.0.

    J’ai du boulot ;o)

    Encore merci.

    Répondre

Réagissez à « Le monde des objets (3) »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Programmation »

Les derniers articles

Les articles les plus lus