Licence CC BY-NC-SA

La programmation modulaire V : Polymorphisme, abstraction et héritage multiple

Publié :

Ce chapitre est en lien direct avec le précédent : nous allons approfondir nos connaissances sur la dérivation de type et surtout améliorer notre façon de concevoir nos objets. Vous allez le voir, notre façon de procéder pour concevoir nos unités militaires est encore rudimentaire et n'exploite pas toutes les capacités du langage Ada en matière de POO. Il y a encore une propriété fondamentale de la POO que nous n'avons pas complètement exploitée : le polymorphisme.

Polymorphisme

Méthodes polymorphes

Nous allons continuer à bricoler nos packages. Désormais, toutes nos unités peuvent attaquer de front puisque par dérivation du type étiqueté T_Unit, toutes bénéficient de la même méthode attaque(). Malheureusement, s'il semble logique qu'un fantassin ou un cavalier puisse attaquer de front, il est plutôt suicidaire d'envoyer un archer ou une catapulte en première ligne. Il faudrait que les types issus de la classe T_Archer_Unit disposent d'une méthode attaque() spécifique.

En fait il faudrait surcharger la méthode attaque(), c'est ça ?

Pas exactement. Surcharger la méthode consisterait à écrire une seconde méthode attaque() (distincte) avec des paramètres différents (de par leur nombre, leur type ou leur mode de passage). Or, nous bénéficions déjà, par dérivation de type, d'une méthode « Attaque(attaquant : IN T_Archer_Unit ; defenseur : OUT T_Unit'class) ». Nous ne souhaitons pas la surcharger mais bien la réécrire.

Bref, tu veux qu'on utilise OVERRIDING, c'est cela ?

