POURQUOI CE BLOG, POUR QUI ?


POURQUOI CE BLOG, POUR QUI ?
Ce Blog s'adresse à tous ceux qui sont passionnés par les sciences informatiques , Professionnels,Etudiants,Amateurs ...
Les sujets exposés dans la suite se rapporteront essentiellement sur l'analyse informatique,la programmation,le développement ainsi que à l'architecture IT.
QUI SUIS JE ?
Je suis Kangulungu Lubilanji, Consultant-Freelance sur les technologies .NET,C#,ASP.NET ... Contactez moi pour plus d'informations.

La Programmation OO: Chapitre 12. Redéfinition des méthodes

Chapitre 12.  Redéfinition des méthodes
Ce chapitre décrit une des possibilités offertes par l'héritage et qui est à la base du polymorphisme : la redéfinition dans les sous-classes de méthodes,d'abord définies dans la superclasse. La mise en oeuvre de cette pratique et le résultat surprenant, de ces effets, tant pendant la phase de compilation que lors de celle de l'exécution, seront analysés en profondeur.

La redéfinitions des méthodes, nous avons vu que l'héritage permet à des classes d'être à la fois elles-même, et en même temps un ensemble successif de superclasses. Elles sont elles-même car, en plus des caractéristiques qu'elles héritent de leur parent (avec « s » ou sans « s »), grand-parent et arrière-grand-parent, elles peuvent rajouter des attributs et des méthodes qui leur sont propres. L'héritage permet également un mécanisme supplémentaire, un tant soit peu plus subtil, mais extrêmement puissant : la redéfinition de méthodes déjà existantes chez le ou les parent.
Comme indiqué dans le diagramme UML ci-après, il s'agit de récupérer la même signature de méthode que celle déclarée chez le père, mais d'en modifier le corps d'instruction. En substance,  la classe père et la classe fils partagent une activité, bien qu'elles l'exécutent différemment. Ce qu'elles partagent en réalité c'est le nom de l'activité, mais pas la pratique à proprement parler. Dans le code correspondant à ce diagramme, on constate que le corps d'instructions de la version du fils de cette méthode partagée avec le père fait d'abord appel à  la version du père, avant d'y mettre son grain de sel. Le mot base sert simplement de référent vers la superclasse. En son absence, on se serait retrouvé en présence d'une dangereuse boucle récursive infinie. Il est, de ce fait, indispensable, afin de préciser la version de la méthode dont il s'agit : celle du fils ou celle du père. C'est un type d'écriture très souvent rencontré.
  class O1 {
        public void jefaisPresqueLaMemeChose()
        {
            Console.WriteLine("test");
        }
    }
    class FilsO1:O1
    {
        public void jefaisPresqueLaMemeChose()
        {
            base.jefaisPresqueLaMemeChose();  
        }
    }
Pourquoi l'application de ce principe de redéfinition des méthodes, participant de l'héritage, est-elle très courantes en OO ?

Beaucoup de verbiage mais peu d'actes véritables, l'héritage s'est inspiré de nos mécanismes d'organisation cognitif, pour transposer les avantages qu'ils permettent : simplicité, économie, adaptabilité, flexibilité, réemploi, au développement logiciel. Une autre caractéristique de notre conceptualisation du monde est que nous consacrons moins de concepts à en décrire les propriétés fonctionnelles et actives que les simples propriétés structurelles, comme si l'image que nous nous faisons de la nature était moins riche en fonctionnalité qu'en structure.

Le vocabulaire que nous dédions aux modalités actives est moins riches que celui dédié à la perception statique des choses. C'est d'ailleurs une des raisons fondamentales qui expliquent que nous organisons notre conceptualisation de manière taxonomique : nous regroupons toutes les classes qui partagent les mêmes modalités actives. Tous les animaux, les millions d'espèces existantes, vivent, mangent, dorment et meurent. Il le font sans doute d'une manière qui leur est propre, mais ils le font tous. Les chanteurs d'opéra, de rock, de folk, de jazz, de gospel..., chantent tous, font tous des disques, passent à la télé mais, heureusement pour nous,  de façon différente et pas en même temps.

