58. Héritage ▲
L'héritage définit un type plus spécialisé depuis un type plus général servant de type de base.
Le type spécialisé acquiert les membres du type de base et par conséquent peut lui être substitué. L'héritage est disponible pour les classes, pas pour les structures. La classe héritant d'une autre classe est appelée une sous-classe, et la classe héritée s'appelle une superclasse, aussi appelée classe de base. Il y a deux types d'héritage en langage D. Nous couvrirons dans ce chapitre l'héritage d'implémentation et laisserons l'héritage d'interface à un chapitre ultérieur. Lors de la définition d'une sous-classe, la superclasse est spécifiée après un deux-points :
class
SousClasse : SuperClasse {
// ...
}
Pour voir un autre exemple, supposons qu'il y a déjà une classe représentant une horloge :
class
Horloge {
int
heure;
int
minute;
int
seconde;
void
ajustage
(
int
heure, int
minute, int
seconde =
0
) {
this
.heure =
heure;
this
.minute =
minute;
this
.seconde =
seconde;
}
}
Apparemment, les membres de cette classe n'ont pas besoin de valeurs spéciales pendant leur construction. Il n'y a donc pas de constructeur. Les membres sont positionnés par la fonction membre ajustage() :
auto
deskClock =
new
Horloge;
deskClock.adjust
(
20
, 30
);
writefln
(
"%02s:%02s:%02s"
,
deskClock.heure, deskClock.minute, deskClock.seconde);
Il serait plus utile de produire la chaîne d'heure par la fonction toString(). Elle sera ajoutée plus tard lors de l'explication sur la surcharge.
La sortie :
00
:00
:00
Avec si peu de fonctionnalité, Horloge pourrait aussi bien être une structure, et selon les besoins du programme, cela pourrait être suffisant. Cependant, le fait que ce soit une classe permettra d'hériter d'Horloge. Pour voir un exemple d'héritage, considérons une classe alarme_horloge, qui inclut toutes les fonctionnalités d'Horloge, mais fournit aussi un moyen de régler une alarme. Commençons par définir ce type de classe sans s'occuper d'Horloge. Si nous faisions cela, nous devrions avoir à inclure les mêmes trois membres et la fonction ajustage(). alarme_horloge devrait aussi avoir d'autres membres pour ses fonctionnalités additionnelles.
class
alarme_horloge {
int
heure;
int
minute;
int
seconde;
int
heure_alarme;
int
minute_alarme;
void
adjustage
(
int
heure, int
minute, int
seconde =
0
) {
this
.heure =
heure;
this
.minute =
minute;
this
.seconde =
seconde;
}
void
ajustage_alarme
(
int
heure, int
minute) {
heure_alarme =
heure;
minute_alarme =
minute;
}
}
L'héritage est utile dans ces cas. Faire hériter alarme_horloge depuis Horloge simplifie la nouvelle classe et réduit la duplication de code :
class
alarme_horloge : Horloge {
int
heure_alarme;
int
minute_alarme;
void
ajustage_alarme
(
int
heure, int
minute) {
heure_alarme =
heure;
minute_alarme =
minute;
}
}
Cette nouvelle définition est équivalente à la première.
Comme alarme_horloge hérite des membres de Horloge, la classe peut être utilisée de la même façon qu'Horloge :
auto
horloge_de_chevet =
new
alarme_horloge;
horloge_de_chevet.ajustage
(
20
, 30
);
horloge_de_chevet.ajustage_alarm
(
7
, 0
);
Les membres qui ont été hérités de la superclasse peuvent être accédés comme s‘ils étaient membres de la sous-classe :
writefln
(
"%02s:%02s:%02s ♫%02s:%02s"
,
horloge_de_chevet.heure,
horloge_de_chevet.minute,
horloge_de_chevet.seconde,
horloge_de_chevet.heure_alarme,
horloge_de_chevet.minute_alarme);
La sortie :
20
:30
:00
♫07
:00
Une fonction AlarmClock.toString serait plus utile dans ce cas. Elle sera définie plus tard.
L'héritage utilisé dans cet exemple est l'héritage implémentation.
Si nous imaginons la mémoire comme un ruban allant de haut en bas, le placement des membres de horloge_alarme en mémoire peut être représenté comme ci-dessous ;
|
L'illustration ci-dessus sert juste à donner une idée de comment les membres d'une superclasse et d'une sous-classe peuvent être combinés ensemble. La couche actuelle des membres dépend des détails d'implémentation du compilateur utilisé. Par exemple, la partie marquée autres données inclut typiquement le pointeur vers la table des fonctions virtuelles (vtbl) du type particulier de cette classe. Les détails de la couche objet sont en dehors du périmètre de ce livre.
58-1. Attention: héritage seulement si « c'est un »▲
Nous avons vu que l'héritage d'implémentation consiste à acquérir des membres. Considérez ce type d'héritage seulement si le sous-type peut être considéré comme une sorte de supertype, comme dans l'expression « l'alarme est une horloge ». «C'est un » n'est pas la seule relation entre les types, une relation plus commune est «a un» . Par exemple, considérons que nous voulons ajouter le concept de batterie à la classe Horloge. Il ne serait pas approprié d'ajouter Batterie à Horloge par héritage, car la déclaration «une horloge est une batterie» n'est pas vraie.
class
Horloge : Batterie {
// ← MAUVAIS DESIGN
// ...
}
Une horloge n'est pas une batterie, elle a une batterie. Quand il y a une telle relation de conteneur, le type contenu doit être défini comme un membre du type qui le contient.
class
Horloge {
Batterie batterie; // ← design correct
// ...
}
58-2. Héritage d'au plus une classe▲
Les classes peuvent seulement hériter d'une classe de base (qui peut elle-même potentiellement hériter d'une classe de base). En d'autres termes, l'héritage multiple n'est pas supporté en langage D. Supposons par exemple qu'il y ait aussi une classe émettrice_son, et bien qu' «une alarme horloge est un objet émetteur de son» soit également vrai, il n'est pas possible d'hériter de Horloge et de Emetteur_son :
class
Emetteur_son {
// ...
}
class
Horloge_alarme : Horloge, Emetteur_son {
// ← erreur de compilation
// ...
}
Par contre, il n'y a pas de limite au nombre d'interfaces dont une classe peut hériter. Nous allons voir le mot clé Iinterface dans un prochain chapitre.
Additionnellement, il n'y a pas de limite à la profondeur de hiérarchie d'héritage :
class
InstrumentDeMusique {
// ...
}
class
InstrumentACorde : InstrumentDeMusique {
// ...
}
class
Violon : InstrumentACorde {
// ...
}
La hiérarchie d'héritage ci-dessus définit une ration entre l'élément le plus général : l'instrument de musique, en passant par l'instrument à cordes, puis le violon.
58-3. Tableaux hiérarchiques▲
Les types liés par la relation « est un » forment une hiérarchie de classe.
Selon les conventions OOP, les hiérarchies de classe sont représentées par les superclasses en haut et la sous-classe en bas. Les relations d'héritage sont indiquées par les flèches pointant des sous-classes aux superclasses.
Par exemple, le code suivant peut être une hiérarchie d'instruments de musique :
|
58-4. Accéder aux membres d'une superclasse▲
Le mot clé super permet de faire référence aux membres hérités de la superclasse.
class
AlarmeHorloge : Horloge {
// ...
void
foo
(
) {
super
.minute =
10
; // Le membre 'minute' hérité
minute =
10
; // Même chose s'il n'y a pas d'ambiguïté
}
}
Le mot clé super n'est pas tout le temps nécessaire ; minute seul a la même signification dans le code ci-dessus. Le mot clé super est requis quand la superclasse et la sous-classe ont des membres avec le même nom. Nous verrons cela ci-dessous quand nous aurons besoin d'écrire super.reset() et super.toString(). Si de multiples classes d'un arbre d'héritage définissent un symbole avec le même nom, on peut utiliser le nom spécifique de la classe pour supprimer l'ambiguïté entre les symboles :
class
Device {
string fabricant;
}
class
Horloge : Device {
string fabricant;
}
class
AlarmeHorloge : Horloge {
// ...
void
foo
(
) {
Device. fabricant =
"Sunny Horology, Inc."
;
Horloge. fabricant =
"Better Watches, Ltd."
;
}
}
58-5. Construire des membres de superclasse▲
L'autre usage du mot clé super est d'appeler le constructeur de la superclasse. C'est similaire à appeler les constructeurs surchargés de la classe courante : this lors de l'appel aux constructeurs de la classe courante et super pour les constructeurs de la superclasse.
Il n'est pas nécessaire d'appeler le constructeur de la superclasse explicitement, si le constructeur de la sous-classe fait un appel explicite à une surcharge de super, ce constructeur est donc exécuté par cet appel. Sinon, et si la superclasse a un constructeur par défaut, il est exécuté automatiquement avant d'entrer dans le corps de la sous-classe.
Nous n'avons pas encore défini de constructeur pour les classes Horloge et AlarmeHorloge. Pour cette raison, les membres de ces deux classes sont initialisés par les valeurs .init de leur type respectif, qui est 0 pour les int.
Supposons que Horloge ait le constructeur suivant :
class
Horloge {
this
(
int
heure, int
minute, int
seconde) {
this
.heure =
heure;
this
.minute =
minute;
this
.seconde =
seconde;
}
// ...
}
Ce constructeur doit être utilisé lors de la construction des objets Horloge :
auto
horloge =
new
Horloge
(
17
, 15
, 0
);
Naturellement, les programmeurs qui utilisent le type Horloge directement devraient utiliser cette syntaxe. Cependant, lors de la construction d'un objet AlarmeHorloge, ils ne peuvent pas construire cette partie d'Horloge séparément. En outre, les utilisateurs d'AlarmeHorloge n'ont même pas besoin de savoir qu'ils héritent d‘Horloge.
Un utilisateur d'AlarmeHorloge devrait simplement construire un objet AlarmeHorloge et l'utiliser dans le programme sans avoir à prêter attention à son héritage d‘Horloge :
auto
bedSideClock =
new
AlarmeHorloge
(
/* ... */
);
// ... utilisation comme un objet AlarmeHorloge ...
Pour cette raison, la construction de la superclasse est de la responsabilité de la sous-classe. La sous-classe appelle le constructeur de la superclasse avec la syntaxe super() :
class
AlarmeHorloge : Horloge {
this
(
int
heure, int
minute, int
seconde, // pour les membres d'Horloge
int
heureAlarme, int
minuteAlarme) {
//pour les membres d' AlarmeHorloge
super
(
heure, minute, seconde);
this
. heureAlarme =
heureAlarme;
this
. minuteAlarme =
minuteAlarme;
}
// ...
}
Le constructeur d'AlarmeHorloge prend comme arguments ses propres membres et les membres de la superclasse, puis les utilise pour construire sa partie superclasse.
58-6. Surcharger les définitions des fonctions membres▲
Un des bénéfices de l'héritage est la capacité à redéfinir des fonctions membres de la superclasse dans la sous-classe. Ceci est appelé la surcharge : la définition existante de la superclasse est écrasée par la sous-classe avec le mot clé override .
Les fonctions surchargeables sont appelées fonctions virtuelles. Les fonctions virtuelles sont implémentées par le compilateur à travers les tables de pointeurs de fonctions virtuelles (vtbl) et les pointeurs vtbl. Les détails de ce mécanisme sont hors périmètre de ce livre. Cependant, il doit être connu par chaque programmeur système que les appels aux fonctions virtuelles sont plus coûteux que les appels aux fonctions régulières. En D, chaque fonction membre de classe non privée est virtuelle par défaut. Pour cette raison, quand une fonction de superclasse n'a aucune raison d'être surchargée, elle devrait être définie comme finale, elle sera ainsi non virtuelle. Nous verrons le mot clé final plus tard dans le chapitre interfaces.
Supposons que Horloge a une fonction membre utilisée pour remettre tous ses membres à zéro :
class
Horloge {
void
reset
(
) {
heure =
0
;
minute =
0
;
seconde =
0
;
}
// ...
}
Cette fonction est héritée d' AlarmeHorloge et peut être appelée sur un objet AlarmeHorloge :
auto
bedSideClock =
new
AlarmeHorloge
(
20
, 30
, 0
, 7
, 0
);
// ...
bedSideClock.reset
(
);
Cependant, en ignorant nécessairement les membres d' AlarmeHorloge et Horloge, reset peut seulement remettre à zéro ses propres membres. Pour cette raison, pour remettre aussi bien à zéro des membres de sous-classes, reset() doit être surchargée :
class
AlarmeHorloge : Horloge {
override
void
reset
(
) {
super
.reset
(
);
alarmeHeure =
0
;
alarmeMinute =
0
;
}
// ...
}
La sous-classe remet seulement à zéro ses propres membres et délègue le reste de la tâche à Horloge par l'appel à super.reset(). Notez qu'écrire seulement reset() ne devrait pas fonctionner, car cela appellerait la fonction reset() d'AlarmeHorloge, Appeler reset() en interne pourrait causer une récursion infinie.
La raison pour laquelle j'ai retardé la définition de toString() jusqu'à maintenant est que pour les classes, cette fonction doit être définie par le mot clé override. Comme nous le verrons dans le prochain chapitre, chaque classe hérite automatiquement d'une superclasse Object qui définit déjà la fonction membre toString().
Pour cette raison, la fonction membre toString() pour les classes doit être définie en utilisant le mot clé override :
import
std.string;
class
Horloge {
override
string toString
(
) const
{
return
format
(
"%02s:%02s:%02s"
, heure, minute, seconde);
}
// ...
}
class
AlarmeHorloge: Horloge {
override
string toString
(
) const
{
return
format
(
"%s ♫%02s:%02s"
, super
.toString
(
),
alarmeHeure, alarmeMinute);
}
// ...
}
Notez que AlarmeHorloge délègue encore certaines tâches à Horloge par l'appel à super.toString().
Ces deux surcharges de toString() permettent la conversion des objets AlarmeHorloge en chaînes :
void
main
(
) {
auto
HorlogedeBureau =
new
AlarmeHorloge
(
10
, 15
, 0
, 6
, 45
);
writeln
(
HorlogedeBureau);
}
La sortie :
10
:15
:00
♫06
:45
58-7. Utiliser les sous-classes à la place de la superclasse▲
Comme la superclasse est plus générale, et la sous-classe plus spécialisée, les objets de sous-classes peuvent être utilisés quand un type d'objet de la superclasse est requis. Cela s'appelle le polymorphisme. Les concepts de types généraux et spécialisés peuvent être vus dans les déclarations comme «ce type est de ce type » : «l'alarme d'horloge est une horloge», «l'étudiant est une personne», «le chat est un animal», etc. En conséquence, une alarme d'horloge peut être utilisée là où une horloge est nécessaire, un étudiant peut être utilisé là où une personne est nécessaire, et un chat peut être utilisé là où un animal est nécessaire. Quand un objet sous-classe est utilisé comme un objet superclasse, il ne perd pas son propre
type spécialisé. Cela est similaire aux exemples de la vraie vie : utiliser une alarme d'horloge simplement comme une horloge ne change pas le fait que c'est une alarme d'horloge. Elle se comporterait toujours comme un réveil. Supposons qu'une fonction prenne un objet Horloge en paramètre, lequel est remis à zéro à un certain endroit durant son exécution :
void
use
(
Horloge horloge) {
// ...
horloge.reset
(
);
// ...
}
Le polymorphisme rend possible l'envoi d'un AlarmeHOrloge à une telle fonction :
auto
HorlogedeBureau =
new
AlarmeHorloge
(
10
, 15
, 0
, 6
, 45
);
writeln
(
"Avant : "
, HorlogedeBureau);
use
(
HorlogedeBureau);
writeln
(
"Après : "
, HorlogedeBureau);
C'est en accord avec la relation « une alarme d'horloge est une horloge » comme résultat, les membres de l'objet HorlogedeBureau sont remis à zéro :
Avant : 10
:15
:00
♫06
:45
Après : 00
:00
:00
♫00
:00
L'observation importante ici est que non seulement les membres de Horloge , mais aussi les membres de AlarmeHorloge ont été réinitialisés.
Bien que use() appelle reset() sur l'objet Horloge, comme l'objet actuel est un objet AlarmeHorloge, la fonction qui est appelée est AlarmeHorloge.reset. Selon sa définition plus haut, AlarmeHorologe.reset remet à zéro les membres des deux classes Horloge et AlarmeHorloge.
En d'autres termes, bien que use() utilise l'objet comme une Horloge, l'objet réel peut être un type hérité qui se comporte de manière particulière.
Ajoutons une autre classe à la hiérarchie de Horloge. La fonction reset() de ce type positionne ses membres à des valeurs aléatoires :
import
std.random;
class
horlogeCassee : Horloge {
this
(
) {
super
(
0
, 0
, 0
);
}
override
void
reset
(
) {
heure =
uniform
(
0
, 24
);
minute =
uniform
(
0
, 60
);
seconde =
uniform
(
0
, 60
);
}
}
Quand un objet de HorlogeCassee est envoyé à use(), alors la fonction spéciale reset() de HorlogeCassee devrait être appelée. Encore une fois, bien qu'il soit passé comme un objet Horloge, l'actuel objet est toujours un objet HorlogeCassee :
auto
shelfClock =
new
HorlogeCassee;
use
(
shelfClock);
writeln
(
shelfClock);
La sortie des valeurs de temps aléatoires en résultat de remise à zéro de HorlogeCassee :
00
:00
:00
58-8. L'héritage est transitif▲
Le polymorphisme n'est pas juste limité à deux classes. Les sous-classes de sous-classes peuvent aussi être utilisées à la place de toute superclasse dans la hiérarchie.
Considérons la hiérarchie InstrumendeMusique :
class
InstrumentdeMusique {
// ...
}
class
InstrumentACorde : InstrumentdeMusique {
// ...
}
class
Violon : InstrumentACorde {
// ...
}
Les héritages ci-dessus construisent les relations suivantes : « Un instrument à cordes est un instrument de musique », et « un violon est un instrument à cordes ». Par contre, il est aussi vrai de dire : « le violon est un instrument de musique ». En conséquence, un objet Violon peut être utilisé à la place d'un objet IntrumentdeMusique.
Supposons que tout le code supporté ci-dessous ait été aussi défini :
void
playInTune
(
InstrumentdeMusique instrument,
MorceaudeMusique mrceau) {
instrument.tune
(
);
instrument.play
(
morceau);
}
// ...
auto
monViolon =
new
Violon;
playInTune
(
myViolin, improvisation);
Bien que playInTune() attende un InstrumentdeMusique, il va être appelé avec un objet Violon à cause de la relation « un violon est un instrument de musique ».
L'héritage peut être aussi profond que nécessaire.
58-9. Fonctions et classes abstraites▲
Parfois, il y a des fonctions membres qu'il est naturel de faire apparaître dans une interface de classe, même si cette classe ne peut pas fournir sa définition. Quand il n'y a pas de définition concrète d'une fonction membre, cette fonction est une fonction membre abstraite. Une classe qui a au moins une fonction abstraite est une classe abstraite.
Par exemple, la superclasse PieceEchec dans une hiérarchie devrait avoir un membre estValide() qui détermine si un mouvement donné est valide pour ce type de pièce. Comme la validité d'un mouvement dépend du type actuel de la pièce d'échec, la classe générale PieceEchec ne peut prendre la décision elle-même. Les mouvements valides peuvent seulement être connus par la sous-classe comme le pion, le roi, etc.
Le mot clé Abstract spécifie que la classe héritée doit implémenter une telle méthode elle-même :
class
PieceEchec {
abstract
bool isValid
(
in
Square from, in
Square to);
}
Il n'est pas possible de construire des objets de classes abstraites :
auto
piece =
new
PieceEchec; // ← erreur compilation
La sous-classe devrait avoir à surcharger et implémenter toutes les fonctions abstraites afin que la classe soit abstraite et donc constructible :
class
Pion : PieceEchec {
override
bool isValid
(
in
Square from, in
Square to) {
// ... implémentation de isValid pour pion ...
return
decision;
}
}
Il est maintenant possible de construire des objets Pion :
auto
piece =
new
Pion; // compile
Notez qu'une fonction abstraite pourrait avoir sa propre implémentation, mais requerra toujours une sous-classe pour fournir sa propre implémentation d'une telle fonction. Par exemple, sa propre implémentation de PiecedEchec devrait fournir quelques contrôles utiles :
class
PiecedEchec {
abstract
bool isValid
(
in
Square from, in
Square to) {
// Nous avons besoin que la position « de »
// soit différente de la position « à »
return
from !=
to;
}
}
class
Pion : PiecedEchec {
override
bool isValid
(
in
Square from, in
Square to) {
// Vérifions d'abord si le mouvement est valide
// n'importe quelle pièce
if
(!
super
.isValid
(
from, to)) {
return
false
;
}
// ... puis contrôlons qu'il est valide pour le Pion ...
return
decision;
}
}
La classe PiecedEchec est toujours une classe abstraite même si isvalid() était déjà implémentée. Mais la classe Pion est non abstraite et peut être instanciée.
58-10. Exemple▲
Considérons une hiérarchie de classe qui représente un véhicule ferroviaire :
|
Les fonctions que WagonPassager déclarera comme abstraites sont indiquées par des points d'interrogation.
Comme mon but est seulement de présenter une hiérarchie de classe et signaler certaines décisions de design, je ne vais pas implémenter ces classes entièrement. Elles se contenteront d'afficher des messages.
La classe la plus générale de la hiérarchie ci-dessus est VehiculeFerroviere. Dans ce programme, il saura seulement comment bouger lui-même :
class
VehiculeFerroviere {
void
avance
(
in
size_t kilomètres) {
writefln
(
"Le véhicule avance de %s kilomètres"
,
kilometres);
}
}
Une classe qui hérite de VehiculeFerroviere est locomotive, qui n'a pas encore de membre particulier :
class
Locomotive : VehicleFerroviere {
}
Nous allons ajouter une fonction membre spéciale klaxonner plus tard durant un des exercices.
WagonPassager est un VehiculeFerroviere. Cependant, si la hiérarchie supporte différents types de wagons passager, alors certains comportements comme le chargement et le déchargement doivent être faits selon leurs types exacts. Pour cette raison, VehiculeFerroviere peut seulement déclarer ces deux fonctions comme abstraites :
class
WagonPassager : VehiculeFerroviere {
abstract
void
chargement
(
);
abstract
void
dechargement
(
);
}
Charger et décharger un wagon de passagers est aussi simple que d'ouvrir les portières d'une voiture, tandis que le chargement et déchargement de fret peut nécessiter des porteurs et des treuils. Les sous-classes suivantes fournissent des définitions pour les fonctions abstraites de WagonPassager :
class
WagonPassager : VehiculeFerroviere {
override
void
chargement
(
) {
writeln
(
"Les passagers montent à bord"
);
}
override
void
dechargement
(
) {
writeln
(
"Les passagers descendent"
);
}
}
class
VoituredeFret chargement WagonPassager {
override
void
chargement
(
) {
writeln
(
"les ca sont chargés"
);
}
override
void
dechargement
(
) {
writeln
(
"Les caisses sont déchargées"
);
}
}
Être une classe abstraite n'empêche pas l'usage de WagonPassager dans le programme. Les objets de WagonPassager ne peuvent pas être construits, mais WagonPassager peut être utilisé comme une interface. Comme la sous-classe définit les deux relations « un wagon passagers est un véhicule ferroviaire » et « le wagon de fret est un véhicule ferroviaire », les objets de WagonPassager et de WagondeFret peuvent être utilisés à la place de VehiculeFerroviere, cela sera vu dans la classe Train ci-dessous.
La classe représentant un train peut consister en une locomotive et un tableau de véhicules ferroviaires :
class
Train : VehiculeFerroviere {
Locomotive locomotive;
VehiculeFerroviere[] wagons;
// ...
}
Je voudrais répéter un point important: bien que Locomotive et Wagon héritent tous les deux de VehiculeFerroviere, il ne serait pas correct que Train hérite de l'un ou l'autre. L'héritage implique la relation «est un» et un train n'est ni une locomotive ni des wagons de passagers. Un train consiste en leur ensemble.
Si nous avons besoin que chaque train doive avoir une locomotive, le constructeur de Train doit s'assurer qu'il prenne un objet Locomotive valide. De même, si les wagons sont optionnels, ils peuvent être ajoutés par une fonction membre :
import
std.exception;
// ...
class
Train : VehiculeFerroviere {
// ...
this
(
Locomotive locomotive) {
enforce
(
locomotive !is
null
,
"Locomotive ne peut être null"
);
this
.locomotive =
locomotive;
}
void
ajoutWagon
(
Wagon[] voiture...) {
this
.voiture ~=
voiture;
}
// ...
}
Remarquez que ajoutVoiture() peut valider ainsi les objets Wagon, j'ignore cette validation ici.
Nous pouvons imaginer que les départs et arrivées de trains puissent aussi être supportés :
class
Train : VehiculeFerroviere {
// ...
void
stationDepartstring station) {
foreach
(
wagon; wagons) {
wagon.chargement
(
);
}
writefln
(
"Départ depuis la station %s"
, station);
}
void
stationArriveestring station) {
writefln
(
"Arrivée à la station %s"
, station);
foreach
(
wagon; wagons) {
wagon.dechargement
(
);
}
}
}
/* La fonction main suivante fait usage de la hiérarchie VehiculeFerroviere :*/
import
std.stdio;
// ...
void
main
(
) {
auto
locomotive =
new
Locomotive;
auto
train =
new
Train
(
locomotive);
train.ajoutWagon
(
new
wagonPassager, new
wgondeFret);
train.departStation
(
"Ankara"
);
train.avance
(
500
);
train.arriveStation
(
"Haydarpaşa"
);
}
La classe Train est utilisée par les fonctions qui sont fournies par deux interfaces séparées :
- Quand la fonction avance() est appelée, l'objet Train va être utilisé comme un objet VehiculeFerroviere, car la fonction est déclarée par VehiculeFerroviere ;
- Quand les fonctions departStation() et arriveStation() sont appelées, train va être utilisé comme un objet Train, car ces fonctions sont déclarées par Train.
Les flèches indiquent que les fonctions chargement() et dechargement() fonctionnent selon le type actuel de Wagon :
Les passagers montent à bord |
← |
Les caisses sont chargées |
← |
Départ de la station Ankara |
|
Le véhicule avance de 500 kilomètres |
|
Arrivée à la station Haydarpaşa |
|
Les passagers descendent |
← |
Les caisses sont déchargées |
← |
58-11. Résumé▲
- L'héritage est utilisé pour la relation « est un » ;
- chaque classe peut hériter d'une classe ;
- super a deux usages : appeler le constructeur de la superclasse et accéder aux membres de celle-ci ;
- override sert à redéfinir des fonctions membres d'une superclasse, spécialement pour une sous-classe ;
- abtract requiert qu'une fonction membre doit être surchargée.
58-12. Exercices▲
-
Modifions VehiculeFerroviere. En plus de retourner la distance d'avancement, faisons-la également faire des sons. Pour garder la sortie courte, affichons les sons par paliers de 100 kilomètres :
Sélectionnezclass
VehiculeFerroviere{
void
avance
(
in
size_t kilometres){
writefln
(
"Le véhicule avance de %s kilomètres"
, kilometres);foreach
(
i;0
.. kilometres/
100
){
writefln
(
" %s"
,faireSon
(
));}
}
// ...
}
- Cependant, faireSon() ne peut pas être défini par VehiculeFerroviere, car les véhicules ont des sons différents :
Laissez Train.faireSon pour le prochain exercice.
Parce qu'il doit être surchargé, faireSon() doit être déclaré comme abstraite par la superclasse :
class
vehiculeFerroviere {
// ...
abstract
string faireSon
(
);
}
// Implémentez faireSon() pour la sous-classe et essayez
// le code avec le main() suivant :
void
main
(
) {
auto
wagonn1 =
new
wagonPassager;
wagonn1.avance
(
100
);
auto
wagonn2 =
new
wagonMarchandise;
wagonn2.avance
(
200
);
auto
locomotive =
new
Locomotive;
locomotive.avance
(
300
);
}
Faites en sorte que le programme produise la sortie suivante :
Le véhicule avance de 100
kilomètres
clac clac
Le véhicule avance de 200
kilomètres
clac clac
clac clac
Le véhicule avance de 300
kilomètres
chop chop
chop chop
chop chop
Notez qu'il n'est pas requis que les sons wagonPassager et wagonMarchandise soient différents. Ils peuvent partager la même implémentation depuis wagon.
Pensez à comment faireSon()peut être implémenté pour Train. Une idée est que train.faireSon retourne une chaîne des sons des membres de Train.