Cours complet pour apprendre à programmer en D


précédentsommairesuivant

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 :

 
Sélectionnez
class SousClasse : SuperClasse {
    // ...
}

Pour voir un autre exemple, supposons qu'il y a déjà une classe représentant une horloge :

 
Sélectionnez
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() :

 
Sélectionnez
 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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
20:30:0007: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 ;

Image non disponible

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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

Image non disponible

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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
    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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
void main() {
    auto HorlogedeBureau = new AlarmeHorloge(10, 15, 0, 6, 45);
    writeln(HorlogedeBureau);
}

La sortie :

 
Sélectionnez
10:15:0006: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 :

 
Sélectionnez
void use(Horloge horloge) {
    // ...
    horloge.reset();
    // ...
}

Le polymorphisme rend possible l'envoi d'un AlarmeHOrloge à une telle fonction :

 
Sélectionnez
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 :

 
Sélectionnez
Avant : 10:15:0006:45
Après : 00:00:0000: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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
class PieceEchec {
    abstract bool isValid(in Square from, in Square to);
}

Il n'est pas possible de construire des objets de classes abstraites :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

  1. 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 ;
  2. 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

  1. 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électionnez
    class 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());
            }
        }
     
        // ...
    }
  2. 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+