Licence CC BY-NC-SA

La programmation modulaire IV : Héritage et dérivation

Après un chapitre sur la généricité plutôt (voire carrément) théorique, nous enchaînons avec un second tout aussi compliqué, mais ô combien important. Je dirais même que c'est le chapitre phare de cette partie IV : l'héritage. Et pour cause, l'héritage est, avec l'encapsulation, la propriété fondamentale de la programmation orientée objet.

Pour illustrer ce concept compliqué, nous prendrons, tout au long de ce chapitre, le même exemple : imaginez que nous souhaitions créer un jeu de stratégie médiéval. Votre mission (si vous l'acceptez :soleil: ) : créer des objets pour modéliser les différentes unités du jeu : chevalier, archer, arbalétrier, catapulte, voleur, écuyer… ainsi que leurs méthodes associées : attaque frontale, défense, tir, bombardement, attaque de flanc…

Pour bien commencer

Héritage : une première approche théorique

Attention, question métaphysique : qu'est-ce qu'un héritage ?

Eh bien c'est quand papy décède et que les enfants et petits-enfants se partagent le pactole ! :p

Mouais, j'espérais un peu plus de sérieux. Mais vous n'êtes pas si loin de la définition d'héritage en programmation. Non, nos programmes ne vont pas décéder mais ils vont transmettre leurs biens. En effet, dire qu'un objet B hérite d'un objet A, cela revient à dire que B bénéficiera de tous les attributs de A : types, fonctions, procédures, variables… et ce, sans avoir besoin de tout redéfinir.

On dit alors que A est l'objet père et B l'objet fils. On parle alors d'héritage Père-Fils (ou mère-fille, c'est selon les accords). De même, si un objet C hérite de B, alors il bénéficiera des attributs de B, mais aussi de A par héritage. On dira que C est le fils de B et le petit-fils de A.

Un dernier exemple : si un deuxième objet D hérite de A, on dira que D et B sont deux objets frères. Ils bénéficient chacun des attributs du père A, mais B ne peut pas bénéficier des attributs de son frère D et réciproquement : il n'y a pas d'héritage entre frères en programmation. Comment cela vous êtes perdus ? :p Allez, pour ceux qui ont toujours été nuls en généalogie, voici un petit schéma où chaque flèche signifie «hérite de» :

Principe de l'héritage

Héritage : une approche par l'exemple

Pour mieux comprendre l'utilité de l'héritage (et avant de découvrir son fonctionnement en Ada), revenons à l'exemple donné en introduction. Nous souhaiterions créer un type d'objet pour représenter des unités militaires pour un jeu de stratégie. Quelles sont les attributs et fonctionnalités de base d'une unité militaire ? Elle doit disposer d'un potentiel d'attaque, de défense et de mouvement. Elle doit également pouvoir attaquer, se défendre et se déplacer.

Toutefois, certaines unités ont des «pouvoirs spéciaux» afin de rendre le jeu plus attrayant : certaines sont montées à cheval pour prendre l'ennemi de vitesse et l'attaquer par le côté ; d'autres ont la capacité d'attaquer à distance à l'aide d'arcs, d'arbalètes ou d'engin de siège ; et dans le cas des engins de siège, les unités qui en sont dotées ont alors en plus la possibilité de bombarder l'ennemi pour l'affaiblir avant l'assaut. Résumons par un petit schéma :

Capacités des différentes unités

Les unités d'artillerie bénéficient des capacités des unités archères, qui elles-mêmes disposent des capacités des Unités classiques (mais l'inverse n'est pas vrai). De même, les unités montées disposent des capacités des unités classiques mais pas de celles des unités archères. On dit alors que tous ces types d'unités font partie de la classe de types Unité. Les types Archer et Artillerie font partie de la sous-classe fille Archers, mais pas de la sous-classe sœur Unités montées.

Mais plutôt que ce lourd tableau, nous modéliserons cette situation à l'aide d'un diagramme de classe UML comme nous l'avons fait précédemment avec nos objets A, B, C et D :

Diagramme de classe de nos unités