Il n'est dès lors pas surprenant de retrouver des mêmes noms d'activité, ici de méthode, pour les classes et leurs sous-classes. Notez que cette mise en commun des noms d'activités à différents niveaux hiérarchiques prend toutes sa raison d'être, tant dans la pratique cognitive qu'en programmation, dans des situations ou ces activités sont mises en pratiques par une tierce « entité ». Cette possibilité offerte à une classe d'interagir avec un ensemble d'autres classes, en leur envoyant un même message, compris par toutes mais exécuté de manière différente explique pour une grande part que l'on retrouve ce message à plusieurs niveaux. Elle est illustré par le diagramme UML qui suit.

Dans le diagramme de classe précédent, un objet de la classe 02 déclenche le même message sur tous les objets issus de la superclasse 01, mais ce même message sera exécuté différemment en fonction de la sous-classe finale dont est issu l'objet recevant ce message. Une classe peut donc interagir avec un ensemble d'autres comme s'il s'agissait d'une seule et même classe. Elle n'a pas nécessairement besoin d'en connaître la nature pour en disposer. En fait tout un large pan du programme lui devient invisible. C'est une nouvelle forme d'encapsulation si chère à l'OO. La « tierce classe » devient complètement aveugle aux spécifications des différentes sous-classes avec lesquelles elle interagira en dernier ressort. Ces spécification constitueront ainsi pour le programmeur un large espace de liberté et de variabilité.

La base du polyporphisme, l'héritage offre la possibilité pour une classe de s'adresser à une autre, en sollicitant de sa part un service qu'elle est capable d'exécuter de mille manières différentes, selon que ce service, nommé toujours d'une seule façon, se trouve redéfini dans autant de sous-classes, toutes héritant du destinataire du message. C'est la base du polymorphisme. Cela permet à notre classe de traiter toutes ses classes interlocutrices comme une seule, et de ne modifier en rien son comportement si on ajoute une de celles-ci ou si une de celles-ci modifie sa manière de répondre aux message de la première : simplicité de conception, économie d'écriture et évolution sans heurt : tout l'OO est là, dans le polymorphisme.  

Ce faisant nous rentrons en plein dans la pratique du «  polymorphise  ». Ce message, au niveau de la superclasse, possédera, oui ou non, un corps d'instruction par défaut. Nous verrons que, dans un type particulier de classe (appelée classe « abstrait  »), le message pourra se borner à n'exister qu'en tant que seule signature. Une sous-classe, au moins, deviendra indispensable afin d'en permettre une première réalisation.

Un match de football polymorphique, Nous allons illustrer ce mécanisme en nous repenchant sur notre simulation du match de football que nous avions juste esquissée précédemment. Dans un premier temps, nous avions délibérément évité toute mise en pratique de l'héritage. Or, si une classe se prête assez naturellement à cette mise en pratique, c'est bien la classe Joueur, comme montré dans le diagramme qui suit. Nous spécialisons la classe  Joueur,en trois sous-classes : Attaquant, Defenseur, Gardien. Rien, du côté des attributs, ne particularise vraiment les différentes sous-classes de joueur, sans doute un tenue un peu différente pour le gardien de but.
Y a-t-il lieu de rajouter de nouvelles méthodes dans les sous classes ? Là encore, le gardien de but peut, sans essuyer de punitions de la part de l'arbitre, attraper la balle avec les mains. Le mode d'interaction entre les joueurs et la balle sera donc quelque peu différent dans le cas du gardien qui peut, dans un premier temps faire comme tous les joueurs c'est à dire lui donner de violents coups de pied, mais, de surcroît, la caresser de ses mains.

Dans ce diagramme UML, aucun nouvel attribut ni méthode ne vient se rajouter dans les sous-classes de joueur. Ce qu'octroie l'héritage ici est la redéfinition des méthodes : interagitBalle() pour le gardien de but et balle et avance() pour tous les joueur. Pour illustrer le polymorphisme de la méthode avance(), le scénario imaginé, est un entraîneur excité et paniqué en fin de partie, qui hurle d'avancer à tous ses joueur. C'est son envoi de message à lui. Sans doute le dernier avant longtemps. Chaque joueur va donc avancer, mais tous le feront à leur manière. Surtout, dans ce hurlement de la dernière chance. Il n'est plus question pour l'entraîneur d'en particulariser le contenu en fonction des joueurs auxquels il est adressé. Que ceux-ci exécutent à leur manière les ordres, ainsi qu'ils ont appris à le faire pendant les entraînements.
Tous les joueur se déplaceront, mais respectant une zone de déplacement sur le terrain, liée à la place qu'ils occupent ainsi qu'aux placements des joueur adverses. Bel exemple de polymorphisme. Nous allons d'ailleurs illustrer cette première mise en musique de polymorphisme en langage C#, et de manière très graduelle, afin d'en expliquer les avantages, les subtilités syntaxiques.

la classe Balle : 
  class Balle {
        public Balle() { }
        public void bouge(){
            Console.WriteLine("la balle bouge");
        }
    }

la classe Joueur : 
class Joueur {
        private int posSurLeTerrain;
        private Balle laBalle;

        public Joueur(Balle laBalle) {
            this.laBalle = laBalle;
        }

        public virtual void interagitBalle(){ /* le mot-clé virtual */
            Console.WriteLine("Je tape la balle avec le pieds");
            laBalle.bouge();
        }
        public virtual void avance()  /* le même mot-clé virtual */
        {
            Console.WriteLine("la postion actuelle du " + this + " est " + posSurLeTerrain );
            laBalle.bouge();
            posSurLeTerrain += 20;
        }
        public int positionGet {
            get {
                return posSurLeTerrain;
            }
            set{
                posSurLeTerrain = value;
            }
        }
    }
posSurLeTerrain est un attribut clé qui indiquera la position du joueur à un instant donné sur le terrain. Dans un  souci de simplicité, on ne spécifie q'une valeur, qui pourrait être la distance par rapport au but. 
C'est la valeur maximale de cette distance, selon la nature des joueurs, qui imposera de redéfinir leur déplacement. Comme nous utiliserons quelque fois cet attribut dans les sous-classes à venir; des méthodes d'accès, get() et set(), deviennent nécessaires. En plus de récupérer et d'afficher la classe de l'objet en question, à chaque exécution de la méthode avance(), le joueur incrémente sa position de 20.
On note l'apparition de mot clé virtual au début de la signature des méthodes qui vont se prêter à une redéfinition dans les sous-classes.
Précisons la nature des joueurs, trois sous-classes : Gardien, Defenseur, Attaquant vont hériter de la classe Joueur. Dans la classe Gardien, les deux méthodes interagitBalle() et avance() seront redéfinies alors que dans les deux autres sous-classes, seule la méthode avance() le sera.

