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 11. Héritage

Chapitre 11. Héritage
Dans la poursuite de la modularisation mais verticale cette fois, ce chapitre introduit la pratique de l'héritage, comme, vers le haut, une factorisation dans la superclasse des attributs et des méthodes communs aux sous-classes, et vers le bas, la spécialisation de ces sous-classes par l'addition et le raffinement des méthodes et des attributs qui leur sont propres.


Comment regrouper les classes dans des superclasses, Reprenons l'exemple de notre écosystème du chapitre 3, dont une vue de la simulation est présentée ci-après. Le premier constat qui s'impose, c'est que nous avons démultiplié le nombre de proies, prédateurs, plantes et point d'eau. grâce à une des possibilités et avantage de la classe, qui est de donner naissance à une multitude d'objets sans se préoccuper, pour chacun , de re-préciser ce qu'il fait et de quoi il est fait.


Jusqu'ici nous avons codé ce modèle à l'aide de 5 classes(sans compter la vision). D'abord, les deux classes d'animaux : Proie et Prédateur, ensuite les deux classes de ressources naturelles: Plante et Eau, puis finalement la classe Jungle. Cette dernière agrège toutes les autres et lance la méthode principale qui, de manière itérée, fait évoluer les ressources et se déplacer les animaux. Vous aurez sans doute été sensible à ce petit dérapage sémantique, effectué sous contrôle bien sûr, et qui nous amène à réunir, sous le même concept d'animaux, la proie et le prédateur, ainsi que sous le même concept de ressources, l'eau et la plante.

Nous allons, en effet, joindre le geste logiciel à la parole, et introduire deux nouvelles classes Faune et Ressource, dont la raison d'être sera de regrouper ce qu'il y a de commun à la proie et au prédateur pour la première et de commun aux points d'eau et aux plantes pour la seconde. Voyons, dans un premier temps, les attributs communs à la proie et au prédateur, que nous installerons dorénavant  dans la faune. Chacun se trouve situé en un point précis (x,y), chacun se déplace avec une vitesse projetée sur chaque axe (vitx, vity), chacun possède une énergie qui décroît suite aux efforts, et s'accroît grâce aux ressources, chacun est associé aux ressources disponibles dans la jungle, avec lesquelles ils interagissent.  

Toutes ces propriétés se retrouveront dès à présent « plus haut dans les classes », c'est à dire dans la faune. Que reste-t-il, qui particularise et différencie encore la proie du prédateur ? Le prédateur interagit, en plus, avec les seules proies et vice versa. Les proies pouvant mourir, elles possèdent un attribut supplémentaire indiquant leur état de vie.

Toutes les plantes et tous les points d'eau sont caractérisés par les deux-mêmes attributs : leur quantité et un compteur temporel qui sert à rythmer leur évolution naturelle (la plante qui pousse et l'eau qui s'évapore). Ici la factorisation est encore plus radicale que dans le cas des animaux, car il ne reste, au bout du compte, aucun attribut qui différencie les plantes de l'eau.

Dans quelle mesure ne sont-il pas alors simplement des objets différents d'une même classe Ressource ? Car si la seule différence qui subsiste entre des objets est la valeur de certains de leurs attributs, il n' y a plus lieu de découper les classes en sous- classes. Il n'existe pas de sous classes de voiture rouge ou bleue, car ce sont simplement deux objets différents de la même classe voiture. Dans pareil cas, il suffit de s'en tenir, bien sûr, à la seule diversification des objets, qui sert justement à cela : encoder par chacun des valeurs d'attributs différentes. N'utilisez jamais l'héritage de manière abusive, pour ce qu'il n'est pas dans les langages  OO, c'est-à-dire la différentiation des objets appartenant à une même classe par la seule valeur des attributs. Ne faites pas systématiquement de sous-classes pour les jeunes hommes et les hommes âgés, si seul leur âge les différencie, ou de sous-classes pour les voitures rapides ou lentes si, là encore, seul leur vitesse maximale les différencie et rien d'autre. Pour l'instant, nous nous sommes limités aux seuls attributs, et nous verrons bien vite que les méthodes jouent un rôle encore plus important, lors de cette factorisation dans une superclasse des caractéristiques communes aux sous-classes. A elles seules, elles justifieront, pour les ressources, la présence de ces deux niveaux hiérarchiques. 