Chaque case correspond à un type d'objet particulier. La première ligne indique le nom du type ; la seconde ligne fait la liste de ses attributs (notez que les attributs hérités de T_Unit ne sont pas réécrits) ; la troisième ligne liste les méthodes supplémentaires. Chacune des flèches signifie toujours « hérite de … ». Bien ! Maintenant que les idées sont fixées, voyons comment réaliser ce fameux héritage en Ada.

Héritage

Héritage de package simple

Données initiales

Supposons que nous disposions d'un package P_Unit contenant le type T_Unit et ses méthodes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package P_Unit is

   type T_Unit is record
      Att, Def, Mov, Vie : Integer := 0 ;
   end record ; 

   procedure Attaque(Attaquant, Defenseur : in out T_Unit) ; 
   procedure Defense(Defenseur : in out T_Unit) ; 
   procedure Deplacer(Unite : in out T_Unit) ; 
   function Est_Mort(Unite : in T_Unit) return Boolean ; 
end P_Unit ;

P_Unit.ads

Le corps des méthodes sera minimal, pour les besoins du cours :

 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
With ada.text_IO ;                Use Ada.Text_IO ; 

package body P_Unit is

   procedure Attaque(Attaquant, Defenseur : in out T_Unit) is
   begin
      Put_line("   >> Attaque frontale !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.att) ; 
   end Attaque ; 

   procedure Defense(Defenseur : in out T_Unit) is
   begin
      put_line("   >> Votre unite se defend.") ; 
      Defenseur.def := integer(float(Defenseur.def) * 1.25) ; 
   end Defense ; 

   procedure Deplacer(Unite : in out T_Unit) is
   begin
      Put_line("   >> Votre unite se deplace") ; 
   end Deplacer ;  

   function Est_Mort(Unite : in T_Unit) return Boolean is
   begin
      return (Unite.vie <= 0) ; 
   end Est_Mort ; 

end P_Unit ;

P_Unit.adb

On n'est pas sensé mettre le type T_Unit en GENERIC et PRIVATE voire LIMITED PRIVATE ?

Un type générique ? Non, la généricité n'a rien à voir avec l'héritage. Nous avons besoin d'un type concret ! En revanche, nous devrions encapsuler notre type pour réaliser proprement notre objet mais pour les besoins du cours, nous ne respecterons pas cette règle d'or (pour l'instant seulement). Nous disposons également d'un programme appelé Strategy qui crée deux objets Hallebardier et Chevalier de type T_Unit et les fait s'affronter :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
with P_Unit ;            use P_Unit ;
with Ada.Text_IO ;       use Ada.Text_IO ; 

procedure Strategy is
   Hallebardier : T_Unit :=  (Att => 3,
                              Def => 5, 
                              Mov => 1, 
                              Vie => 10) ; 
   Chevalier    : T_Unit :=  (Att => 5,
                              Def => 2, 
                              Mov => 3, 
                              Vie => 8) ; 
begin
   Attaque(Chevalier,Hallebardier) ; 
   if Est_mort(Hallebardier)
      then put_line("Echec et Mat, baby !") ; 
      else put_line("Pas encore !") ; 
   end if ; 
end Strategy ;

Strategy.adb

Un premier package fils

Mais comme Chevalier devrait être une unité montée, nous voudrions proposer le choix au joueur entre attaquer et attaquer par le flanc. Nous allons donc créer un package spécifique pour gérer ces unités montées : nous ne nous soucierons pas du type pour l'instant, seulement du package. Nous ne voudrions pas être obligés de recréer l'intégralité du package P_Unit, nous allons donc devoir utiliser cette fameuse propriété d'héritage. Attention, ouvrez grand les yeux, ça va aller très vite :

1
2
3
package P_Unit.Mounted is
   procedure Attaque_de_flanc(Attaquant, Defenseur : in out T_Unit) ;
end P_Unit.Mounted ;

P_Unit-Mounted.ads

:magicien: TADAAAM ! ! ! C'est fini, merci pour le déplacement !

:waw: … Attend ! C'est tout ? Mais t'as absolument rien fait ? :pirate:

Mais bien sûr que si ! Soyez attentifs au nom : « PACKAGE P_Unit.Mounted » ! Le simple fait d'étendre le nom du package père (P_Unit) à l'aide d'un point suivi d'un suffixe (Mounted) suffit à réaliser un package fils. Simple, n'est-ce pas ? En plus, il est complètement inutile d'écrire « WITH P_Unit », puisque notre package hérite de toutes les fonctionnalités de son père (types, méthodes, variables…) !

Le nom du package fils doit s'écrire sous la forme Père . Fils. En revanche, le nom du fichier correspondant doit s'écrire sous la forme Père - Fils.ad# (avec un tiret et non un point) pour éviter des confusions avec les extensions de fichier.

Quant au corps de ce package, il sera lui aussi très succinct :

1
2
3
4
5
6
7
8
9
With ada.text_IO ;                Use Ada.Text_IO ; 

package body P_Unit.Mounted is
   procedure Attaque_de_flanc(Attaquant, Defenseur : in out T_Unit) is
   begin
      Put_line("   >> Attaque par le flanc ouest !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.att*Attaquant.mov) ;
   end Attaque_de_flanc ; 
end P_Unit.Mounted ;

P_Unit-Mounted.adb

Utilisation de notre package

Maintenant, revenons à notre programme Strategy. Nous souhaitons laisser le choix au joueur entre Attaque() et Attaque_De_Flanc(), ou plus exactement entre P_Unit.Attaque() et P_Unit.Mounted.Attaque_De_Flanc(). Nous devons donc spécifier ces deux packages, le père et le fils, en en-tête :

 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
with P_unit ;                  use P_Unit ;
with P_Unit.Mounted ;          use P_Unit.mounted;
with Ada.Text_IO ;             use Ada.Text_IO ;

procedure Strategy is
   Hallebardier : T_Unit :=  (Att => 3,
                              Def => 5,
                              Mov => 1,
                              Vie => 10) ;
   Chevalier    : T_Unit :=  (Att => 5,
                              Def => 2,
                              Mov => 3,
                              Vie => 8) ;
   choix : character ;
begin
   Put("Voulez-vous attaquer frontalement (1) ou par le flanc (2) ? ") ;
   get(choix) ; skip_line ;
   if choix = '1'
      then Attaque(Chevalier,Hallebardier) ;
      else Attaque_de_flanc(Chevalier,Hallebardier) ;
   end if ;

   if Est_mort(Hallebardier)
      then put_line("Echec et Mat, baby !") ;
      else put_line("Pas encore !") ;
   end if ;
end Strategy ;

Il n'y a là rien d'extraordinaire, vous l'auriez deviné vous-même.

Deux héritages successifs !

Passons maintenant à nos packages pour archers et artillerie. Le premier doit hériter de P_Unit, mais vous savez faire désormais (n'oubliez pas de réaliser le corps du package également) :

1
2
3
package P_Unit.Archer is
   procedure Attaque_a_distance(Attaquant, Defenseur : in out T_Unit) ;
end P_Unit.Archer ;

P_Unit-Archer.ads

1
2
3
4
5
6
7
8
9
With ada.text_IO ;                Use Ada.Text_IO ; 

package body P_Unit.Archer is
   procedure Attaque_a_distance(Attaquant, Defenseur : in out T_Unit) is
   begin
      Put_line("   >> Une pluie de fleches est decochee !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.tir) ;
   end Attaque_a_distance ; 
end P_Unit.Archer ;

P_Unit-Archer.adb

Quant au package pour l'artillerie, nous souhaiterions qu'il hérite de P_Unit.Archer, de manière à ce que les catapultes et trébuchets puisse attaquer, attaquer à distance et en plus bombarder ! Il va donc falloir réutiliser la notation pointée :

1
2
3
package P_Unit.Archer.Artillery is
   procedure Bombarde(Attaquant, Defenseur : in out T_Unit) ;
end P_Unit.Archer.Artillery  ;

P_Unit-Archer_Artillry.ads

1
2
3
4
5
6
7
package body P_Unit.Archer.Artillery is
   procedure Bombarde(Attaquant, Defenseur : in out T_Unit) is
   begin
      Put_line("   >> Bombardez-moi tout ça !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.bomb) ;
   end Bombarde ; 
end P_Unit.Archer.Artillery  ;

P_Unit-Archer_Artillry.adb

Et le tour est joué ! Seulement, cela va devenir un peu laborieux d'écrire en en-tête de notre programme :

1
with P_Unit.Archer.Artillery ;      use P_Unit.Archer.Artillery ;

Alors pour les «fainéants du clavier», Ada a tout prévu ! Il y a une instruction de renommage : RENAMES. Celle-ci va nous permettre de créer un package qui sera exactement le package P_Unit.Archer.Artillery mais avec un nom plus court :

1
2
3
With P_Unit.Archer.Artillery ; 

package P_Artillery renames P_Unit.Archer.Artillery ;

P_Artillery.ads

Notez qu'il est complètement inutile de réaliser le corps de ce package P_Artillery, ce n'est qu'un surnommage.

Héritage avec des packages privés

Perso, je ne vois pas vraiment l'intérêt de ce fameux héritage. :( On aurait très bien pu faire trois packages P_Mounted, P_Archer et P_Artillery avec un « WITH P_Unit », et le tour était joué ! Quel intérêt de faire des packages Père-fils ?

L'intérêt est double : primo, cela permet de hiérarchiser notre code et notre travail, ce qui n'est pas négligeable lors de gros projets. Secundo, vous oubliez que nous avons négligé l'encapsulation ! Il n'est pas bon que notre type T_Unit soit public. En fait, nous devrions modifier notre package P_Unit pour privatiser T_Unit et lui fournir une méthode pour l'initialiser :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package P_Unit is

   type T_Unit is private ;

   Function Init(Att, Def, Mov, Vie : Integer := 0) return T_Unit ; 
   procedure Attaque(Attaquant, Defenseur : in out T_Unit) ;
   procedure Defense(Defenseur : in out T_Unit) ;
   procedure Deplacer(Unite : in out T_Unit) ;
   function Est_Mort(Unite : in T_Unit) return Boolean ;

private

   type T_Unit is record
      Att, Def, Mov, Vie : Integer := 0 ;
   end record ;

end P_Unit ;

P_Unit.ads

Mais, si T_Unit devient privé, alors les procédures Attaque_De_Flanc(), Attaque_a_distance() ou Bombarde() n'y auront plus accès puisqu'elles ne sont pas dans le corps du package P_Unit !?! o_O

Tout dépend de la solution que vous adoptez. Si vous faites appel aux instructions WITH et pas à l'héritage, ces procédures rencontreront effectivement des problèmes. Mais si vous faites appel à l'héritage alors il n'y aura aucun souci, car les spécifications d'un package fils ont visibilité sur les spécifications publiques et privées du package père ! L'héritage se fait donc également sur la partie PRIVATE. L'inverse n'est bien sûr pas vrai : le père n'hérite rien de ses fils. Pour plus de clarté, voici un schéma sur lequel les flèches signifient qu'une partie a la visibilité sur ce qui est écrit dans une autre.

Visibilité entre packages père et fils

On comprend alors que la propriété d'héritage donne de très nombreux droits d'accès aux packages fils, bien plus que ne l'autorisent les clauses de contexte WITH. Les packages fils peuvent ainsi manipuler à leur guise les types privés, ce qui ne serait pas possible autrement. Pour rappel, voici le schéma de la visibilité d'un programme sur un package que nous avons déjà vu. La visibilité est nettement restreinte :

Visibilité sur un package

Héritage avec des packages génériques

Réalisation

Nous allons considérer pour cette sous-partie, et cette sous-partie seulement, que nos points de vie, d'attaque, de défense… ne sont pas des integer comme indiqués précédemment mais des types entiers génériques ! Je sais, ça devient compliqué mais il faut que vous vous accrochiez. ;) Nous obtenons donc un nouveau package P_Unit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
generic
   type T_Point is range <> ; 
   Valeur_Initiale : T_Point ;              --Tiens ! Une variable générique ?!?
package P_Unit is

   type T_Unit is private ;

   Function Init(Att, Def, Mov, Vie : T_Point := Valeur_Initiale) return T_Unit ; --Revoilà la variable générique ! 
   procedure Attaque(Attaquant, Defenseur : in out T_Unit) ;
   procedure Defense(Defenseur : in out T_Unit) ;
   procedure Deplacer(Unite : in out T_Unit) ;
   function Est_Mort(Unite : in T_Unit) return Boolean ;

private

   type T_Unit is record
      Att, Def, Mov, Vie : T_Point := Valeur_Initiale ; --Encore cette variable générique !
   end record ;

end P_Unit ;

P_Unit.ads

Nous avons donc un type d'objet T_Unit qui est non seulement privé mais également générique : les points de vie, de défense… sont entiers mais on ne sait rien de plus : sont-ce des Integer ? Des Natural ? Des Positive ? … On n'en sait rien et c'est d'ailleurs pour cela que j'ai du ajouter une variable générique pour connaître la valeur initiale (eh oui, 0 ne fait pas partie des Positive par exemple :) ). Mais la difficulté n'est pas là. Puisque les fils héritent des propriétés du père, si P_Unit est générique alors nécessairement tous ses descendants le seront également ! Prenons le cas de P_Unit.Mounted, nous allons devoir le modifier ainsi :

1
2
3
4
generic
package P_Unit.Mounted is
   procedure Attaque_de_flanc(Attaquant, Defenseur : in out T_Unit) ;
end P_Unit.Mounted ;

Tu as oublié de réécrire le type T_Point et la variable Valeur_Initiale après GENERIC !

Non, je n'ai rien oublié. Le type T_Point et la variable Valeur_Initiale ont déjà été défini dans le package père, donc P_Unit.Mounted les connaît déjà. On doit simplement réécrire le mot GENERIC mais sans aucun paramètre formel (à moins que vous ne vouliez en ajouter d'autres bien sûr).

Instanciation

Et c'est à l'instanciation que tout cela devient vraiment (mais alors VRAIMENT :diable: ) pénible. Si vous souhaitez utiliser P_Unit.Mounted vous allez devoir l'instancier, vous vous en doutez ! Sauf qu'il n'a pas de paramètres formels ! Donc il faut préalablement instancier le père :

1
package MesUnites is new P_Unit(Natural,0) ;

Et pour instancier le fils, il faudra utiliser non plus le package générique père P_Unit mais le package que vous venez d'instancier :

1
package MesUnitesMontees is new MesUnites.Mounted ;

Je vois que certains commencent à peiner. Alors revenons à des packages non-génériques, cela vaudra mieux.

Dérivation et types étiquetés

Créer un type étiqueté

Nous n'avons fait pour l'instant que réaliser des packages pères-fils avec de nouvelles méthodes mais notre diagramme de classe indiquait que les unités d'archers devaient bénéficier de points de tir et les unités d'artillerie de points de tir et de bombardement.

Diagramme de classe des unités

Comment faire ? Nous avons la possibilité d'utiliser des types structurés polymorphes ou mutants mais cela va à l'encontre de la logique de nos packages ! Il faudrait que le type T_Unit prévoit dès le départ la possibilité de disposer de points de tir ou de bombardement alors que le package P_Unit ne les utilisera pas. De plus, sur d'importants projets, le programmeur réalisant le package P_Unit n'est pas nécessairement le même que celui qui réalise P_Unit.Archer ! Les fonctionnalités liées au tir et au bombardement ne le concernent donc pas, ce n'est pas à lui de réfléchir à la façon dont il faut représenter ces capacités. Enfin, les types mutants ou polymorphes impliquent une structure très lourde (souvenez vous des types T_Bulletins et des nombreux CASE imbriqués les uns dans les autres) : difficile de relire un tel code, il sera donc compliqué de le faire évoluer, de le maintenir, voire même simplement de le réaliser.

C'est là qu'interviennent les types étiquetés ou TAGGED en Ada. L'idée du type étiqueté est de fournir un type de base T_Unit fait pour les unités de base, puis d'en faire des «produits dérivés» appelés T_Mounted_Unit, T_Archer_Unit ou T_Artillery_Unit enrichis par rapport au type initial. Pour cela nous allons d'abord étiqueter notre type T_Unit, pour spécifier au compilateur que ce type pourra être dérivé en d'autres types (attention, on ne parle pas ici d'héritage mais de dérivation, nuance) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package P_Unit is

   type T_Unit is tagged private ;

   function Init(Att, Def, Mov, Vie : Integer := 0) return T_Unit ; 
   procedure Attaque(Attaquant, Defenseur : in out T_Unit) ;
   procedure Defense(Defenseur : in out T_Unit) ;
   procedure Deplacer(Unite : in out T_Unit) ;
   function Est_Mort(Unite : in T_Unit) return Boolean ;

private

   type T_Unit is tagged record
      Att, Def, Mov, Vie : Integer := 0 ;
   end record ;

end P_Unit ;

Chacun aura remarqué l'ajout du mot TAGGED pour indiquer l'étiquetage du type. Vous aurez également remarqué que le type T_Unit ne fait toujours pas mention des compétences Tir ou Bomb et pour cause. Nous allons maintenant créer un type T_Archer_Unit qui dérivera du type T_Unit initial :

1
2
3
4
5
6
7
8
package P_Unit.Archer is
   type T_Archer_Unit is new T_Unit with private ;
   procedure Attaque_a_distance(Attaquant : in out T_Archer_Unit ; Defenseur : in out T_Unit) ;
private
   type T_Archer_Unit is new T_Unit with record
      tir : Integer := 0 ;
   end record; 
end P_Unit.Archer ;

Vous remarquerez que le type T_Archer_Unit ne comporte pas de mention TAGGED. Normal, c'est un type dérivé d'un type étiqueté, il est donc lui-même étiqueté. L'instruction « TYPE T_Archer_Unit IS NEW T_Unit … » est claire : T_Archer_Unit est un nouveau type d'unité, issu de T_Unit. L'instruction WITH RECORD indique les spécificités de ce nouveau type, les options supplémentaires si vous préférez. À noter que l'instruction WITH n'a pas ici le même sens que lorsque nous écrivons WITH Ada.Text_IO, c'est là toute la souplesse du langage Ada.

Je vous déconseille d'écrire seulement « TYPE T_Archer_Unit IS PRIVATE ». Cela reviendrait à cacher à votre programme Strategy qu'il y a eu dérivation, or nous n'avons pas à le cacher, et vous allez comprendre pourquoi.

Ce code n'est pas encore opérationnel !

Et nos méthodes ?

Méthodes héritées de la classe mère

Mais ?!? Comme T_Archer_Unit n'est pas un SUBTYPE de T_Unit, notre procédure Attaque() ne va plus marcher avec les archers ?! :o

Eh bien si ! Un type étiqueté hérite automatiquement des méthodes du type père ! C'est comme si nous bénéficiions automatiquement d'une méthode « PROCEDURE Attaque(Attaquant, Defenseur : IN OUT T_Archer_Unit) » sans avoir eu besoin de la rédiger. On dit que la méthode est polymorphe puisqu'elle accepte plusieurs «formes» de données. Sympa non ?

Méthodes s'appliquant à l'intégralité de la classe

Attend ! T'es en train de me dire que les archers ne pourront attaquer que d'autres archers ? Comment je fais pour qu'un archer puisse attaquer une unité à pied ou un cavalier ? Je dois faire du copier-coller et surcharger la méthode Attaque ?

En effet, c'est problématique et ça risque d'être long. Heureusement, vous savez désormais que le type T_Archer_Unit est de la classe de type de T_Unit, il suffit simplement de revoir notre méthode pour qu'elle s'applique à toute la classe T_Unit, c'est-à-dire le type T_Unit et sa descendance ! Il suffira que le paramètre Defenseur soit accompagné de l'attribut 'class :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package P_Unit is

   type T_Unit is tagged private ;

   function Init(Att, Def, Mov, Vie : Integer := 0) return T_Unit ; 
   procedure Attaque(Attaquant : in T_Unit ; Defenseur : in out T_Unit'class) ;
   procedure Defense(Defenseur : in out T_Unit) ;
   procedure Deplacer(Unite : in out T_Unit) ;
   function Est_Mort(Unite : in T_Unit) return Boolean ;

private

   type T_Unit is tagged record
      Att, Def, Mov, Vie : Integer := 0 ;
   end record ;

end P_Unit ;

Vous remarquerez que je n'abuse pas de cet attribut puisque tous les types dérivant de T_Unit hériteront automatiquement des autres méthodes. De même, nous pouvons modifier notre package P_Unit.Archer pour que l'attaque à distance s'applique à toute la classe de type T_Unit :

1
2
3
4
5
6
7
8
package P_Unit.Archer is
   type T_Archer_Unit is new T_Unit with private ;
   procedure Attaque_a_distance(Attaquant : in T_Archer_Unit ; Defenseur : in out T_Unit'class) ;
private
   type T_Archer_Unit is new T_Unit with record
      tir : Integer := 0 ;
   end record; 
end P_Unit.Archer ;

Grâce à l'attribut 'class, nous avons donc indiqué que notre méthode ne s'applique pas qu'à un seul type mais à toute une classe de type. Compris ? Bien, nous allons maintenant créer le type T_Artillery en le dérivant de T_Archer_Unit. Enfin, quand je dis «nous», je veux dire «vous allez créer ce type» ! Moi, j'ai assez travaillé :soleil: .

1
2
3
4
5
6
7
8
package P_Unit.Archer.Artillery is
   type T_Artillery_Unit is new T_Archer_Unit with private ;
   procedure Bombarde(Attaquant : in T_Artillery_Unit ; Defenseur : in out T_Unit'class) ;
private
   type T_Artillery_Unit is new T_Archer_Unit with record
      bomb : integer := 0 ;
   end record ;
end P_Unit.Archer.Artillery  ;

Là encore, pas besoin d'étiqueter T_Artillery_Unit puisqu'il dérive du type T_Archer_Unit qui lui-même dérive du type étiqueté T_Unit. De même, notre nouveau type hérite des méthodes des classes T_Unit et T_Archer_Unit.

Eh ! Mais comment on fait pour T_Mounted_Unit vu qu'il n'a aucun attribut de plus que T_Unit ?

La réponse en image (ou plutôt en code source) :

1
2
3
4
5
6
package P_Unit.Mounted is
   Type T_Mounted_Unit is new T_Unit with private ;
   procedure Attaque_de_flanc(Attaquant : in T_Mounted_Unit ; Defenseur : in out T_Unit'class) ;
private
   Type T_Mounted_Unit is new T_Unit with null record ;
end P_Unit.Mounted ;

Eh oui, il suffit d'indiquer « WITH NULL RECORD » : c'est-à-dire «sans aucun enregistrement». Et n'oubliez pas que notre nouveau type héritera directement des méthodes de la classe mère T_Unit, mais pas de celles des classes sœurs.

Surcharge de méthodes

C'est normal que mon code ne compile plus ? GNAT ne veut plus de mon type T_Archer_Unit à cause de la fonction Init ?!? o_O

C'est normal en effet puisque par héritage, la classe de type T_Archer_Unit bénéficie d'une méthode « FUNCTION Init(Att, Def, Mov, Vie : Integer := 0) RETURN T_Archer_Unit ». Sauf que cette méthode a été prévue à la base pour renvoyer un type structuré contenant 4 attributs : att, def, mov et vie. Or le type T_Archer_Unit en contient un cinquième supplémentaire : Tir ! Il y a donc une incohérence et GNAT vous impose donc de remplacer cette méthode ("Init" must be overridden). Pour cela, rien de plus simple, il suffit de réécrire cette méthode :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package P_Unit.Archer is
   type T_Archer_Unit is new T_Unit with private ;
   overriding
   function Init(Att, Def, Mov, Vie : Integer := 0) return T_Archer_Unit ; 
   procedure Attaque_a_distance(Attaquant : in T_Archer_Unit ; Defenseur : in out T_Unit'class) ;
private
   type T_Archer_Unit is new T_Unit with record
      tir : Integer := 0 ;
   end record; 
end P_Unit.Archer ;

Le mot-clé OVERRIDING apparu avec la norme Ada2005, permet d'indiquer que nous réécrivons la méthode et qu'elle remplace donc la méthode mère. Toutefois, ce mot-clé est optionnel et rien ne serait chamboulé si vous le retiriez hormis peut-être la lisibilité de votre code. Notez également qu'aucun attribut Tir n'a été ajouté, la méthode devra donc attribuer une valeur par défaut à cet attribut. Pour disposer d'une véritable fonction d'initialisation, il faudra surcharger la méthode :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package P_Unit.Archer is
   type T_Archer_Unit is new T_Unit with private ;
   overriding
   function Init(Att, Def, Mov, Vie : Integer := 0) return T_Archer_Unit ; 
   function Init(Att, Def, Mov, Vie, Tir : Integer := 0) return T_Archer_Unit ; 
   procedure Attaque_a_distance(Attaquant : in T_Archer_Unit ; Defenseur : in out T_Unit'class) ;
private
   type T_Archer_Unit is new T_Unit with record
      tir : Integer := 0 ;
   end record; 
end P_Unit.Archer ;

Notez qu'il n'est pas nécessaire de remplacer cette méthode pour les T_Mounted_Unit qui sont identiques aux T_Unit, mais que le même travail devra en revanche s'appliquer à la classe de type T_Artillery_Unit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package P_Unit.Archer.Artillery is
   type T_Artillery_Unit is new T_Archer_Unit with private ;
   overriding
   function Init(Att, Def, Mov, Vie, Tir : Integer := 0) return T_Artillery_Unit ; 
   function Init(Att, Def, Mov, Vie, Tir, Bomb : Integer := 0) return T_Artillery_Unit ; 
   procedure Bombarde(Attaquant : in T_Artillery_Unit ; Defenseur : in out T_Unit'class) ;
private
   type T_Artillery_Unit is new T_Archer_Unit with record
      bomb : integer := 0 ;
   end record ;
end P_Unit.Archer.Artillery  ;

Un autre avantage des classes

Vous remarquerez que toutes mes méthodes à deux paramètres ont toujours en premier paramètre l'attaquant. Pourquoi est-ce important ? Eh bien car avec une classe il est possible d'éluder le premier paramètre en écrivant ceci :

1
Chevalier.attaque(Hallebardier) ;

Au lieu du sempiternel et peu parlant :

1
Attaque(Chevalier,Hallebardier) ;

Plus clair non ? Les deux écritures demeurent possibles, mais vous constaterez que l'écriture pointée aura de plus en plus ma préférence (notamment car elle est utilisée dans la plupart des langages orientés objets actuels comme Java… ). Pour que cette écriture puisse être possible il faut que votre type soit étiqueté (qu'il définisse une classe) et qu'il coïncide avec le premier paramètre de vos méthodes ! C'est donc pour cela que l'attaquant était mon premier paramètre : il est plus logique d'écrire Attaquant.attaque(defenseur) que defenseur.attaque(attaquant) ! :-°


En résumé :

  • Pour créer un objet nous devons préalablement créer une classe , c'est-à-dire un type structuré et étiqueté (TAGGED RECORD).
  • Les méthodes s'appliquant à un type TAGGED RECORD seront automatiquement dérivées aux types fils. Pour qu'une méthode s'applique à la classe entière sans effet de dérivation vous devrez utiliser l'attribut 'class.
  • Une classe doit être encapsulée et donc de type PRIVATE ou LIMITED PRIVATE.
  • Pour parfaire l'encapsulation, types et méthodes doivent être enfermés dans un package spécifique. Les types dérivés sont eux enfermés dans des packages-fils : c'est l'héritage.
  • Éventuellement, la classe peut être générique permettant à d'autres programmeurs d'employer vos outils avec divers types de données.