Redéfinir une méthode dans une sous-classe (ce qu'en anglais on désigne comme la pratique override ) consiste à reprendre exactement sa signature, à ceci près que l'on rend l'accès à la méthode dans la sous-classe moins sévère qu'il ne l'est dans la superclasse. Un public ou protected peut remplacer un private, mais non l'inverse, par simple respect du principe de substitution. Une superclasse ne peut en faire plus qu'une sous-classe, pas plus qu'une méthode dans une sous-classe ne peut se rendre moins accessible que celle qu'elle redéfinit dans la superclasse. Si je peux envoyer un message à tous les objets issus d'un superclasse, je dois pouvoir envoyer ce même message à tous les objets issus de la sous-classes de celle-ci. A Priori, une méthode définie comme private dans la superclasse, étant inaccessible et de l'extérieur et par ses enfants, ne devrait jamais pouvoir se prêter à une redéfinition.

La redéfinition des méthodes, une méthode redéfinie dans une sous-classe possédera la même signature que celle définie dans la superclasse avec, comme unique différence possible, un mode d'accès moins restrictif. En général, une méthode définie protected ou public dans la superclasse sera redéfinie comme protected ou public dans la sous-classe.

Penchons-nous sur le constructeur de Gardien comme dans le code ci-dessous. Celui-ci fait appel par l'entremise de base, au constructeur de la superclasse. Comme l'attribut laBalle trouve son origine dans la superclasse Joueur, c'est automatiquement le constructeur de celle-ci (pour autant qu'il s'en occupait dans la superclasse) qui devra à nouveau prendre en charge son initialisation dans la sous-classe.

La redéfinition des deux méthodes. La méthode interagitBalle(), redéfinie seulement chez le gardien, se comporte, dans un premier temps, comme la méthode déclarée initialement chez le Joueur (et c'est de nouveau la raison de l'utilisation du mot-clè base, indispensable afin d'éviter une récursion infinie), mais ensuite rajoute une fonctionnalité qui lui est propre. La méthode avance(), quand à elle, ne se produira que dans les limites permises par la fonction et le placement de chacun des joueurs.

Conditionner dans la redéfinition de la méthode l'appel à la méthode original est également une pratique assez courante. Une sous-classe a quelquefois ceci de plus spécifique que ses attributs, au contraire de ce qui se passe pour la superclasse, ne peuvent prendre toutes les valeurs. Ainsi un  entier sera un réel dont la partie décimale ne peut prendre que la valeur nulle. Cela colle parfaitement à la vision « ensembliste» de l'héritage, puisque seul un sous-ensemble de toutes les valeurs d'attributs possibles sera admis pour les sous-classes.

Vous constaterez également que la signature des méthodes redéfinies inclut le mot-clé new. Nous n'avons pas le choix, la encore, sous le regard coercitif du compilateur. Ne rien mettre provoquerait un avertissement de la part du compilateur. Il s'agit donc, ou de déclarer les méthodes comme new (c'est en effet une nouvelle version de la « même» méthode), ou alternativement, d'opter pour un couplage du mot-clé virtual, lors de la déclaration de la version première de la méthode, avec le  mot-clé override, lors de la redéfinition de la méthode. Bien évidement l'effet n'est pas le même.

 class Gardien : Joueur{
        public Gardien(Balle laBalle): base(laBalle){
            positionGet = 0;
        }
        public /*override*/ new void interagitBalle() /*override*/
        {
            base.interagitBalle();
            Console.WriteLine("Je prend la balle avec les mains");
        }
        public /*override*/ new void avance()
        {
            if(positionGet < 10)
                Console.WriteLine("Moi Gardien je peux prendre la balle en main ");
            if (positionGet < 20)
            base.avance();
        }
    
    }
    class Defenseur : Joueur
    {
        public Defenseur(Balle laBalle)
            : base(laBalle)
        {
            positionGet = 20;
        }
        public /*override*/ new void avance()
        {
            if (positionGet < 100)
                base.avance();
        }
    }
    class Attaquant : Joueur
    {
        public Attaquant(Balle laBalle)
            : base(laBalle)
        {
            positionGet = 100;
        }
        public /*override*/ new void avance()
        {
            if (positionGet < 200)
                base.avance();
            if (positionGet > 150)
                Console.WriteLine("Moi attaquant je fais attention hors-jeu ");
        }
    }

Passons à l’entraîneur, 

class Entraineur {
        private Joueur[] lesJoureurs;
        public Entraineur(Joueur[] lesJoureurs){
            this.lesJoureurs = lesJoureurs;
        }
        public void panique(){
            Console.WriteLine("C'est la panique");
            for (int i = 0; i < lesJoureurs.Length; i++)
                lesJoureurs[i].avance(); /* le même message tous les joueur !! Polymorphisme */
        }
    }
L’entraîneur est associé à un tableau d'objets typé Joueur, ce qui se traduit par une relation 1-> 1..n comme dans le schéma ci-dessus. De son seul point de vue, tous les objets avec lesquels l'entraîneur se doit d'interagir sont issu de la classe Joueur. C'est dans la méthode panique que l'entraîneur envoie le message désespéré d'avancer à tous les joueurs.L'entraîneur envoie le message d'avancer à tous les joueurs de son tableau, sans se préoccuper d'aucune sorte de la nature du joueur qui le recevra. Sa seule certitude (le compilateur le lui a assuré) c'est que tous ses joueurs seront en mesure de pouvoir l'exécuter.
Il semble donc que tout les objets du tableau joueur, peuvent bénéficier de deux typages, un typage dit statique, celui que seul le compilateur comprend et vérifie (le connu de l'entraîneur) et un typage dynamique, qui se révélera seulement à l'exécution.

Comme la figure ci-dessous l'indique quatre objets sont stockés en mémoire un pour le tableau de référent et trois pour les joueurs. Au moment de l'exécution, les objets finaux, référés par les trois éléments du tableau, ne sont plus du type de la superclasse Joueur, mais chacun d'une sous-classe différentes. Il faut se souvenir que l'opération  new ne s'effectue que pendant l'exécution. Il n'est donc pas possible de prévoir, avant l'exécution, au moment de la compilation, de quel type dynamique sera l'objet. Nous pourrions très facilement nous retrouver dans une situation dans laquelle la création de l'objet serait conditionnée par une information à découvrir pendant l'exécution. Cela veut dire, qu'avant l'exécution, on ne peut présager avec certitude de la classe finale dont l'objet sera une instance. La seule garantie que l'on ait est le typage statique de cet objet, qui est forcément une superclasse de la classe finale.
Dans des nombreux cas, la classe qui déclare l'objet et la classe qui suit l'opération new sont les même, comme lorsque nous écrivons : 01 unObjet01 = new 01().

Mais, ici la donne a changé. Nous nous retrouvons dans une nouvelle situation, assez singulière, ou la classe à gauche et à droite de cette instruction peuvent être différentes (mais pas indépendantes). La classe à droite, c'est-à-dire, la classe finale, révélée au moment de l'exécution, se doit d'être absolument ou la même ou une sous classe de la classe à gauche, c'est à dire la classe fournie par le typage statique  On peut écrire, sans heurter le compilateur : 01 unObjet01 = new Fils01().

En substance, le compilateur se satisfait, pour la justesse syntaxique, d'une superclasse, alors que le type final, à l'exécution, pourrait être une sous classe de celle-ci. Rien de choquant à cela, étant donné le principe de substitution, qui nous dit que si, la superclasse peut le faire, toute sous-classe le fera également sans problème. Mais nous devons, à partir de maintenant, nous efforcer de différencier le type statique, la classe à gauche, la seule importante pour le compilateur, du type dynamique, la classe à droite, la seule vraiment importante pour l'exécution du programme.

Revenons en à notre code, en C# ci-dessus, si on  exécute le programme comme montré jusqu'à présent, c'est à dire, sans déclarer les méthodes à redéfinir virtual, il est alors obligatoire de rajouter le mot-clé new lors de la redéfinition des méthodes. En présence de ce mot-clé, le résultat est non polymorphique.
En revanche, en présence du couple virtual/override, on obtient bien le résultat polyporphique attendu.
L'addition de new force la main, marque le coup, et indique explicitement que la redéfinition de cette méthode dans la sous-classe, ne se verra utilisée qu'en présence d'un objet typé statiquement par cette sous-classe.

Quand la sous-classe doit se démarquer pour marquer, rajoutons dans notre simulation de match de football et dans la sous-classe Attaquant, la méthode suivante : marqueUnBut(), comme indiqué ci-après :
class Attaquant : Joueur
    {
        public Attaquant(Balle laBalle)
            : base(laBalle)
        {
            positionGet = 100;
        }
        public /*override*/ new void avance()
        {
            if (positionGet < 200)
                base.avance();
            if (positionGet > 150)
                Console.WriteLine("Moi attaquant je fais attention hors-jeu ");
        }
        public void marqueUnBut()
        {
                Console.WriteLine("Goal !!!!");
        }
    }
on se place dans le cas extrême, où seuls les attaquants sont autorisés à marquer. Il serait somme toutes assez naturel de les en autoriser. Or, le compilateur fait une totale obstruction. Si dans la méthode appelante vous écrivez :
lesJoueurs[2].marqueUnbut() ;
Bien que tout leur permette de le faire, car ils sont bien attaquant et peuvent marquer des buts, cette instruction générera une erreur de compilation. C'est normal, puisque le rôle premier du compilateur est de vérifier que tout envoi de message est conforme au typage statique de l'objet. Nous avons bien dit au typage statique et non dynamique, puisque ce type est supposé non connu au moment de la compilation. Vous pourriez vous étonner de l'étroitesse de vue du compilateur. Dans l'instruction  :
lesJoueurs[2] = new Attaquant(uneBalle) ;
ce même compilateur pourrait se rendre compte que le type final de l'objet, le type à l'exécution, le seul qui compte in fine, est Attaquant et donc, que lesJoueurs[2] peuvent, de fait, marquer un but. Dans un cas semblable vous avez tout à fait raison, il le pourrait.

Mais considérons maintenant un cas plus général, correspondant au code suivant : 
int a ;
Joueur unJoueur ; 
 a  =  Console.ReadLine(); /* instruction de lecture au clavier !!! c'est l'idée !!! */ 
 if (a > 1)
  unJoueur = new Gardien();
 else
 unJoueur = new Attaquant();
 unJoueur.marqueUnBut();

ici vous admettez que, si le compilateur se basait sur le type dynamique, il serait bien en peine de savoir si la réception du message par le joueur est possible ou pas. Et c'est bien pour cela que le compilateur, féroce, mais néanmoins prudent, ne se base, pour sa vérification de la conformité des envois de message, que sur le typage statique.
Les attaquants participent à un « casting », d'où un problème basique, comment détourner l'attention du compilateur ? Comment lui faire comprendre que, bien que leMarquageDeBut ne soit pas vrai de tous les joueurs, nous savons, nous, programmeurs compétents, que le joueur[2] est bien un attaquant et qu'il peut se le permettre. La solution est de recourir au « casting », traduit de différentes manières en français « transtypage », « coercion » (on en passe et des meilleurs), et qui consiste à forcer la main au compilateur de la manière suivante :
((Attaquant)unJoueur[2]).marqueUnBut();
Cela revient à faire comprendre au compilateur qu'une information sera obtenue seulement pendant l'exécution. Le compilateur acceptera un casting d'une classe dans une de ses sous-classe, mais dans aucune autre. Il est évident qu'il n'y aura jamais lieu d'opérer ce casting dans les sens contraire. Le principe de substitution nous permet toujours de faire passer une sous-classe pour sa superclasse (on parle alors de casting « implicite»). Cela, est complètement admis et parfaitement normal. Ce qui ne l'est plus, c'est de faire passer la superclasse pour sa sous-classe. Car en effet, rien ne nous incite à penser que cela puisse fonctionner

Et de fait, cela pourrait ne pas marcher. Comme à chaque fois que vous désactivez un système de protection, cela peut se retourner contre vous. Supposons qu'alors que notre programme compile merveilleusement, un gardien plutôt qu'un attaquant soit installé à la place de joueur[2]. comme ci-après : 

lesJoueurs[2] = new Gardien()  /* gardien  à la place de l'attaquant */ 

le compilateur ne tiquera pas il lira l'instruction : ((Attaquant)unJoueur[2]).marqueUnBut(); puisqu'il ne connaît pas le type final du joueur[2]. Mais, lors de l'exécution, une erreur surviendra, de type « mauvais casting ».

Eviter les « mauvais casting », une erreur de type « mauvais casting » surgit. Le programme s'attendait à recevoir un  gardien pendant l'exécution, et vous lui passez un attaquant à la place. Comme cette erreur se produit pendant l'exécution, et qu'il vaut mieux tenter de prévenir toute forme d'erreur dès l'écriture du code, il y a deux manières de procéder.
Vous pourriez accepter l'erreur si elle survient, et recourir alors à une gestion d'exception fort encouragé. Mais il y a mieux à faire : empécher un telle erreur de se produire. Vous pouvez vérifier le type dynamique comme suit : 
Attaquant unAttaquant = lesJoueur[2] as Attaquant ;
if (Attaquant != null)
unAttaquant.marqueUnBut() ;
En C#, on force le casting. Ça passe ou ça « classe ». Si cela marche, c'est à dire si, à l'exécution, l'objet est bien du type de la classe dans laquelle on désire le « caster », le nouveau référent recevra la bonne adresse, sinon il  recevra 0 ou null, mais l'envoi de message ne s’effectuera pas, bien heureusement.

Polymorphisme et casting, une des conséquence du polymorphisme est le recours au « casting » qui vise à récupérer des fonctionnalités propres à l'une ou l'autre sous-classe lors de l'exécution d'un programme. Sa pratique est parfois délicate et demande une attention soutenue, car elle peut mener à des erreurs pendant la phase d’exécution.C# par l'introduction de la généricité dans leurs dernières versions, tentent de diminuer ce recours.









Aucun commentaire: