Licence CC BY-NC-SA

L'héritage

Publié :

Dans ce chapitre, nous allons parler d'héritage ! Sachez qu'il s'agit de l'une des notions les plus importantes de la programmation orientée objet, et qui en font tout son intérêt ! Nous détaillerons ce concept tout au long du chapitre, mais pour vous remettre les idées en place, cela permet de créer une ou des nouvelles classes en se basant sur une autre. Entre autres, cela permet d'écrire des classes qui sont similaires en utilisant une autre classe qui regroupe l'ensemble des propriétés communes. Dans ce chapitre, la seule difficulté présente est la notion d'héritage en elle-même. Au niveau du code, il n'y aura pas énormément de nouveautés.

La notion d'héritage

Nous avons déjà brièvement présenté le concept, cependant vu son importance, ce n'est pas superflu d'en remettre une couche ! L'héritage permet de créer une ou des nouvelles classes en réutilisant le code d'une classe déjà existante. On parle alors de classe de base ou superclasse ou encore classe mère pour cette dernière, et de sous-classes ou classes filles pour les classes héritées de celle-ci. L'idée est donc ici d'étendre une classe de base, notamment en lui ajoutant de nouvelles propriétés. D'ailleurs le mot-clé qui permet d'étendre une classe est extends, que nous avons déjà croisé dans le deuxième chapitre:

1
2
3
public class Hello extends Sprite {

}

Nous reviendrons sur la syntaxe Actionscript plus tard. Pour l'instant, nous allons principalement nous focaliser sur la notion d'héritage.

Quand est-ce utile d'utiliser l'héritage ?

C'est en effet une question à laquelle les débutants ont souvent du mal à répondre. En réalité, nous pouvons introduire une relation d'héritage lorsque la condition suivante est respectée : la sous-classe est un sous-ensemble de la superclasse.

Ce terme mathématique barbare signifie que la sous-classe appartient à l'ensemble de la superclasse. Si ce n'est pas encore bien clair, voici quelques exemples qui vous aideront à bien comprendre :

  • l'Actionscript appartient à l'ensemble des langages de programmation
  • les fourmis appartiennent à l'ensemble des insectes
  • les avions appartiennent à l'ensemble des véhicules
  • les voitures appartiennent également à l'ensemble des véhicules
  • les 4L appartiennent à l'ensemble des voitures.

Comme vous pouvez le constater, les relations précédentes ne peuvent s'effectuer que dans un seul sens. Vous remarquerez également d'après les deux derniers exemples qu'il est possible d'avoir plusieurs niveaux d'héritage.

En quoi est-ce différent d'une instance de classe ?

Il est vrai qu'il serait possible par exemple, de créer des instances Avion et Voiture d'une classe Vehicule. Nous pourrions de cette manière définir des valeurs distinctes pour chacun des attributs afin de les différencier. En utilisant le concept d'héritage, nous pourrions écrire deux sous-classes Avion et Voiture qui hériteraient de l'ensemble des attributs et méthodes de la superclasse, et ce sans réécrire le code à l'intérieur de celles-ci. Mais tout l'intérêt vient du fait que l'utilisation de l'héritage nous permet de définir de nouveaux attributs et de nouvelles méthodes pour nos sous-classes. Sachez qu'il est également possible de redéfinir des méthodes de la superclasse, mais nous en reparlerons quand nous introduirons le polymorphisme plus loin dans ce chapitre !

Construction d'un héritage

Dans la suite de ce chapitre, nous allons principalement nous intéresser aux manipulations du côté des classes filles. Cependant nous aurons besoin d'une superclasse à partir de laquelle nous pourrons travailler. C'est pourquoi je vous propose une classe Vehicule, dont le code est donné ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package
{
    public class Vehicule
    {
        protected var _marque:String;
        protected var _vitesse:int;

        public function Vehicule(marque:String, vitesse:int)
        {
            _marque = marque;
            _vitesse = vitesse;
        }

        public function get marque():String
        {
            return _marque;
        }

        public function get vitesse():int
        {
            return _vitesse;
        }

        public function set vitesse(vitesse:int):void
        {
            _vitesse = vitesse;
        }

    }

}

La portée protected

Nous avions rapidement mentionné cette portée sans en expliquer vraiment le fonctionnement. Maintenant vous savez comment fonctionne l'héritage, nous allons pouvoir utiliser cette nouvelle portée qu'est protected ! Cette portée a été introduite afin de rendre les propriétés visibles non seulement depuis la classe où elles sont définies comme private, mais également depuis l'ensemble de ses sous-classes. Voyez les attributs de la classe Vehicule définie juste avant :

1
2
protected var _marque:String;
protected var _vitesse:int;

Ces attributs seront donc visibles depuis les éventuelles sous-classes que nous pourrons définir.

Lorsque nous avions introduit le concept d'encapsulation, nous avions dit qu'il fallait spécifier une portée de type private à tous vos attributs. Or comme nous l'avons dit, les attributs de ce type ne sont accessibles que depuis la classe où ils ont été déclarés. C'est pourquoi il est maintenant préférable d'opter pour la portée protected à chaque fois que l'une de vos classes est susceptible devenir une superclasse. En pratique, on utilise quasiment tout le temps cette portée dans l'hypothèse d'un héritage futur.

À présent vous connaissez l'ensemble des portées que propose l'Actionscript, dont voici un petit récapitulatif :

  • public : propriété visible n'importe où
  • private : propriété visible uniquement à l'intérieur de la classe qui l'a définie
  • protected : propriété visible depuis la classe où elle est définie, ainsi que depuis l'ensemble de ses sous-classes
  • internal : propriété visible depuis l'ensemble du package où elle est définie.

Construction des sous-classes

L'héritage

Comme nous l'avons dit en début de chapitre, l'héritage se fait en « étendant » une classe à l'aide du mot-clé extends comme ceci :

1
2
3
4
5
6
7
package
{
    public class Voiture extends Vehicule
    {

    }
}

Attention à l'ordre dans lequel sont placés les différents éléments. Nous sommes ici en train de déclarer une nouvelle classe nommée Voiture qui hérite de la classe Vehicule.

Par cette simple extension, la classe Voiture hérite donc de l'ensemble des propriétés de la classe Vehicule. Il est toutefois nécessaire de lui rajouter au moins un constructeur, et éventuellement quelques propriétés supplémentaires pour lui donner de l'intérêt. Par exemple, nous pourrions introduire un attribut _traction pour définir si la voiture est une traction ou non, ou encore un attribut _immatriculation pour identifier celle-ci :

1
2
3
4
5
6
7
8
package
{
    public class Voiture extends Vehicule
    {
        protected var _traction:Boolean;
        protected var _immatriculation:String;
    }
}

Le constructeur

Comme toute classe, notre sous-classe Voiture doit posséder un constructeur. Cependant, celle-ci hérite d'une classe mère qui possède son propre constructeur qui est nécessaire à l'initialisation des attributs. Depuis une classe fille, il est possible d'appeler le constructeur de sa superclasse par la fonction super() :

1
2
3
4
5
public function Voiture()
{
    super();
    // Instructions supplémentaires
}

L'appel du constructeur super() de la superclasse doit obligatoirement être la première instruction du constructeur de votre sous-classe. Les nouvelles instructions seront placées à la suite pour permettre l'initialisation de vos nouvelles variables.

Voici donc un exemple de constructeur pour notre classe Voiture :

1
2
3
4
5
6
public function Voiture(marque:String, vitesse:int, immatriculation:String)
{
    super(marque, vitesse);
    _immatriculation = immatriculation;
    _traction = true;
}

Vous remarquerez que la fonction super() est le constructeur de la superclasse. Il est donc normal de retrouver les différents paramètres du constructeur défini plus haut.

Les méthodes

En plus de pouvoir définir de nouveaux attributs, il est possible de rajouter autant de méthodes que nous souhaitons à l'intérieur d'une sous-classe. Étant donné nous utilisons encore le concept d'encapsulation, nous commencerons par créer des accesseurs à ce nouvel attribut. Je vous propose de découvrir quelques-uns d'entre eux :

1
2
3
4
5
6
7
8
9
public function set immatriculation(nouvelleImmatriculation:String):void
{
    _immatriculation = nouvelleImmatriculation;
}

public function get immatriculation():String
{
    return _immatriculation;
}

Comme vous pouvez le voir, ces méthodes fonctionnent exactement de la même manière que pour une classe quelconque. En revanche ce qu'il peut être intéressant, c'est d'utiliser les méthodes définies à l'intérieur de la classe mère. Ainsi si les méthodes de votre classe mère ont une portée public ou protected, celles-ci sont accessibles depuis les classes filles. Nous avons ainsi un mot-clé super qui nous permet de faire référence à la superclasse, et d'utiliser ses propriétés. Voici par exemple une méthode nommée accelerer() qui permet d'augmenter la vitesse du véhicule :

1
2
3
4
5
public function accelerer():void
{
    var nouvelleVitesse:int = super.vitesse + 15;
    super.vitesse = nouvelleVitesse;
}

Vous noterez que nous aurions pu utiliser directement l'attribut _vitesse de la classe Vehicule pour définir la variable nouvelleVitesse. En revanche nous sommes obligés d'utiliser l'accesseur vitesse() pour modifier la valeur de l'attribut. En effet, seules les propriétés définies par le mot-clé function, donc les méthodes, peuvent être redéfinies. Nous reviendrons là-dessus juste après, lorsque nous parlerons du polymorphisme.

Le mot-clé super fait référence à l'objet via la classe mère, par opposition au mot-clé this qui pointe sur l'objet en lui-même .

Néanmoins, le mot-clé super est facultatif dans la plupart des cas. Le code ci-dessous est tout à fait fonctionnel :

1
2
3
4
5
public function accelerer():void
{
    var nouvelleVitesse:int = vitesse + 15; // L'accesseur de la classe-mère sera automatiquement sélectionné
    vitesse = nouvelleVitesse;
}

Il peut même être simplifié (vu que vous avez compris le principe) :

1
2
3
4
public function accelerer():void
{
    vitesse += 15;
}

Rappel : les accesseurs simulent le fonctionnement des attributs. Il est donc possible d'utiliser ici tous les opérateurs mathématiques : vitesse est considéré comme un nombre.

Comme je n'ai pas tout réécrit, je vous propose tout de même un schéma UML résumant les propriétés des deux classes Vehicule et Voiture ainsi que le lien qui les unit :

Héritage entre les classes Vehicule et Voiture

La substitution d'une sous-classe à une superclasse

Un autre avantage de l'utilisation de l'héritage est le fait de pouvoir substituer une sous-classe à une superclasse. C'est-à-dire qu'il est possible de manipuler une classe fille comme s'il s'agissait d'une instance de la classe mère.

Parce qu'un exemple vaut mille mots, prenons le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var MesVehicules:Array = new Array();
MesVehicules.push(new Vehicule("Airbus A380", 900));
MesVehicules.push(new Vehicule("Bicyclette", 25));
MesVehicules.push(new Voiture("Renault 4L", 100, "911 SDZ 15"));
for (var i:int = 0; i < MesVehicules.length; i++)
{
    trace("Un véhicule de type " + MesVehicules[i].marque + " peut se déplacer à la vitesse de " + MesVehicules[i].vitesse + "km/h.");
}
/* Affiche :
Un véhicule de type Airbus A380 peut se déplacer à la vitesse de 900km/h.
Un véhicule de type Bicyclette peut se déplacer à la vitesse de 25km/h.
Un véhicule de type Renault 4L peut se déplacer à la vitesse de 100km/h.
*/

Il n'y a ici rien de surprenant, les accesseurs de la classe Vehicule sont bien accessibles depuis la classe Voiture. En revanche ce qui deviendrait intéressant, ce serait de créer une méthode presenter() qui permet de présenter un objet de type Vehicule, comme ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var MesVehicules:Array = new Array();
MesVehicules.push(new Vehicule("Airbus A380", 900));
MesVehicules.push(new Vehicule("Bicyclette", 25));
MesVehicules.push(new Voiture("Renault 4L", 100, "911 SDZ 15"));
function presenter(unVehicule:Vehicule):void 
{
    trace("Un véhicule de type " + unVehicule.marque + " peut se déplacer à la vitesse de " + unVehicule.vitesse + "km/h.");
}
for (var i:int = 0; i < MesVehicules.length; i++)
{
    presenter(MesVehicules[i]);
}
/* Affiche :
Un véhicule de type Airbus A380 peut se déplacer à la vitesse de 900km/h.
Un véhicule de type Bicyclette peut se déplacer à la vitesse de 25km/h.
Un véhicule de type Renault 4L peut se déplacer à la vitesse de 100km/h.
*/