Héritage des attributs, le diagramme UML ci-après illustre à l'aide d'un nouveau symbole graphique, le mécanisme d'héritage par les classes Parents et Enfants. Comme cela est visible sur le diagramme, de par la présence de la flèche d'héritage(seule la pointe la différencie de celle symbolisant le lien d'association dirigée), tous les attributs et les méthodes caractérisant la superclasse deviennent, automatiquement, attributs et méthodes de la sous-classe, sans qu'il n'y ait besoin de le préciser davantage. C'est la direction de la flèche qui spécifie lesquelles sont les superclasses et lesquelles sont les sous-classes, nullement leur position dans le diagramme de classe, même s'il est de coutume d'installer les sous-classes en dessous des superclasses.

Premières conséquences de cette application de l'héritage, il ne peut y avoir dans la sous-classe, par rapport à  sa superclasse, que des caractéristiques additionnelles ou de précisions. Ce que l'héritage permet d'abord c'est de rajouter  dans la sous-classe de nouveaux attributs et de nouvelles méthodes (qui auront comme responsabilité la gestion de ces nouveaux attributs), les seuls à préciser dans la déclaration des sous classes.

Ce que la relation d'héritage cherche à reproduire dans l'écriture logicielle, c'est l'existence, dans notre manière d'appréhender le monde, de concepts plus spécifiques et génériques. Nous le faisons tout naturellement pour des raisons d'économie déjà entrevue dans le premier chapitre, et nous retrouvons cette pratique « taxonomique » dans de nombreuses disciplines intellectuelles humaines : politique, économique, sociologique, biologique ... La possibilité de hiérarchiser notre conceptualisation du monde en différents niveau d'abstraction nous permet une utilisation plus flexible de l'un ou l'autre de ces niveaux, selon le contexte d'utilisation. L'objet est unique mais chacun le désigne à sa manière, afin de communiquer son souhait le plus économiquement et le plus effectivement qui soit. Le choix du bon niveau d'abstraction se justifie par un souci d'optimisation de la communication, par le souhait « de dire le plus avec le moins ».
Nul besoin de savoir qu'il s'agit d'un IBM ThinkPad pour réaliser que, tout IBM qu'il est, il permet de taper du code ou jouer. 
Un concept est plus abstrait qu'un autre si, dans sa fonction descriptive, il englobe cet autre, s'il est plus général, plus passe-partout, plus adaptable. Comme ci-dessous, il en est ainsi de « machine », plus abstrait que « portable », plus abstrait que « IBM ThinkPad ».

Vous nous verrez taper sur un « portable » et non pas sur un « IBM ThinkPad ». Clairement, un des niveaux d'abstraction se voit privilégié par rapport aux autres. La raison en est simple, c'est le niveau le plus usité lors de toute communication d'information concernant, en effet, l'objet référé au cours de cette communication. C'est le niveau de base, celui qui caractérise le mieux l'objet, qui capture le plus d'informations sur rôles et les fonctions qu'on lui attribue.

Pourquoi l'addition de propriétés ? Pourquoi une classe plus spécifique ne peut-elle que rajouter des propriétés par rapport à sa superclasse. Pour la bonne et simple raison qu'elle se doit de rester également cette superclasse, et qu'elle le restera, tant qu'elle partagera avec cette superclasse les mêmes propriétés. D'ou la seule pratique valide qui consiste à trouver dans la classe héritant un ajout d'informations par rapport à la classe héritée. Rien de ce qu'est ou de ce que fait une superclasse ne peut ne pas être ou ne pas être fait par toutes ses sous-classes. La sous-classe peut en faire plus, pour se démarquer, mais jamais moins. Ce mode de fonctionnement est évidemment transportable aux objets, instances des classes et sous-classes correspondantes, et donne lieu à un principe clé de la pratique orientée objet, principe qui est dit de « substitution ».

Principe de substitution : un  héritier peut représenter la famille, partout où un objet, instance d'une classe apparaît, on peut, sans que cela pose le moindre problème, lui substituer un objet quelconque, instance d'une sous-classe. Tout message compris par un objet d'une superclasse le sera obligatoirement par tous les objets issus des sous-classes. L'inverse est évidement faux. Toute Ferrari peut se comporter comme une voiture, tout portable comme un ordinateur et tout livre d'informatique OO comme un livre. C'est pour cette simple raison qu'il sera toujours possible de typer statiquement un objet par une superclasse bien que, lors de sa création et son utilisation, il sera plus précisément instance d'une sous-classe de celle-ci, par exemple « SuperClasse  unObjet = new SousClasse() » (le compilateur ne bronche pas, même si l'objet typé superclasse sera finalement créé comme instance de la sous-classe ; c'est aussi la raison pour laquelle vous devez écrire deux fois le nom de la classe dans l'instruction de la création d'objet) ou encore :
 SuperClasse  unObjet = new  SuperClasse   () ;
 SousClasse  unObjetSpecifique = new SousClasse() ;
unObjet  = unObjetSpecifique ;  /* l'inverse serait refusé par le compilateur */

Tout message autorisé par le compilateur, lorsqu'il est censé s'exécuter sur un objet d'une superclasse, peut tout autant s'exécuter sur un objet de toutes ses sous-classes. On pourra donc toujours assigner un objet d'une sous-classe à un objet d'une superclasse (car ce qu'est sensé faire la superclasse, toute sous-classe peut le faire), mais jamais l'inverse.

« Casting explicite » versus « casting implicite », le « casting » permet à une variable typée d'une certaine façon lors de sa création d'adopter un autre type le temps d'une manœuvre. Conscient des risques que nous prenons en recourant à cette pratique, le compilateur l'accepte si nous recourons à un « casting explicite ». C'est le cas en programmation classique quand on veut, par exemple, assigner une variable réelle à une variable entière. Dans le cas du principe de substitution, les informaticiens parlent souvent d'un « casting implicite », signifiant par là, qu'il n'y a pas lieu de contrer l'opposition du compilateur quand nous faisons passer un objet d'une sous-classe pour un objet d'une superclasse. Le compilateur (gardien du temple de la bonne syntaxe et de la bonne pratique) ne bronchera pas, car cette démarche est tout à  fait naturelle et acceptée d'office. En revanche, faire passer un objet d'une superclasse pour celui d'une sous-classe requiert la présence d'un « casting explicite » par lequel vous  prenez l'entière responsabilité de ce comportement risqué. Le compilateur vous met ne garde (vous êtes en effet sur le point de commettre une bêtise) mais, ensuite, vous en faites ce que vous voulez en votre âme et conscience. Par exemple, si vous transférez un réel dans un entier (la superclasse dans la sous-classe), c'est en effet une bêtise, car vous perdez la partie décimale. En général, vous en êtes pleinement conscient et assumez la responsabilité de cette perte d'information. Les puristes de l'informatique détestent la pratique du casting (explicite évidemment) qui est toujours évitable par un typage plus fin et une écriture de code plus soignée. Un mauvais casting sera responsable d'une erreur à l'exécution du code lorsque l'on assigne à une classe un type qui ne lui correspond pas, et ce alors que le compilateur à laisser faire.

Héritage ou  composition, un objet de type sous-classe est d'autant plus un objet de type superclasse que son stockage en mémoire se compose, d'abord, d'un objet de type superclasse, puis d'un espace mémoire réservé aux attributs qui lui sont propres, comme indiqué dans la figure ci-après. Ce mode de stockage ressemble à s'y méprendre au mode de stockage d'un objet, qui serait en partie composé d'un autre objet en son sein. Cela revient à dire qu'eu égard au stockage des objets en mémoire, rien ne différencie vraiment un lien de composition entre deux classes d'un lien d'héritage entre ces deux même classes. Nous verrons qu'il n'en va plus de même lors de l'appel des méthodes.


Dans le cas de la composition, il y a bien envoi de messages de la classe qui offre le logement à celle qui est logée. Dans le cas de l'héritage, on ne parlera plus d'envoi de message, car il s'agit bien d'une méthode propre à la classe elle-même, quitte à être héritée d'une autre. N'oublions pas que l'héritage crée une vraie fusion entre les deux classes, alors que la composition maintien un rapport de clientélisme. Certains programmeurs tendent à favoriser tant que faire se peut la composition au détriment de l'héritage. Nous défendons ici la position classiquement admise : faites parler les concepts de la réalité que vous cherchez à reproduire et écoutez-les. C'est la réalité que vous cherchez à dépeindre qui doit avoir le dernier mot. Si deux entités qui vous intéressent entrent dans une relation taxonomique (comme la Ferrari et la voiture), recourez à l'héritage, dans tous les autres cas (comme le moteur et la voiture) choisissez la composition, sans oublier bien sûr les autres possibilités que sont l'agrégation ou l'association (comme la voiture et son propriétaire).

En substance, une relation d'héritage peut conceptuellement toujours se transformer en une composition mais, là encore, l'inverse n'est pas vrai. Un disque dure n'est pas une espèce d'ordinateur, un chapitre n'est pas un livre. On peut dire, en revanche, qu'un livre de programmation contient un livre et qu'un portable contient un ordinateur. 

La place de l'héritage, l'héritage trouve parfaitement sa place dans une démarche logicielle dont le souci principal devient la clarté d'écriture, la réutilisation de l'existant, la fidélité au réel, et une maintenance de code qui n'est pas mise à mal par des évolutions continues, dues notamment à l'addition de nouvelles classes.

Héritage des méthodes, à l'instar de l'héritage des attributs, les méthodes s'héritent également, et toute sous-classe peut ajouter de nouvelles méthodes lors de sa déclaration. Comme schématisé ci-dessous par le diagramme UML, lorsque la classe 01 désire communiquer avec la classe Fille02, elle a la possibilité, soit de lui envoyer les messages qui sont propres à cette classe, soit de lui envoyer tous ceux hérités de la classe 02. Dans la famille classe, je demande la fille... Quand la proie ou le prédateur consomme l'eau ou la plante, elles envoient à l'eau ou la plante le même message diminueQuantite(). Ce message n'est ni déclaré dans la classe Eau ni dans la classe Plante, mais elles en héritent toutes deux de leur superclasse Ressource. 
Pour les proies et les prédateurs aussi, de nouvelles méthodes communes aux deux, comme tourneLaTete(), reperUnobjet(), peuvent être déclarées plus haut dans la classe Faune. Cela permet à toute classe nécessitant d'interagir avec les proies ou les prédateurs de le faire directement avec la faune « qu'il y a en eux », sans se préoccuper de savoir exactement de quelle faune il s'agit.
La classe 01 communique avec la classe Fille02 en lui envoyant des messages qui sont, soit hérités de 02, soit propres à la classe héritière.
Voici le code correspondant :

class O1{
       private FilleO2 lienFilleO2;
       public O1 ( FilleO2 lienFilleO2){
        this.lienFilleO2 = lienFilleO2;
       }
       public void jeTravaillePour01(){
        lienFilleO2.jeTravaillePour02();
        lienFilleO2.jeTravaillePourLaFille02();
       }
       public void voidjeTravailleAussiPour01(FilleO2 lienFilleO2){
        lienFilleO2.jeTravaillePour02();
        lienFilleO2.jeTravaillePourLaFille02();
       }
       public void voidjeTravailleAussiPour01(O2 lienO2){
        lienO2.jeTravaillePour02();
       }
    }
     class O2{
        public O2 (){}
       public void jeTravaillePour02(){
           ....
       }
    }
     class FilleO2 : O2{
       public FilleO2 (){}
       public void jeTravaillePourLaFille02(){
           ....
       }
    }

Messages et niveaux d'abstraction, l'héritage permet à une classe, communiquant avec une série d'autres classes, de leur parler à différents niveau d'abstraction, sans qu'il soit toujours besoin de connaître leur nature ultime (on retrouvera ce point essentiel dans l'explication du polymorphisme), ce qui facilite l'écriture, la gestion et la stabilisation du code.
L'héritage des méthodes ne consiste en rien, comme pour les attributs, en une éventuelle duplication des méthodes, de sorte à les retrouver dans les sous-classes, mais bien à un mécanisme de recherche qui démarre des sous-classes pour grimper dans les niveaux supérieurs.

La recherche des méthodes dans la hiérarchie, les méthodes des superclasses et des sous-classes étant, comme les attributs, stockées ensemble, mais dans la mémoire des méthodes cette fois, lors de l'envoi du message d'01 vers la Fille02, la méthode recherchée le sera, d'abord, dans la zone mémoire correspondante au type de l'objet, c'est-à-dire la zone mémoire Fille02. Si la méthode ne s'y trouve pas, grâce à l'instruction d'héritage comme indiqué ci-dessous, on sait que cette méthode peut se trouver plus haut, quelque part dans une superclasse. La montée en cordée, de superclasse en superclasse, à la découverte de la méthode recherchée, peut-être longue, et tout dépendra de la profondeur de la structure hiérarchique d'héritage réalisée dans l'application logicielle. Toutes les méthodes dans la hiérarchie peuvent s'appliquer sur l'objet, car le compilateur aura bien vérifié que chacune, quel que soit le niveau hiérarchique où elle se trouve, n'interférera qu'avec les attributs et les méthodes qui existent à ce niveau.
Ces montées et descentes, pendant l'exécution du programme, à la recherche de la méthode appropriée à exécuter sur l'objet, ont amené certains à parler d'un fonctionnement de type  « yoyo ». Sans conteste, les voyages dans la RAM ralentissent considérablement toute l'exécution d'un programme. Or, on a déjà rencontré ces déplacements en examinant l'activation successive d'objets, qui peuvent se trouver stockés n'importe où dans la RAM. On accroît ce phénomène, en le reproduisant du côté des méthodes, dont la quête peut également occasionner ces périples incessants.
Rien de bien original à répondre à cette critique fondée si ce n'est, une nouvelle fois, d'accepter la programmation OO pour ce qu'elle est : une approche plus simplifiée, plus intuitive, plus stable, et dont la complexification est mieux maîtrisée, du développement logiciel, et non pour ce qu'elle n'est pas, une volonté d'exploitation à tous crins des possibilités d'optimisation liée au fonctionnement intime des processeurs. Il s'agit bien d'un parti OO contre processeur.

Encapsulation Protected, un attribut ou une méthode déclaré protected dans une classe devient accessible dans toutes les sous-classes. La charte de la bonne programmation OO déconseille l'utilisation de « protected ».

Héritage et constructeurs, il est fréquent que la sous-classe ajoute des attributs par rapport à la superclasse. Tout objet, instance de la sous-classe, possède dès lors deux ensembles d'attributs, ceux qui lui sont propres et ceux hérités de là-haut. Se pose alors le problème de la pratique des constructeurs, que nous savons être indispensable, en tout cas vivement conseillé, pour l'initialisation de ces attributs lors de la création de chaque objet.Comment doit se comporter le constructeur de la sous classe dans le traitement des attributs qui ne lui incombent qu'indirectement. c'est à dire par l'héritage?
Voyons le code : 
  class O1{
       protected int unAttribut01; 
       public O1 (int unAttribut01){
        this.unAttribut01 = unAttribut01;
       }
    }
     class FilsO1 : O1{
        private int unAttributFils01; 
        public FilsO1 (int unAttribut01, int unAttributFils01 ):base(unAttribut01){
            /* Notez l'appel à la superclasse */
            this.unAttributFils01 = unAttributFils01;
        }
    }
l'appel au constructeur de la superclasse qui se fait par le mot-clé base, et dès la déclaration de la méthode (dès sa signature) plutôt que dans le corps d'instructions. Cela garantit qu'il s'agira en effet de la première instruction exécutée.
Pourquoi, de fait, faire appel au constructeur de la superclasse ? Simplement, dixit le compilateur, parce qu'on n'a pas le choix. Chaque classe s'occupe de l'initialisation de ses propres attributs. Gardez toujours à l'esprit le découpage fort des responsabilités en OO. Rendez à chaque classe ce qui appartient à chaque classe. Chacun à sa classe... et les attributs seront bien gardés.
Ramification descendantes et ascendantes, notre conceptualisation du monde s'arrange bien de cette multiplicité qui, de manière plus formelle, élargit la structure de l'héritage : d'arbre (quand l'héritage ne peut ramifier que de manière descendante), en graphe (quand les ramifications peuvent se faire autant dans le sens descendant - plusieurs sous-classes pour une classe - qu'ascendant - plusieurs superclasses pour une classe ,voir la figure qui suit). En principe, toute classe pourrait réunir en son sein des caractéristiques différentes provenant de plusieurs superclasses. Il suffit qu'elle les additionne.

Le multihéritage, le C# interdit le multihéritage (en partie, il l'autorise pour les interfaces), tout le problème provient de la nécessité pour les caractéristiques héritées d'être vraiment différentes entre elles. 




Aucun commentaire: