Après les classes aiguille et zone on va aborder la classe signal ou plutôt les classes signaux. Les signaux sont très polymorphes (il y a beaucoup de variétés) et leur mode de fonctionnement est très varié. L’héritage, et le polymorphisme qui en découle, vont permettre de prendre en compte relativement facilement les variantes rencontrées dans la signalisation SNCF réelle. Cela va permettre aussi de monter les mécanismes du polymorphisme, et il y en a de très subtils. Il ne faut pas s’inquiéter, ces mécanismes du polymorphisme font, automatiquement, ce qu’on aimerait bien que cela fasse et même au delà des espérances.
Un gestionnaire en C++ pour votre réseau (2)
Signaux
. Par :
. URL : https://www.locoduino.org/spip.php?article167On distinguera ici deux catégories de signaux, ceux sans BAL (Bloc Automatique Lumineux) et ceux avec BAL.
Pour pouvoir mélanger des signaux avec et sans BAL (utile pour les réseaux ayant des parties avec BAL et des parties sans) le fonctionnement des deux catégories sera identique, on aura juste des méthodes pour ouvrir ou fermer les signaux, tout le reste sera automatique, c’est à dire que le calcul des feux et la présentation des cibles pour les signaux mécaniques sera fait automatiquement en fonction de l’environnement (autres signaux, aiguilles, zones, …). L’aubinage est aussi pris en compte. L’aubinage est la fermeture automatique d’un signal lors de son franchissement par un train (le nom vient de l’invention, par Mr Aubine, de la pédale mécanique qui fermait les signaux mécaniques au passage d’un train, et le nom est resté pour les signaux lumineux) .
Des objets signaux sont nécessaires dans le gestionnaire pour assurer la sécurité, c’est par leur biais que les arrêts et ralentissements des trains sont gérés. Par contre les signaux réels ne sont là que pour faire joli, mais ils sont pris en compte (aussi bien les lumineux que les mécaniques).
Les signaux lumineux peuvent présenter différents feux, en voici quelques uns (avec les abréviations utilisées) :
- voie libre (Vl) : feu vert
- avertissement (A) : feu jaune
- sémaphore (S) : feu rouge
- carré (C) : deux feux rouges
- carré violet (Cv) : feu violet
- manœuvre (M) : feu blanc
- ralentissement (R) : deux feux jaunes horizontaux
- rappel de ralentissement (RR) : deux feux jaunes verticaux
- disque (D) : un feu jaune et un feu rouge
Certains feux peuvent êtres clignotants (Vlc,Ac,Sc,Mc,Rc,RRc). Certains signaux peuvent aussi présenter un petit feu blanc appelé œilleton.
Les signaux mécaniques peuvent présenter différentes cibles, en voici quelques unes (avec les abréviations utilisées) :
En plus des cibles, les signaux mécaniques présentent aussi des feux ; quand aucune cible n’est présentée le feu est à voie libre (Vl).
Signaux sans BAL
Commençons par la première catégorie, les signaux sans BAL. Il y a pas mal de variété dans cette catégorie, voici une liste non exhaustive (avec les feux) :
- carre violet (Cv M)
- avertissement (A Vl)
- avertissement avec ralentissement (A Vl R)
- carré (C Vl)
- carré avec avertissement (C A Vl)
- carre avec rappel de ralentissement (C Vl RR)
- carre avec avertissement et rappel de ralentissement (C A Vl RR)
- sémaphore (S Vl)
- disque (D Vl)
- …
Ces signaux pouvant êtres mécaniques ou lumineux. Les carrés avec « avertissement » réalisent l’avertissement du carré suivant.
On va détailler ici les classes pour quelques signaux manuels (on en trouvera d’autres dans les fichiers joints). Ces signaux sont judicieusement choisis pour illustrer les mécanismes de l’héritage et du polymorphisme, tout en montrant la facilité de mise en oeuvre (je n’ose imaginer comment on ferait en programmation classique !).
Comme pour la classe zone
on va écrire une classe signal
qui regroupe tout ce qui est commun à tous les signaux. Il faut commencer par coder les feux, il y a beaucoup de feux différents. Une façon bien pratique est d’écrire une énumération, en s’arrangeant pour que chaque feu soit codé sur un bit, le numéro de chaque feu est alors une puissance de 2 (1, 2, 4, 8, 16, 32, 64, …) qui peut aussi s’écrire en utilisant l’opérateur de décalage à gauche (1<<0, 1<<1, 1<<2, 1<<3, 1<<4, …), ceci pour faciliter l’utilisation :
enum { E=0, // eteint
Vl=1<<0, A=1<<1, S=1<<2, C=1<<3, R=1<<4, RR=1<<5, M=1<<6, Cv=1<<7,
Vlc=1<<8, Ac=1<<9, Sc=1<<10, Rc=1<<11, RRc=1<<12, Mc=1<<13, D=1<<14, X=1<<15
};
Il est prévu presque tous les feux (SNCF), même les clignotants (avec un petit c). Il y a 15 feux possibles plus un libre (X), cela tient dans un type int
. Pour la portabilité on déclare un type spécial sur 16 bits donc un int
pour l’Arduino :
typedef unsigned int typeFeux; // 16 cas possibles ( 32 avec un long )
Outre les feux présentés par le signal, on a besoin d’un entier pour l’interface avec l’électronique (commande des feux et/ou des moteurs), plus en option un nom (pour la mise au point) et un type (pour la mise au point et pour distinguer éventuellement les mécaniques des lumineux, ou d’autres usages). Cela donne :
class Signal {
typeFeux feux; // les feux
int no; // numero du signal
char* nom; // le nom du signal OPTION
int type; // le type du signal OPTION
};
Il faut ensuite une méthode d’accès et des méthodes de test :
typeFeux getFeux() { return feux; } // obtention des feux
boolean ferme() { return feux&(S|C|Cv); } // test signal ferme
boolean ouvert() { return !ferme(); } // test signal ouvert
Il faut aussi des méthodes pour ouvrir et fermer les signaux ainsi que les aubiner :
virtual void ouvrir() {} // ouverture manuelle
virtual void fermer() {} // fermeture manuelle
virtual void aubiner() { fermer(); } // fermeture automatique (retrosignalisation)
Ces méthodes seront redéfinies (si besoin) dans les classes des signaux particuliers (d’où le mot clé virtual
).
Pour gérer les ralentissements (à 30 ou 60km/h ) il faut des méthodes :
virtual boolean ralentissement30() { return false; } // true si ralentissement a 30
virtual boolean ralentissement60() { return false; } // true si ralentissement a 60
Ces méthodes seront redéfinies (si besoin) dans les classes des signaux particuliers.
Pour assurer l’automatisme du calcul des feux, il faut pouvoir, en général, accéder aux signaux précédents et suivants. Il faut aussi, quand un signal est manœuvré (manuellement ou automatiquement), qu’il avertisse le précédent pour qu’il mette à jour ses feux, c’est le rôle de la méthode majFeux()
(Mise A Jour Feux) :
virtual Signal* suivant() { return NULL; } // le signal suivant
virtual Signal* precedent() { return NULL; } // le signal precedent
virtual void majFeux() {} // appelee par le signal suivant
Ces méthodes seront redéfinies (si besoin) dans les classes signaux particulières.
Pour finir il faut une méthode pour manœuvrer le signal, cette méthode fait l’interface avec l’électronique (leds, moteurs, …). Elle doit être complétée par l’utilisateur (en utilisant la variable no) :
void manoeuvrer() { … } // manoeuvre du signal
Cette méthode peut aussi gérer la zone d’arrêt (si elle existe).
La classe Signal
comporte aussi quelques méthodes utilitaires et des méthodes en option.
Voici la classe Signal
complète :
typedef unsigned int typeFeux; // 16 cas possibles ( 32 avec long )
// les feux possibles
enum { E=0, // eteint
Vl=1<<0, A=1<<1, S=1<<2, C=1<<3, R=1<<4, RR=1<<5, M=1<<6, Cv=1<<7,
Vlc=1<<8, Ac=1<<9, Sc=1<<10, Rc=1<<11, RRc=1<<12, Mc=1<<13, D=1<<14, X=1<<15
};
enum {MECANIQUE,LUMINEUX,BAS,BAL, ... }; // des types de signaux
class Signal {
typeFeux feux;
int no; // numero du signal
char* nom; // le nom du signal OPTION
int type; // le type du signal OPTION
Signal(typeFeux f,int n) { feux=f; no=n; } // constructeur mini
Signal(int t,typeFeux f,int n) { feux=f; no=n; } // constructeur
Signal(char* nm,int t,typeFeux f,int n) { nom=nm; type=t; feux=f; no=n; } // constructeur maxi
typeFeux getFeux() { return feux; } // methode d'acces
boolean ferme() { return feux&(S|C|Cv); } // methode de test
boolean ouvert() { return !ferme(); } // methode de test
virtual void ouvrir() {} // ouverture manuelle
virtual void fermer() {} // fermeture manuelle
virtual void aubiner() { fermer(); } // fermeture automatique (retrosignalisation)
void manoeuvrer() { ... } // manoeuvre effective du signal
virtual boolean ralentissement30() { return false; } // true si ralentissement a 30
virtual boolean ralentissement60() { return false; } // true si ralentissement a 60
virtual void majFeux() {} // mise a jour du signal precedent
virtual Signal* suivant() { return NULL; } // le signal suivant
virtual Signal* precedent() { return NULL; } // le signal precedent
Signal* selonAiguille(Aiguille* a,Signal* s1,Signal* s2) { return a->directe()?s1:s2; }
};
La méthode utilitaire selonAiguille()
facilite le choix des signaux suivants. On trouvera aussi dans les fichiers joints des méthodes pour gérer la présentation des cibles pour les signaux mécaniques.
Quelques signaux sans BAL
Avec cette classe de base, on peut maintenant écrire quelques classe de signaux particuliers.
Carré violet
Commençons par un carré violet, c’est le signal le plus simple car il ne dépend pas d’autres signaux :
class CarreViolet:Signal {
CarreViolet(int n):Signal(Cv,n) {} // constructeur mini
CarreViolet(char* nm,int t,int n):Signal(nm,t,Cv,n) {} // constructeur maxi
virtual void ouvrir() { feux=M; manoeuvrer(); }
virtual void fermer() { feux=Cv; manoeuvrer(); }
};
Cette classe hérite de la classe Signal
. Deux constructeurs sont proposés, un constructeur minimum (juste ce qui est indispensable) et un constructeur maximum (avec les options : nom et type), l’utilisateur peut adapter les constructeurs en fonction de ses besoins.
Les méthodes redéfinies ouvrir()
et fermer()
modifient l’état du signal en deux phases :
- calcul des feux (ici c’est facile)
- manœuvre du signal (changement des allumages de leds et/ou commande des moteurs, contrôle de la zone d’arrêt).
En pratique (voir le fichier joint) un test pour savoir si le signal est déjà dans la bonne position est ajouté.
Cette classe peut être instanciée directement, pour chaque carré violet nécessaire :
Signal* cv3=new CarreViolet("Cv3",BAS,1); // constructeur maxi
Avertissement
Le signal d’avertissement est un peu plus compliqué car il dépend du signal suivant qui doit être un carré ou un sémaphore. Conformément aux choix qui ont été faits, le fonctionnement de ce signal est entièrement automatique, il est lié à son carré ou sémaphore, il n’y a donc pas de méthode de manœuvre manuelle (ouvrir()
ou fermer()
). Le signal peut être lumineux ou mécanique, il peut être aubiné :
class Avertissement:Signal {
Avertissement(int n):Signal(A,n) {} // constructeur mini
Avertissement(char* nm,int t,int n):Signal(nm,t,A,n) {} // constructeur maxi
virtual void aubiner() { // appelé par les méthodes actions de zone
feux=A;
manoeuvrer();
}
virtual void majFeux() { // appelé par le signal suivant
if (suivant()->ferme()) feu=A;
else feu=Vl;
manoeuvrer();
}
};
On retrouve deux variantes du constructeur, une méthode aubiner()
(fermeture automatique). La méthode majFeux()
est nouvelle, elle est appelée par le signal suivant lors d’un changement de celui-ci, fait le calcul des feux et la manœuvre du signal (leds et/ou moteur).
En pratique (voir le fichier joint), un test est effectué sur le suivant, s’il est NULL une erreur est déclenchée, c’est à l’utilisateur d’afficher un message. Ce test est indispensable pour éviter d’inextricables erreurs à l’exécution. Un test est aussi effectué pour n’appeler la méthode manoeuvrer()
que si c’est nécessaire.
Pour utiliser cette classe, comme on le fait pour la classe Zone
, il faut écrire une nouvelle classe héritant de la classe Avertissement
et redéfinir la méthode suivant()
, l’écriture d’un ou plusieurs constructeurs est aussi nécessaire. Exemple :
class A09: Avertissement { // attention aux pins analogiques A0, A1, A2, …
A09(int n):Avertissement(n) {} // constructeur mini
A09(char* nm,int t,int n):Avertissement(nm,t,n) {} // constructeur maxi
virtual Signal* suivant() { return c2; }
};
Exemple d’utilisation :
Signal* a9=new A09("A9",MECANIQUE,11);
Carré
Le signal carré est simple :
class Carre:Signal {
Carre(int n):Signal(C,n) {} // constructeur mini
Carre(char* nm,int t,int n):Signal(nm,t+CARRE,C,n) {} // constructeur maxi
virtual void ouvrir() { feux=Vl; manoeuvrer(); precedent()->majFeux(); }
virtual void fermer() { feux=C; manoeuvrer(); precedent()->majFeux(); }
};
Outre deux variantes de constructeurs, il y a deux méthodes pour manœuvrer manuellement le carré, ouvrir()
et fermer()
. Ces deux méthodes font la manœuvre du signal d’avertissement précédent en appelant la méthode majFeux()
sur l’avertissement.
En pratique, comme pour la classe précédente, des tests sont ajoutés pour éviter des manœuvres inutiles ou des pointeurs NULL. Une méthode utilitaire (de la classe Signal
) manoeuvreEtMajFeux()
est utilisée pour simplifier les écritures.
Pour utiliser cette classe, toujours la même technique, écrire une classe qui hérite de la classe Carre
et qui redéfinit la méthode precedent()
. Exemple :
class C2:Carre {
C2(int n):Carre(n) {} // constructeur mini
C2(char* nm,int t,int n):Carre(nm,t,n) {} // constructeur maxi
virtual Signal* precedent() { return a9; }
};
Exemple d’utilisation :
Signal* c2=new C2(3);
Carré avec rappel de ralentissement
Le carré avec rappel de ralentissement est une variante de carré. Tout naturellement, en programmation objet, on écrit une classe qui hérite de Carre
et qui ne redéfinit que les méthodes nécessaires, ici c’est la méthode ouvrir()
:
class CarreRappelRalentissement:Carre {
CarreRappelRalentissement(int n):Carre(n) {} // constructeur mini
CarreRappelRalentissement(char* nm,int t,int n):Carre(nm,t,n) {} // constructeur maxi
virtual void ouvrir() {
if (ralentissement60()) feux=RRc; else
if (ralentissement30()) feux=RR;
else feux=Vl;
manoeuvrer();
precedent()->majFeux();
}
};
La méthode ouvrir()
fait le calcul des feux, les méthodes booléennes ralentissement60()
et ralentissement30()
sont héritées de Signal
, elles retournent false
(par défaut). Comme pour la classe Carre
, pour utiliser la classe CarreRappelRalentissement
il faut écrire une nouvelle classe qui redéfinit la méthode precedent()
et qui redéfinit une des méthodes ralentissement60()
et ralentissement30()
(ou les deux). Exemple :
class C3:CarreRappelRalentissement {
C3(int n):CarreRappelRalentissement(n) {} // constructeur mini
C3(char* nm,int t,int n):CarreRappelRalentissement(nm,t,n) {} // constructeur maxi
virtual Signal* precedent() { return c2; }
virtual boolean ralentissement30() { return aig1->deviee(); } // si l'aig1 est deviee
};
Exemple d’utilisation :
Signal* c3=new C3(5);
Arbitrairement l’information de ralentissement est attachée au carré rappel de ralentissement. Bien évidemment le ralentissement sera affiché sur le signal précédent qui doit pouvoir présenter le ralentissement (avertissement avec ralentissement ou carre avec ralentissement).
On trouvera dans le fichier signaux.h les SNCF les plus courants sans BAL.
Signaux avec BAL
La deuxième catégorie est celle des signaux avec BAL, ce sont uniquement des signaux lumineux, pouvant tous présenter le sémaphore, l’avertissement et la voie libre. Voici un liste non exhaustive (avec les feux) :
- sémaphore (S A Vl)
- sémaphore avec ralentissement (S A Vl R)
- carré (C S A Vl)
- carre avec ralentissement (C S A Vl R)
- carre avec rappel de ralentissement (C S A Vl RR)
- …
On trouve aussi souvent des carrés pouvant présenter un feu de manœuvre (M) qui ne sont pas prévus ici mais que l’on pourrait facilement prendre en compte.
Pour ne pas alourdir la classe Signal
une classe SignalBAL
héritant de la classe Signal
est écrite, cette classe comprend ce qui est spécifique aux signaux de BAL :
class SignalBAL:Signal {
boolean modePermanent=false; // ouverture permanente
SignalBAL(typeFeux f,int n):Signal(f,n) {} // constructeur mini
SignalBAL(int t,typeFeux f,int n):Signal(t,f,n) {} // constructeur
SignalBAL(char* nm,int t,typeFeux f,int n):Signal(nm,t,f,n) {} // constructeur maxi
virtual void aubiner() {} // fermeture automatique par retrosignalisation
virtual void desaubiner() {} // ouverture automatique par retrosignalisation
virtual typeFeux calculFeux() { return E; } // calcul des feux
void setPermanent(boolean b) { modePermanent=b; } // ouverture permanente
virtual boolean cantonOccupe() { return false; } // occupation du "canton"
};
Le gestion automatique des signaux se fait par l’appel des méthodes aubiner()
et desaubiner()
. Ces méthodes sont appelées par la rétrosignalisation. La méthode cantonOccupe()
permet de tester l’occupation du "canton".
Pour pouvoir utiliser les méthodes de SignalBAL
il faut déclarer les signaux de BAL du type SignalBAL
et non Signal
, sinon il y aura des erreurs de compilation.
Sémaphore
Commençons par le plus simple, le sémaphore de BAL. Le fonctionnement de ce signal est entièrement automatique sans aucune action manuelle, il n’y a donc pas de méthode ouvrir()
ou fermer()
, la fermeture et l’ouverture du signal se font par la rétrosignalisation, par le biais des méthodes aubiner()
et desaubiner()
. Le calcul des feux devant être fait dans deux méthodes, une méthode annexe calculFeux()
est introduite :
class SemaphoreBAL:SignalBAL {
SemaphoreBAL(int n):Signal(Vl,n) {} // constructeur mini
SemaphoreBAL(char* nm,int t,int n):Signal(nm,t+BAL,Vl,n) {} // constructeur maxi
virtual void aubiner() {// appelé par les methodes actions de zone
feu=S; manoeuvrer(); precedent()->maj();
}
virtual void desaubiner() { // appelé par les methodes des actions de zone
feux=calculFeux(); manoeuvrer(); precedent()->maj();
}
virtual typeFeux calculFeux() { // feux a l'ouverture
if (cantonOccupe()) return S; // "canton" occupe ( precaution )
if (suivant()->ferme()) return A; else return Vl;
}
void majFeux() { Signal* s; typeFeux f; // appelé par le signal suivant
feux=calculFeux(); manoeuvrer(); precedent()->maj(); }
}
virtual boolean cantonOccupe() { return true; } // test d'occupation du "canton"
};
Normalement la fermeture du signal se fait lors de l’occupation de la zone suivant le signal par la méthode aubiner()
et l’ouverture se fait lors de la libération de la dernière zone du "canton" par la méthode desaubiner()
. C’est la rétrosignalisation qui contrôle cela, suivant le cheminement suivant :
De même pour l’ouverture :
Pour palier les anomalies de circulation de nos trains (ruptures d’attelages, mise ou retrait de matériels sur une zone sans précautions, …), une vérification de l’occupation du "canton" est faite par le biais de la méthode cantonOccupe()
.
Comme d’habitude, pour un signal particulier il faut écrire une classe qui hérite de SemaphoreBAL
, et qui redéfinit les méthodes nécessaires :
class S5:SemaphoreBAL{
S5(int n):SemaphoreBAL(n) {} // constructeur mini
S5(char* nm,int t,int n):SemaphoreBAL(nm,t,n) {} // constructeur maxi
virtual Signal* suivant() { return c3; }
virtual Signal* precedent() { return c2; }
virtual boolean cantonOccupe() { return occupees(z8,z9); }
};
Exemple d’utilisation :
Signal* s5=new S5("S5",BAL,3);
Carré
Passons, comme avant dernier exemple, à un carré avec BAL. Cela peut sembler curieux à certains, mais c’est nécessaire dans les postes d’aiguillage du type PRS (Poste tout Relais à transit Souple) et dans le cas d’itinéraires permanents. Le fonctionnement de tels carrés est le suivant :
- à l’ouverture (manuelle), calcul des feux comme pour un sémaphore de BAL (S possible)
- à la fermeture (manuelle), mise au carré
- à l’aubinage (automatique par occupation du "canton"), si l’ouverture est permanente, mise au sémaphore sinon mise au carré
- au désaubinage (automatique par libération du "canton"), si l’ouverture est permanente, calcul des feux comme pour l’ouverture sinon rien à faire
On retrouve tout ce qu’il y avait dans sémaphore avec BAL, plus les ouvertures et fermetures manuelles et la gestion du mode permanent :
class CarreBAL:Signal {
CarreBAL(int n):Signal(C,n) {} // constructeur mini
CarreBAL(char* nm,int t,int n):Signal(nm,t+BAL,C,n) {} // constructeur maxi
virtual void ouvrir() { // ouverture manuelle
feux=calculFeux(); manoeuvrer(); precedent()->maj();
}
virtual void fermer() { // fermeture manuelle
feux=C; manoeuvrer(); precedent()->maj();
}
virtual void aubiner() { // fermeture automatique (retrosignalisation)
if (modePermanent) feux=S; else feux=C;
manoeuvrer(); precedent()->maj();
}
virtual void desaubiner() { // ouverture automatique (retrosignalisation)
if (modePermanent) feux=calculFeux(); else return;
manoeuvrer(); precedent()->maj();
}
virtual typeFeux calculFeux() { // feux a l'ouverture
if (cantonOccupe()) return S; // "canton" occupe ( precaution )
if (suivant()->ferme()) return A; else return Vl;
}
virtual void majFeux() { // appelé par le signal suivant
if (ferme()) return;
feux=calculFeux();
manoeuvrer();
precedent()->maj();
}
};
Comme d’habitude il faudra écrire une classe, héritant de CarreBAL
, pour chaque signal particulier.
Carre rappel de ralentissement
Pour finir en beauté, un carré avec BAL et rappel de ralentissement, qui, bien évidemment, hérite de CarreBAL
:
class CarreBALRappelRalentissement: CarreBAL {
CarreBALRappelRalentissement(int n):CarreBAL(n) {} // constructeur mini
CarreBALRappelRalentissement(char* nm,int t,int n):CarreBAL(nm,t+BAL,n) {} // constructeur maxi
virtual typeFeux calculFeux() { // feux a l'ouverture
if (ralentissement60()) if (suivant()->ferme()) return A+RRc; else return RRc; else
if (ralentissement30()) if (suivant()->ferme()) return A+RR; else return RR;
else if (suivant()s->ferme()) return A; else return Vl;
}
};
Seule la méthode calculFeux()
doit être redéfinie. Un exemple complexe de signal particulier, pour bien montrer toutes les possibilités :
class C6:CarreBALRappelRalentissement {
C6(int n):CarreBALRappelRalentissement(n) {} // constructeur mini
C6(char* nm,int t,int n):CarreBALRappelRalentissement(nm,t,n) {} // constructeur maxi
virtual Signal* suivant() { return selonAiguille(aig2,c2,c3); } // selon aig2
virtual Signal* precedent() { return c2; }
virtual boolean cantonOccupe() { // canton dynamique
if (aig1->directe()) return occupee(z10);
return occupees(z8,z9);
}
virtual boolean ralentissement60() { return aig1->deviee(); } // si aig1 deviee
virtual boolean ralentissement30() { return aig1->directe() && aig2->deviee(); } // si aig1 directe et aig2 deviee
};
Exemple d’utilisation :
SignalBAL* c6=new C6(5);
La méthode cantonOccupe()
montre que l’on peut avoir des "cantons" dynamiques, les zones qui le composent dépendent de la position d’aiguilles. On peut aussi avoir un ralentissement à 30 et/ou à 60 suivant la position des aiguilles.
Polymorphisme
Il est intéressant ici de regarder, sur ce dernier exemple, comment fonctionnent les mécanismes objet. Regardons pour commencer les relations d’héritages :
-
C6
hérite deCarreBALRappelRalentissement
qui hérite deCarreBAL
qui hérite deSignalBAL
qui hérite deSignal
Ces relations d’héritage constituent une branche de l’arbre formé par toutes les relations d’héritage. Voici l’arbre des signaux de cet article :
Les informaticiens ont l’habitude de représenter leurs arbres à l’envers, la racine est en haut, les feuilles sont en bas, la branche qui nous intéresse est la plus à droite.
Polymorphisme d’affectation
Premier constat, lors de l’instanciation du signal, la variable c6
est déclarée de type pointeur de SignalBAL
et on lui affecte une instance d’une classe de type pointeur de CarreBALRappelRalentissement
. On devrait plutôt écrire, pour respecter les types :
C6* c6=new C6(5);
Si, au niveau des déclarations c’est plutôt utilisé par "fainéantise", le mécanisme est essentiel dans les classes. Si on regarde bien les classes zones et signaux, dans les méthodes on a des paramètres ou des résultats de type Signal*
et lors des appels de méthodes, dans les exemples, il n’y a jamais de pointeur de Signal
(la classe n’est pas instanciable), mais ce sont toujours des pointeurs sur des classes héritant de Signal
. C’est essentiel, parce que cela permet de faire des actions sur les signaux sans se préoccuper du type réel du signal, tous les signaux fonctionnent globalement de la même façon, c’est là toute la puissance de la programmation objet.
On peut affecter à un pointeur sur une classe de base, un pointeur sur une classe dérivée (par héritage).
Polymorphisme de méthode
Plus subtil, regardons ce qui se passe à l’exécution quand on ouvre le signal avec, par exemple, l’instruction :
c6->ouvrir();
Le type de c6
est pointeur de C6
, classe qui n’a pas de méthode ouvrir()
, on regarde alors dans la classe dont elle hérite CarreBALRappelRalentissement
qui n’a pas de méthode ouvrir()
non plus, on regarde alors dans la classe dont elle hérite CarreBAL
, qui elle a une méthode ouvrir()
. C’est celle là qui est utilisée, cette méthode fait plusieurs choses :
ouvrir() { feux=calculFeux(); precedent()->maj(); manoeuvrer(); }
Détaillons :
- Premièrement, quand la méthode
calculFeux()
est appelée, on est dans la méthodeouvrir()
deCarreBAL
, classe qui a une méthodecalculFeux()
, mais ce n’est pas celle là qui est utilisée car le type réel de la variablec6
estC6
qui n’a pas de méthodecalculFeux()
, mais la classe dont elle hériteCarreBALRappelRalentissement
a une méthodecalculFeux()
, donc c’est celle qui est utilisée. C’est très subtil comme mécanisme, l’exécution de la méthodecalculFeux()
va utiliser les mêmes mécanismes et ainsi de suite (on ne détaillera pas plus, ce serait vite inextricable).
En résumé bien qu’étant dans la classe
CarreBAL
, on n’utilise pas sa méthodecalculFeux()
(qui ne gère pas le rappel de ralentissement) mais bien la méthode decalculFeux()
deCarreBALRappelRalentissement
(qui elle gère le rappel de ralentissement) et c’est bien cela que l’on veut.
- Deuxièmement, quand la méthode
precedent()
est appelée, elle existe dansC6
, donc c’est celle là qui est utilisée.
- Troisièmement, quand la méthode
majFeux()
est appelée, comme elle n’existe pas dansC6
, ni dansCarreBALRappelRalentissement
, mais existe dansCarréBAL
, donc c’est celle là qui est utilisée.
- Finalement elle appelle la méthode
manoeuvrer()
qui n’existe pas dansC6
, ni dansCarreBALRappelRalentissement
, ni dansCarreBAL
, ni dansSignalBAL
, mais qui existe dansSignal
, donc c’est celle là qui est utilisée.
C’est inextricable, je me suis trompé plusieurs fois en écrivant l’article, et on n’a pas regardé, pour simplifier, ce qui se passait dans les méthodes appelées dans les méthodes.
En pratique on écrit en respectant les règles simples de la programmation objet et c’est C++ qui fait ce qu’il faut à l’exécution, et il ne se trompe pas, lui. Pour bénéficier de ce mécanisme il suffit de mettre le mot clé virtual devant la méthode.
Si la méthode est virtuelle alors le choix de la méthode est fait à l’exécution en fonction du type réel de l’objet, sinon le choix est fait à la compilation en fonction du type déclaré de l’objet.
Avec ce mécanisme, dit de liaison dynamique ou liaison à l’exécution, tout se passe comme si à l’exécution, on partait du type réel de l’objet sur lequel la méthode est appliquée, et on recherchait dans l’arbre d’héritage en partant du type réel de l’objet, en remontant la branche vers la racine, la première occurrence de la méthode : c’est cette méthode qui est utilisée. On est sûr de trouver une méthode, car sinon il y aurait une erreur de compilation (en pratique le C++ utilise des tables de méthodes virtuelles).
Ce deuxième mécanisme de la programmation objet, complémentaire du premier, bien que plus subtil que le premier, est tout aussi essentiel et très puissant.
Bilan
Ces classes signaux sont assez compliquées, mais ce n’est pas la faute de la programmation objet, mais celle de la réglementation de la signalisation SNCF. A contrario, la programmation objet permet de prendre en compte relativement facilement toutes les contraintes de la signalisation.
Bien évidemment d’autres signaux sont possibles, français comme étrangers. Si je connais bien les subtilités de la signalisation française, je ne connais pas les signalisations étrangères et j’aurais du mal à écrire les classes correspondantes.
Les classes signaux sont assez compliquées et elles interfèrent beaucoup entre elles, certaines ont été testées (avec le locodrome), mais toutes n’ont pas été testées, il peut dont rester des erreurs.
Avec les classes aiguilles et zones, vues dans l’article précédent, et les classes signaux de cet article on a tout ce qu’il faut pour mettre en oeuvre facilement des itinéraires. Ce sera le sujet du prochain article et un exemple complet de mise en oeuvre sera aussi présenté (avec le locodrome).
Comme dans l’article précédent on trouvera dans le fichier ci-dessous toutes les classes bien écrites en C++, d’autres classes de signaux et d’autres exemples. Les classes comportent aussi des méthodes optionnelles (test de clignotement des feux, cibles à présenter, …).
Pour les signaux il y a quatre fichiers :
- un fichier Signal.h qui contient la classe
Signal
- un fichier Signaux.h qui contient des signaux sans BAL
- un fichier SignalBAL.h qui contient la classe
SignalBAL
- un fichier SignauxBAL.h qui contient des signaux avec BAL
Dans le prochain article, je terminerai avec les classes itineraire
et une mise en oeuvre dans l’exemple du Locodrome.
En attendant relisez bien les articles 1 et 2 et rejoignez nous sur le forum :
Modélisation logicielle d’un réseau - le système de Pierre59