Comment se fait-il qu'il n'y ait pas d'erreur pour l'objet de type Voiture ?

Comme nous l'avons dit, nous pouvons substituer une sous-classe à une superclasse. En d'autres termes, il est possible d'utiliser une classe fille comme s'il s'agissait de la classe mère. D'ailleurs si vous vous rappelez bien nous avions dit qu'une sous-classe était un sous-ensemble de la superclasse, ce qui veut dire qu'une Voiture est un Vehicule. Il n'est donc pas surprenant de pouvoir utiliser un objet de type Voiture en tant que Vehicule.

Encore une fois, attention au sens de l'héritage ! Dans notre exemple, il n'est pas possible de substituer un Vehicule à une Voiture ; seul le sens opposé est exact !

Le polymorphisme

Nous allons maintenant parler du polymorphisme ! Nous avons là-encore un nom barbare associé à un concept qui n'est très compliqué au fond.

Précédemment, nous avons appris à étendre une superclasse en ajoutant de nouvelles méthodes à l'intérieur d'une sous-classe. Cependant il est également possible de redéfinir (réécrire) une méthode. Ainsi nous avons la possibilité d'utiliser un nom de méthode commun pour une méthode qui se comportera différemment suivant le type de l'objet.

Pour vous montrer cela, nous allons insérer une nouvelle méthode qu'on nommera sePresenter() à l'intérieur de la classe Vehicule :

1
2
3
4
public function sePresenter():void
{
    trace("Un véhicule de type " + marque + " peut se déplacer à la vitesse de " + vitesse + "km/h.");
}

Rappel : il est conseillé d'utiliser les accesseurs dans la classe elle-même (sauf dans le constructeur).

Si nous ne faisons rien, la classe Voiture héritera de cette nouvelle méthode, et nous pourrons l'utiliser sans problème. Cependant nous aimerions personnaliser le message, notamment en rajoutant son numéro d'immatriculation qui permet également de l'identifier. Nous devrons donc réécrire la méthode sePresenter() pour la classe Voiture. Heureusement nous disposons d'un mot-clé override, qui permet justement de redéfinir une méthode. Voici comment l'utiliser :

1
2
3
4
5
override public function sePresenter():void
{
    trace("Une voiture " + marque + " peut se déplacer à la vitesse de " + vitesse + "km/h.");
    trace("Son immatriculation est : " + immatriculation);
}

Ainsi nous pouvons utiliser la méthode sePresenter() sans nous soucier du type d'objets que nous sommes en train de manipuler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var MesVehicules:Array = new Array();
MesVehicules.push(new Vehicule("Airbus A380", 900));
MesVehicules.push(new Vehicule("Bicyclette", 25));
MesVehicules.push(new Voiture("Renault 4L", 100, "911 SDZ 75"));
for (var i:int = 0; i < MesVehicules.length; i++)
{
    MesVehicules[i].sePresenter();
}
/* Affiche :
Un véhicule de type Airbus A380 peut se déplacer à la vitesse de 900km/h.
Un véhicule de type Bicyclette peut se déplacer à la vitesse de 25km/h.
Une voiture Renault 4L peut se déplacer à la vitesse de 100km/h.
Son immatriculation est : 911 SDZ 75
*/

Pour résumer, le polymorphisme est une technique très puissante, surtout lorsqu'elle est associée à la substitution d'une sous-classe à une superclasse. Nous pouvons ainsi définir des méthodes « par défaut » dans la classe mère, puis de les redéfinir pour différentes classes filles. Il est ensuite possible d'utiliser ces différents objets de la même façon comme s'il s'agissait de la même classe. Vous verrez dans la suite que c'est un atout non négligeable !

Les attributs de classe

Lorsque nous avions parlé d'encapsulation, nous avions introduit les droits d'accès pour les différentes propriétés d'une classe. Or pour ceux qui l'auraient également remarqué, nous avons depuis le départ toujours inséré le mot-clé public devant la définition de chacune de nos classes.

Il existe aussi des droits d'accès pour les classes ?

En effet, tout comme les propriétés, les classes possèdent des droits d'accès qui permettent de définir comment nous pouvons accéder à la classe et même la modifier. En réalité, on parle d'attributs de classes et d'attributs de propriétés de classes, mais nous emploierons dans ce cours le terme « droits d'accès » pour éviter la confusion avec les variables internes aux classes appelées également attributs.

Les différents droits d'accès

En ce qui concerne la définition de classes, l'Actionscript propose quatre droits d'accès différents. Sans plus attendre, je vous propose de les découvrir :

  • public : les droits d'accès « publiques » permettent comme pour les propriétés, de rendre la classe visible et accessible partout dans le code. Il s'agit des droits d'accès recommandés dans la majorité des cas.
  • internal : identiquement aux propriétés, les droits d'accès « internes » restreignent l'accessibilité de la classe au package où elle est définie uniquement. Également ces droits d'accès ne sont pas très utilisés, mais il s'agit de la valeur par défaut lors d'une définition de classe.
  • final : ces droits d'accès sont directement liés à la notion d'héritage. Le terme « final » fait ici référence au fait que la classe ne peut plus être étendue par une autre classe.
  • dynamic : ce mot-clé permet de définir une classe dynamique, c'est-à-dire modifiable depuis l'extérieur de celle-ci, à l'opposé des classes classiques dites scellées. Nous reviendrons sur ce concept un peu particulier dans le prochain chapitre.

Pour résumer tout ce qui concerne les droits d'accès et l'encapsulation, vous devez vous rappeler qu'on doit principalement limiter l'accès aux attributs en utilisant préférentiellement le mot-clé protected. Pour les méthodes et les classes en général, vous privilégierez principalement un accès « publique » à l'aide du mot-clé public. Il existe néanmoins divers autres droits d'accès, qui ne sont utiles que dans de rares occasions.

Exemple d'utilisation

Vous l'aurez compris, ces droits d'accès s'utilisent également devant la déclaration de la classe en question. Voici par exemple la définition de la classe Vehicule que nous avons réalisée au début du chapitre :

1
2
3
4
public class Vehicule
{

}

Nous allons essayer ici de comprendre un peu mieux l'utilité du mot-clé final, étroitement lié au concept d'héritage ! Ce mot-clé permet de définir la classe à laquelle il est associé, comme étant la dernière classe qui finalise l'arborescence d'héritage. C'est-à-dire que cette classe peut très bien hériter d'une autre classe, mais en aucun cas vous ne pourrez créer de classes filles à celle-ci. Je vous propose donc une petite manipulation afin de vérifier la véracité de ces propos. Redéfinissez donc la classe Vehicule de type final :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package
{
    final class Vehicule
    {
        protected var _marque:String;
        protected var _vitesse:int;

        public function Vehicule(marque:String, vitesse:int)
        {
            _marque = marque;
            _vitesse = vitesse;
        }

        public function get marque():String
        {
            return _marque;
        }

        public function get vitesse():int
        {
            return _vitesse;
        }

        public function set vitesse(vitesse:int):void
        {
            _vitesse = vitesse;
        }

    }

}

Nous avons donc maintenant une classe Vehicule qu'il nous est interdit d'étendre. Voyons donc ce qui se passe lorsqu'on tente d'en créer une sous-classe :

1
2
3
4
5
6
7
package
{
    public class Voiture extends Vehicule
    {

    }
}

Si vous tentez donc de lancer votre programme, le compilateur refusera de compiler le projet en vous précisant l'erreur suivante : « Base class is final. », qui signifie que la classe dont on essaie d'hériter est de type final, et que notre héritage est donc contraire à cette définition de classe.

Comme vous pouvez le constater, ce mot-clé final n'apporte aucune réelle fonctionnalité, mais il s'agit plutôt d'une sécurité. Néanmoins l'utilité de ce type de droits d'accès est assez restreinte, et vous définirez majoritairement des classes de type public.


En résumé

  • L'héritage permet de créer une ou des nouvelles classes en utilisant le code d'une classe déjà existante.
  • Pour hériter d'une classe, il faut utiliser le mot-clé extends.
  • La portée protected est spécifique à l'héritage.
  • Le constructeur de la superclasse peut être appelé par la fonction super().
  • Dans le cas d'une relation par héritage, il est possible de substituer une sous-classe à une superclasse.
  • Le polymorphisme permet de redéfinir une méthode de la superclasse par l'intermédiaire du mot-clé override.
  • Les différents droits d'accès liés à la définition d'une classe sont public, internal, final et dynamic.