C'est cela en effet. Notre package P_Unit.Archer deviendrait ainsi :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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) ;
   overriding
   procedure Attaque(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 ;

P_Unit-Archer.adb

 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.Archer is

   function Init(Att, Def, Mov, Vie : Integer := 0) return T_Archer_Unit is
   begin
      return (Att, Def, Mov, Vie, 0) ; 
   end Init ; 

   function Init(Att, Def, Mov, Vie, Tir : Integer := 0) return T_Archer_Unit is
   begin
      return (Att, Def, Mov, Vie, Tir) ; 
   end Init ; 

   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 ; 

   procedure Attaque((Attaquant : in T_Archer_Unit ; Defenseur : in out T_Unit'class) is
   begin
      Put_line("   >> Une attaque frontale avec ... des archers ?!? Ecrasez-les !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - 1) ; 
   end Attaque ; 

end P_Unit.Archer ;

P_Unit-Archer.adb

En procédant ainsi, notre méthode de classe Attaque() aura diverses formes selon que ses paramètres seront de la sous-classe T_Archer_Unit ou non. On dit alors que la méthode est polymorphe. Quel intérêt ? Créons par exemple un sous-programme Tour_joueur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
procedure Tour_Joueur(Joueur1, Joueur2 : in out T_Unit'class) is
   choix : character ;
begin
   put_line("Voulez-vous Attaquer ou Defendre ?") ; 
   get(choix) ; skip_line ; 
   if choix='a' or choix='A'
      then Joueur1.attaque(joueur2) ; 
      else Joueur1.defense ;
   end if ; 
end Tour_Joueur ;

Que va afficher la console ? >> Attaque frontale ! ou >> Une attaque frontale avec… des archers ?!? Écrasez-les ! ? Eh bien vous n'en savez rien, même au moment de la compilation ! Tout dépendra des choix effectués par l'utilisateur : selon que Joueur1 sera un archer (ou une artillerie) ou bien un fantassin (ou un cavalier), le programme n'effectuera pas le même travail. Le choix de la méthode attaque() est ainsi reporté au moment de l'utilisation du logiciel, et non au moment de la compilation.

Objets polymorphes

Objets polymorphes non mutants

Poussons le polymorphisme plus loin encore. Nous souhaiterions maintenant proposer un système de héros au joueur. Pas besoin de créer une classe T_Heros, le principe est simple : un héros est une unité presque comme les autres, sauf qu'elle est unique et qu'il sera possible au joueur de choisir sa classe : fantassin, archer, cavalier ou… artillerie ?!? o_O . Nous voudrions donc créer un objet Massacror de la classe de type T_Unit mais qui aurait la possibilité de devenir de type T_Mounted_Unit ou T_Archer_Unit selon la volonté du joueur, et ainsi de bénéficier de telle ou telle capacité.

On n'est pas censé ne pas pouvoir changer de type ? :-° T'arrêtes pas de répéter qu'Ada est un langage fortement typé !

En effet, nos objets ne peuvent changer de type ou de classe. Par exemple, GNAT ne tolèrera pas que vous écriviez ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
   Massacror   : T_Unit ;             --je n'initialise pas mes objets pour plus de clarté ^^
   arbaletrier : T_Archer_Unit ; 
   chevalier   : T_Mounted_Unit ;
   choix       : character ; 
begin
   put("Que voulez-vous devenir ? Arbaletrier ou Chevalier ?") ; 
   get(choix) ; skip_line ; 
   if choix = 'a' or choix = 'A'
      then heros := arbaletrier ; 
      else heros := chevalier ; 
   end if ;

Ce qui est bien embêtant. Heureusement, la rigueur du langage Ada n'exclut pas un peu de souplesse ! Et si nous faisions appel à l'attribut 'class ? Déclarons Massacror :

1
Massacror   : T_Unit'class ;

Problème, GNAT me demande alors d'initialiser mon objet ! Plusieurs choix sont possibles. Vous pouvez utiliser l'une des méthodes d'initialisation, par exemple :

1
Massacror   : T_Unit'class := P_Unit.Archer.Init(150,200,20,200,300) ;  --Méthode de bourrin !

Votre objet sera alors automatiquement défini comme de type T_Archer_Unit. Pas terrible pour ce qui est du choix laissé au joueur. :( Une autre façon est de l'initialiser avec un autre objet :

1
Massacror   : T_Unit'class := Hallebardier ;

Mais on retrouve le même problème : Massacror sera du même type que Hallebardier. Vous pouvez certes utiliser des blocs DECLARE, mais vous risquez de créer un code lourd, redondant et peu lisible. La solution est davantage de demander à l'utilisateur de faire son choix au moment de la déclaration de votre objet à l'aide d'un nouveau sous-programme Choix_Unit. Ce sous-programme renverra non pas un objet de type T_Unit mais de la classe T_Unit'class :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function Choix_Unit return T_Unit'class is
   choix : character ;
begin
   put_line("Archer, Hallebardier ou Chevalier ?") ;
   get(choix) ; skip_line ;
   if choix='a' or choix='A'
      then return P_Unit.Archer.init(6,1,1,8,7) ;
   elsif  choix='H' or choix='h'
      then return P_Unit.init(5,8,1,10) ;
      else return P_Unit.Mounted.init(7,3,2,15) ;
   end if ;
end choix_unit ;

      ...

Massacror    : T_unit'class := choix_unit ;

Ainsi, il est impossible au programmeur de connaître à l'avance le type qui sera renvoyé et donc le type de Massacror ! Génial non ? C'est l'avantage du polymorphisme.

Euh… J'ai comme un doute sur le fait qu'un chevalier puisse lancer des flèches… :colere2: Comment je fais ensuite pour savoir si je peux utiliser Attaque_a_distance ou pas ?

Pas d'affolement, vous pouvez toujours tester l'appartenance de Massacror à tel ou tel type ou classe de type et ainsi éviter les erreurs d'utilisation de méthodes, simplement en écrivant :

1
2
3
4
5
if Massacror in T_Archer_Unit'class   --Est-ce un archer ou une artillerie ? 
   then ...

if Massacror in T_Mounted_Unit        --Est-ce une unité montée ?
   then ...

Objets polymorphes et mutants

Allons toujours plus loin dans le polymorphisme. Pourquoi nos héros ne pourraient-ils pas changer de type ? Un héros, c'est fait pour évoluer au cours du jeu, sinon ça n'a aucun intérêt. Que seraient devenus Songoku, Wolverine, Tintin ou le roi Arthur s'ils n'avaient jamais évolué ? Pourquoi ne pas écrire :

1
2
3
4
5
6
...
   Massacror    : T_unit'class := choix_unit ;
begin
   Massacror := choix_unit ;
   Massacror := choix_unit ;
...

Seulement là, ce n'est pas le compilateur qui va vous arrêter (rien ne cloche du point de vue du langage), mais votre programme. Tout ira bien si vous ne changez pas le type de Massacror. Mais si vous décidez qu'il sera archer, puis chevalier… PATATRA ! raised CONSTRAINT_ERROR : ###.adb:## tag check failed ! Autrement dit, problème lié à l'étiquetage. En effet, après la déclaration-initialisation de votre objet Massacror, celui-ci dispose d'un type bien précis. Le programmeur ne le connaît pas, certes, mais ce type est bien défini, et il n'a pas de raison de changer ! En fait, il n'a surtout pas le droit de changer.

Bref, c'est complètement bloqué. Même l'attribut 'class ne sert à rien. On est vite rattrapé par la rigueur du langage. :colere2:

Rassurez-vous, il y a un moyen de s'en sortir. L'astuce consiste à faire appel aux pointeurs en créant des pointeurs sur la classe entière (le type racine et toute sa descendance).

Argh ! Noooon ! Pas les pointeurs ! :waw:

Rassurez-vous, nous n'aurons pas besoin de notions très poussées (pour l'instant) et je vous ai fait faire bien pire. Comme nous l'avons dit, Ada est un langage à fort typage : il doit connaître à la compilation la quantité de mémoire statique qui sera nécessaire au programme. Or, entre un objet de type T_Unit et un objet de type T_Archer_unit, il y a une différence notable (la compétence Tir notamment). Impossible de redimensionner durant l'exécution l'emplacement mémoire de notre objet Massacror. À moins de faire appel aux pointeurs et à la mémoire dynamique ! Un pointeur n'étant en définitive qu'une adresse, sa taille ne variera pas, qu'il pointe sur un T_Unit ou un T_Archer_Unit ! Nous allons pour cela faire de nouveau appel à l'attribut 'class :

1
2
type T_Heros is access all T_Unit'class ;
Massacror : T_Heros ;

Et, «ô merveille!», plus aucune initialisation n'est nécessaire ! Toutefois, n'oubliez pas que vous manipulerez un pointeur et qu'il peut donc valoir NULL ! Redéfinissons donc notre sous-programme Choix_Unit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function choix_unit return T_Heros is
   choix : character ;
begin
   put_line("Archer, Hallebardier ou Chevalier ?") ;
   get(choix) ; skip_line ;
   if choix='a' or choix='A'
      then return new T_Archer_Unit'(init(6,1,1,8,7)) ;
   elsif  choix='H' or choix='h'
      then return new T_Unit'(init(5,8,1,10)) ;
      else return new T_Mounted_Unit'(init(7,3,2,15)) ;
   end if ;
end choix_unit ;

Désormais, plus rien ne peut vous arrêter. Vous pouvez changer de type à volonté et surtout, user et abuser du polymorphisme :

1
2
3
4
5
6
7
8
9
Massacror, Vengeator : T_Heros ; 
begin
   loop
      Massacror := Choix_Unit ; 
      Vengeator := Choix_Unit ; 
      Tour_Joueur(Massacror.all,Vengeator.all) ; 
      Tour_Joueur(Vengeator.all,Massacror.all) ; 
   end loop ;
...

Et là, bien malin sera le programmeur qui saura vous dire si le programme affichera >> Une attaque frontale avec… des archers ?!? Écrasez-les ! ou >> Attaque frontale !. Le compilateur lui-même ne saurait vous le dire puisque les objets sont polymorphes et mutants (et donc leur type dépend du bon vouloir du joueur) et que le sous-programme Tour_Joueur fait appel, je vous le rappelle, à la méthode Attaque() qui est elle-même polymorphe !

Pour plus de lisibilité, je n'ai nullement pris la peine désallouer la mémoire avant d'en réallouer aux pointeurs Massacror et Vengeator, mais vous savez bien sûr que c'est une bien mauvaise pratique. Pour un projet plus sérieux, pensez à Ada.Unchecked_Deallocation !

Abstraction

Types abstraits

Compliquons encore la chose, nous souhaiterions cette fois créer une super classe T_Item dont toutes les autres hériteraient : les unités civiles (notées T_Civil), les bâtiments (T_Building) et même les unités militaires (T_Unit) ! À quoi correspondrait la classe T_Item dans notre jeu de stratégie ? Pas à grand chose, je vous l'accorde, mais cela permettrait de mieux hiérarchiser notre code et de regrouper sous une même classe mère les unités militaires du jeu, les unités civiles et les bâtiments ! Et qui dit «même classe mère» dit mêmes méthodes. Cela nous permettra d'utiliser les mêmes méthodes Est_Mort() ou Defend() pour toutes ces classes, et d'user du polymorphisme autant que nous le souhaiterons.

Diagramme de la classe T_Item

Il est évident qu'aucun objet du jeu ne sera de type T_Item ! Tous les objets appartenant à la classe T_Item seront de type T_Unit, T_Civil, T_Mounted_Unit… mais jamais (JAMAIS) de type T_Item. Ce type restera seulement théorique : on parle alors de type abstrait et le terme Ada correspondant (apparu avec la norme Ada95) est tout simplement ABSTRACT ! Dès lors, vous vous en doutez, nous devrons dériver ce type car il sera impossible de l'instancier. Conséquence, un type abstrait (ABSTRACT) est obligatoirement étiqueté (TAGGED). Prenons un exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type T_Item is abstract tagged record
   Def, Vie : Integer ;
end record ;

      --   OU ENCORE

type T_Item is abstract tagged private ; 
...
private
   type T_Item is abstract tagged record
      Def, Vie : Integer ;
   end record ;

Cela nous obligera à revoir la définition de notre type T_Unit :

1
2
3
type T_Unit is new T_Item with record
   Att, Mov : Integer ; 
end record ;

Je répète : il est impossible d'instancier un type abstrait, c'est-à-dire de déclarer un objet de type abstrait !

Très souvent, le type abstrait est même vide : il ne contient aucun attribut et n'existe que comme une racine à l'ensemble des classes :

1
type T_Item is abstract tagged null record

Un autre intérêt des classes abstraites est de pouvoir déclarer un seul type de pointeur pointant sur l'ensemble des sous-classes grâce à l'attribut 'class. Cette possibilité nous permettra de jouer à fond la carte du polymorphisme :

1
type T_Ptr_Item is access all T_Item'class ;

Méthodes abstraites

Qui dit type abstrait dit évidemment, méthodes abstraites. Comme pour les types, il s'agit de méthodes théoriques, inutilisables en l'état : elles auront donc seulement des spécifications et pas de corps (celui-ci sera défini plus tard, avec un type concret) :

1
2
procedure defense(defenseur : in out T_Item'class) is abstract ;
function Est_Mort(Item : in T_Item'class) return Boolean is abstract ;

Autre façon de déclarer des méthodes abstraites :

1
2
procedure defense(defenseur : in out T_Item'class) is null ;
function Est_Mort(Item : in T_Item'class) return Boolean is null ;

Le terme NULL indique que cette fonction ne fait absolument rien, cela revient à écrire :

1
2
3
4
procedure defense(defenseur : in out T_Item'class) is
begin
   null ; 
end defense ;

Mais qu'est-ce que l'on peut bien faire avec des méthodes abstraites ? o_O

Eh bien, comme pour les types abstraits, on ne peut rien en faire en l'état. Le moment venu, lorsque vous disposerez d'un type concret ou mieux, d'une classe de types concrète, il vous faudra réaliser le corps de cette méthode en la réécrivant à l'aide de l'instruction OVERRIDING (c'est pourquoi certains langages parlent plutôt de méthodes retardées). Ces méthodes abstraites nous permettront d'établir une sorte de «plan de montage», une super-méthode pour super-classe, applicable tout autant aux bâtiments qu'aux unités civiles ou militaires. Elles nous évitent de disposer de méthodes distinctes et incompatibles d'un type à l'autre. Bref, ces méthodes abstraites permettent de conserver le paradigme de polymorphisme (et c'est très important en POO). Tout cela nous amène au package suivant (attention, il n'y a pas de corps pour ce package) :

1
2
3
4
5
6
7
8
9
package P_Item is
   type T_Item is abstract tagged private ;
   procedure Defense(I : in out T_Item'class) is abstract ; 
   function Est_Mort(I : T_Item'class) return boolean is abstract ;
private
   type T_Item is abstract tagged record
      def,vie : integer ;
   end record ;
end P_Item ;

P_Item.ads

Et nous oblige à modifier nos anciens packages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package P_Item.Unit is
   type T_Unit is new T_Item with private ;
   procedure Init(Unite : out T_Unit'class ; Att, Def, Mov, Vie : Integer := 0) ;
   procedure Attaque(Attaquant : in T_Unit'class ; Defenseur : in out T_Unit'class ) ;
   procedure Defense(Defenseur : in out T_Item'class) ;         --Réalisation concrète de la méthode Defense
   procedure Deplacer(Unite : in out T_Unit'class ) ;
   function Est_Mort(Unite : in T_Item'class ) return Boolean ; --Réalisation concrète de la méthode Est_Mort
private
   type T_Unit is new T_Item with record
      Att, Mov : Integer := 0 ;
   end record ;
end P_Item.Unit ;

P_Item-Unit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package P_Item.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, Defenseur : in out T_Unit) ;
private
   type T_Archer_Unit is new T_Unit with record
      tir : Integer := 0 ;
   end record;
end P_Item.Unit.Archer ;

P_Item-Unit-Archer.ads

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package P_Item.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 out T_Artillery_Unit'class ; 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_Item.Unit.Archer.Artillery  ;

P_Item-Unit-Archer-Artillery.ads

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

P_Item-Unit-Mounted.ads

Si vous souhaitez que les méthodes defense et Est_Mort soient les mêmes pour les types T_Unit, T_Civil, T_Building…, il est toujours possible de déclarer des méthodes non abstraites ayant un paramètre de type T_Item. Et bien sûr, votre type T_Item ne doit pas être vide. Il n'y a pas d'obligation à créer des méthodes abstraites.

Héritage multiple

Comment Ada gère-t-il l'héritage multiple ?

Nous souhaiterions désormais créer une nouvelle classe d'objet : T_Mounted_Archer ! Pour jouer avec des archers montés disposant à la fois des capacités des archers mais également de celles des unités montées (des cavaliers huns ou mongols par exemple ^^ ). Cette classe dériverait des classes T_Archer_Unit et T_Mounted_Unit comme l'indique le diagramme de classe ci-dessous :

Parenté de la classe T_Mounted_Archer

Le seul soucis… c'est qu'en Ada, comme en Java, il n'y a pas d'héritage multiple !

Hein ?!? Mais y a tromperie sur la marchandise ! C'était marqué dans le titre : héritage multiple. Et pourquoi tu nous parles du langage Java ? Je comptais pas tout réapprendre maintenant que j'ai lu presque tout ce chapitre ! :colere:

Du calme, du calme… vous devez comprendre au préalable que le diagramme ci-dessus pose un sérieux problème (appelé problème du diamant en raison de sa forme en losange). Les types T_Mounted_Unit et T_Archer_Unit héritent tous deux de la classe de type T_Unit. Ils héritent donc de la méthode Attaque(). Oui, mais celle-ci a été redéfinie pour les archers, souvenez-vous ! Et puisque T_Mounted_Archer devrait hériter des méthodes de T_Mounted_Unit et de T_Archer_Unit, que doit afficher la méthode Attaque() pour un archer monté ? >> Une attaque frontale avec… des archers ?!? Écrasez-les ! ou >> Attaque frontale ! ? Il y a un sérieux conflit d'héritage alors que l'atout majeur du langage Ada est censé être la fiabilité. :( Voilà pourquoi l'héritage multiple n'est pas autorisé en Ada comme en Java.

Et si je vous parle du langage Java, c'est parce qu'il s'agit d'un langage entièrement tourné vers la Programmation Orientée Objet : c'est à lui notamment que je pensais quand je vous disais que certains langages comportaient un mot clé class. En revanche, le langage Ada, même s'il disposait de très bons atouts pour aborder la POO, n'était pas conçu pour cela à l'origine. La norme Ada95 aura résolu cette lacune grâce à l'introduction du mot clé TAGGED et de l'attribut 'class, entre autres. Mais concernant l'héritage multiple et les risques de sécurité qu'il pouvait entraîner, point de solutions. Et c'est du côté du langage Java qu'est venue l'inspiration. Ce dernier n'admet pas non plus d'héritage multiple pour ses class mais dispose d'une entité supplémentaire : l'interface ! La norme Ada2005 s'en est inspirée pour fournir à son tour des types INTERFACE.

Qu'est-ce qu'une INTERFACE ? C'est simplement un type super-abtrait disposant de méthodes obligatoirement abstraites et qui peut être hérité plusieurs fois par une classe (et sans risques). Nous allons donc créer un type interface T_Mounted qui bénéficiera d'une méthode Attaque_de_flanc() et dont hériteront les types T_Mounted_Unit et T_Mounted_Archer. Ce qui nous donnera le diagramme suivant :

Diagramme de classe utilisant une interface

Vous aurez remarqué que les flèches d'héritage sont en pointillés pour les interfaces et que leur nom est précédé du terme <<interface>. On ne dit d'ailleurs pas «hériter d'une interface» mais «implémenter une interface».

Réaliser des interfaces Ada

Avec une seule interface

Venons-en maintenant au code :

1
2
3
4
5
6
with P_Item.Unit ;       use P_Item.Unit ; 

package I_Mounted is
   type T_Mounted is interface ; 
   procedure Attaque_de_flanc(Attaquant : in T_Mounted ; Defenseur : in out T_Unit'class) is abstract ; 
end I_Mounted ;

Et le corps du package ?

Le corps ? Quel corps ? Le type T_Mounted étant une INTERFACE, il est 100% abstrait et ses méthodes associées (comme Attaque_de_flanc()) doivent obligatoirement être abstraites. Donc ce package ne peut pas avoir de corps. De plus, il est recommandé de créer un package spécifique pour votre type interface si vous ne voulez pas que GNAT rechigne à compiler. C'est d'ailleurs pour cela que mon package s'appelle I_Mounted et non P_Mounted (I pour INTERFACE bien sûr).

Bien ! Notre interface et ses méthodes étant créées, nous pouvons revenir aux spécifications de notre type T_Mounted_Unit qui doit désormais implémenter notre interface :

1
2
3
4
5
6
7
8
9
With I_Mounted ;      Use I_Mounted ; 

package P_Item.Unit.Mounted is
   type T_Mounted_Unit is new T_Unit and T_Mounted with private ;
   overriding
   procedure Attaque_de_flanc(Attaquant : in T_Mounted_Unit ; Defenseur : in out T_Unit'class) ;
private
   type T_Mounted_Unit is new T_Unit and T_Mounted with null record ; 
end P_Item.Unit.Mounted ;

P_Item-Unit-Mounted.ads

Il n'est pas utile de modifier le corps de notre package puisque la procédure Attaque_de_flanc() existait déjà. Comme vous avez du vous en rendre compte, il suffit d'ajouter « AND T_Mounted » à la définition de notre type pour implémenter l'interface.

Vos types dérivés ne peuvent pas dériver d'une interface, seulement d'un type étiqueté. L'ordre de déclaration est important : d'abord, le type étiqueté père puis la ou les interfaces. Ce qui nous donne le schéma suivant : « type MonNouveauType is new TypeEtiquetéPère and InterfaceMère1 and InterfaceMère2 and... with... »

Venons-en maintenant à notre objectif principal : créer un type T_Mounted_Archer.

1
2
3
4
5
6
7
8
9
With I_Mounted ;      Use I_Mounted ; 

package P_Item.Unit.Archer.Mounted is
   type T_Mounted_Archer is new T_Archer_Unit and T_Mounted with private ;
   overriding
   procedure Attaque_de_flanc(Attaquant : in T_Mounted_Archer ; Defenseur : in out T_Unit'class) ;
private
   type T_Mounted_Archer is new T_Archer_Unit and T_Mounted with null record ; 
end P_Item.Unit.Archer.Mounted ;

P_Item-Unit-Archer-Mounted.ads

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

package body P_Item.Unit.Archer.Mounted is
   procedure Attaque_de_flanc(Attaquant : in T_Mounted_Archer ; Defenseur : in out T_Unit'class) is
   begin
      Put_line("   >> Décochez vos flèches sur le flanc ouest !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.tir*Attaquant.mov) ;
   end Attaque_de_flanc ; 
end P_Item.Unit.Archer.Mounted ;

P_Item-Unit-Archer-Mounted.adb

Le code est finalement très similaire à celui des unités montées, à ceci près que vous devez écrire le corps du package cette fois. Remarquez que j'ai changé la méthode de calcul ainsi que le texte affiché, mais vous pouvez bien-sûr écrire exactement le même code que pour les T_Mounted_Unit.

Mais quel intérêt de réécrire la même chose ? Je pensais que l'héritage nous permettait justement d'éviter cette redondance de code? :(

J'avoue avoir eu moi aussi du mal à comprendre cela à mes débuts. Mais le but principal des types abstraits et des interfaces n'est pas de limiter la redondance du code, mais de permettre le polymorphisme. Sans interface, nous aurions deux méthodes Attaque_de_flanc distinctes ; avec l'interface, ces deux méthodes n'en forme plus qu'une, mais polymorphe ! Souvenez-vous de nos héros : ils pouvaient changer de classe sans que le compilateur ne le sache. Il est donc important de privilégier une méthode polymorphe plutôt qu'une méthode simplement surchargée.

Avec plusieurs interfaces

Dans le même esprit, pourquoi ne pas créer une interface I_Archer ? Ainsi, les archers montés hériteraient directement de T_Unit et implémenteraient I_Mounted et I_Archer. La réalisation de l'interface I_Archer n'est pas très compliquée :

1
2
3
4
5
6
with P_Item.Unit ;       use P_Item.Unit ; 

package I_Archer is
   type T_Archer is interface ; 
   procedure Attaque_a_distance(Attaquant : in T_Archer ; Defenseur : in out T_Unit'class) is abstract ; 
end I_Archer ;

Que devient alors notre type T_Archer_Unit ? Là encore, rien de bien compliqué :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
With I_Archer ;      Use I_Archer ; 

package P_Item.Unit.Archer is
   type T_Archer_Unit is new T_Unit and T_Archer 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 ; 

      -- Réécriture de la méthode héritée de l'interface I_Archer
   overriding
   procedure Attaque_a_distance(Attaquant : in T_Archer_Unit ; Defenseur : in out T_Unit'class) ;

      -- Réécriture de la méthode héritée de la classe T_Unit
   overriding
   procedure Attaque(Attaquant : in T_Archer_Unit ; Defenseur : in out T_Unit'class) ;

private
   type T_Archer_Unit is new T_Unit and T_Archer with record 
      Tir : integer ; 
   end record ; 
end P_Item.Unit.Mounted ;

P_Item-Unit-Archer.ads

Mais ce qui nous intéresse vraiment, c'est notre type T_Mounted_Archer :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
With I_Mounted ;      Use I_Mounted ; 

package P_Item.Unit.Archer.Mounted is
   type T_Mounted_Archer is new T_Archer_Unit and T_Mounted and T_Archer with private ;
   overriding
   procedure Attaque_de_flanc(Attaquant : in T_Mounted_Archer ; Defenseur : in out T_Unit'class) ;
   overriding
   procedure Attaque_a_distance(Attaquant : in T_Mounted_Archer ; Defenseur : in out T_Unit'class) ;
private
   type T_Mounted_Archer is new T_Archer_Unit and T_Mounted and T_Archer with null record ; 
end P_Item.Unit.Archer.Mounted ;

P_Item-Unit-Archer-Mounted.ads

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
With ada.text_IO ;                Use Ada.Text_IO ; 

package body P_Item.Unit.Archer.Mounted is
   procedure Attaque_de_flanc(Attaquant : in T_Mounted_Archer ; Defenseur : in out T_Unit'class) is
   begin
      Put_line("   >> Décochez vos flèches sur le flanc ouest !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.tir*Attaquant.mov) ;
   end Attaque_de_flanc ; 

   procedure Attaque_a_distance(Attaquant : in T_Mounted_Archer ; Defenseur : in out T_Unit'class) is
   begin
      Put_line("   >> Parez ... Tirez !") ; 
      Defenseur.vie := integer'max(0,Defenseur.vie - Attaquant.tir) ;
   end Attaque_a_distance ; 
end P_Item.Unit.Archer.Mounted ;

P_Item-Unit-Archer-Mounted.adb

Comme vous pouvez le constater, mon type T_Mounted_Archer hérite désormais ses multiples attributs du type T_Archer_Unit et de deux interfaces I_Archer et I_Mounted. Il est ainsi possible de multiplier les implémentations : pourquoi ne pas imaginer des classes implémentant 4 ou 5 interfaces ?

Interface implémentant une autre interface

Et nos artilleries ? Nous pourrions également créer une interface I_Artillerie ! Et comme les artilleries héritent des archers, notre interface I_Artillerie hériterait de I_Archer.

Attends, là. Une interface peut hériter d'une autre interface ? o_O

Bien sûr ! Tout ce que vous avez à faire, c'est de lui adjoindre de nouvelles méthodes (abstraites bien sûr). Cela se fait très facilement. Regardez ce que cela donnerait avec nos artilleries :

1
2
3
4
5
6
7
with I_Archer ;       use I_Archer ; 
with P_Item.Unit ;    use P_Item.Unit ; 

package I_Artillery is
   type T_Artillery is interface and T_Archer ; 
   procedure Bombarde(Attaquant : in T_Artillery ; Defenseur : in out T_Unit'class) ;
end I_Artillery ;

Inutile de préciser que T_Artillery hérite de la méthode abstraite Attaque_a_distance() et qu'elle pourrait hériter de plusieurs interfaces (si l'envie vous prenait). Bien. Avant de clore ce chapitre, il est temps de faire un point sur notre diagramme de classe. Celui-ci a en effet bien changé depuis le début du chapitre précédent. Je vous fais un résumé de la situation ? :D

Diagramme de classe final du projet

Fatigués ? C'est normal, vous venez de lire un chapitre intense. Polymorphisme et abstraction ne sont pas des notions évidentes à appréhender. Rassurez-vous, nous en aurons bientôt terminé avec la POO. Il nous reste toutefois un dernier chapitre à aborder avant de nous faire la main sur un nouveau TP : la finalisation et les types contrôlés. Ce chapitre sera bien plus court que les précédents. Alors, encore un peu de courage !


En résumé :

  • Pour qu'une même méthode ait des effets différents selon les sous-classes, il faut créer une méthode polymorphe en précisant : « OVERRIDING ».
  • Le type ABSTRACT hiérarchise et regroupe sous une même classe, différents types, permettant de profiter du polymorphisme.
  • Vous pouvez créer des méthodes abstraites qui devront ensuite être réécrite pour les classes filles.
  • En Ada, l'héritage multiple est contourné grâce aux interfaces. N'importe quelle classe peut implémenter plusieurs interfaces, ce qui permet de garantir le polymorphisme des méthodes.