La POO et le secret de l’héritage ?· La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr…

  • Published on
    11-Sep-2018

  • View
    212

  • Download
    0

Embed Size (px)

Transcript

  • La POO et le secret

    de lhritage

    Julien E. Harbulot | web@julienh.fr

    Fvrier 2015

    Depuis la popularisation du paradigme de programmation fonctionnel, la programmation oriente

    objet (POO) est de plus en plus critique. Ces critiques reposent toutefois sur une utilisation

    inapproprie de la POO, et surtout de l'hritage, dont le rle semble mal compris.

    Cet article fait le point sur l'hritage et sur son rle vis--vis du polymorphisme. Nous verrons que

    la POO n'est pas morte, et qu'elle permet de rutiliser du code existant d'une faon inattendue.

    mailto:web@julienh.fr

  • Sommaire

    Quest-ce que lhritage ? 3

    Une utilisation encore trop courante : lhritage dimplmentation 4

    Cas pratique 4

    Lhritage dimplmentation en chec 7

    La vritable raison dtre de lhritage : lhritage dinterface 8

    Cas pratique 8

    Le polymorphisme 9

    Lhritage dinterface et le systme de type 10

    Lhritage dinterface tient ses promesses 11

    Lhritage dinterface permet dinverser les dpendances 11

    Lhritage, au-del des interfaces 13

    Le principe de substitution de Liskov 13

    Cas pratique 13

    Lhritage nexprime pas la relation est un 14

    Pour aller plus loin 14

    La bonne mthode pour rutiliser des objets existants : la composition 15

    Lhritage dimplmentation est source de rigidit 15

    La composition permet dutiliser linversion de dpendance 15

    Les mthodes de transfert et la composition 16

    POO et rutilisation de code : en bref 17

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 3 sur 17

    Quest-ce que lhritage ?

    Lhritage est un mcanisme de la programmation oriente objet (POO) qui permet de lier deux

    classes, que lon appelle classe-mre et classe-fille. Un tel lien entre deux classes a les

    consquences suivantes :

    la classe-fille dispose de toutes les mthodes et de tous les attributs de porte public et

    protected de la classe-mre, et peut les utiliser comme sil sagissait des siennes,

    le type de la classe-fille peut tre utilis partout o celui de la classe-mre peut ltre,

    certaines mthodes de la classe-mre peuvent tre r-crites dans la classe-fille. Ces

    mthodes sont appeles mthodes virtuelles, et le mcanisme permettant une telle

    modification sappelle le polymorphisme.

    Lhritage permet de rutiliser du code existant, de limiter la duplication, et donc de simplifier la

    maintenance du code.

    Nous verrons nanmoins que les mthodes pour y parvenir ne sont pas les premires auxquelles

    lon pense. En effet, il peut tre utilis de deux faons diffrentes :

    pour hriter de limplmentation dune classe existante, et ainsi rutiliser son code. On parle

    alors dhritage dimplmentation,

    pour hriter de linterface dune classe existante dans le but de permettre la classe-fille dtre

    substitue la classe-mre de faon polymorphique. Cette utilisation sappelle hritage

    dinterface et rend possible la r-utilisation de code dune application qui manipule des

    instances de la classe-mre en lui faisant manipuler des instances de la classe-fille, sans que

    ce dernier nait besoin dtre modifi.

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 4 sur 17

    Une utilisation encore trop courante : lhritage dimplmentation

    Lhritage est parfois utilis comme outil pour factoriser du code entre les objets. Lorsque plusieurs

    objets utilisent le mme code, une solution possible pour viter la duplication est de placer ce code

    dans une classe-mre commune : cest lhritage dimplmentation.

    Cas pratique

    Voici un exemple o lon a factoris le code commun aux classes Peugeot et Renault dans une

    classe-mre appele Voiture.

    class Voiture{ protected: Roue roue_avant_gauche; Roue roue_avant_droite; Roue roue_arriere_gauche; Roue roue_arriere_droite; public: void tourner_a_gauche() { roue_avant_gauche.pivoter_a_gauche(); roue_avant_droite.pivoter_a_droite(); } /* et autres mthodes similaires. . . */ void accelerer() { roue_avant_gauche.tourner_plus_vite(); roue_avant_droite.tourner_plus_vite(); }

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 5 sur 17

    /* et autres mthodes similaires. . . */ }; class Peugeot : public Voiture{ public: string nom_du_modele() { return Peugeot 204 ; } // etc. }; class Renault : public Voiture{ public: string version() { return Renault 12 Gordini ; } // etc. };

    Dans lexemple ci-dessus, les classes Peugeot et Renault partagent du code qui a t factoris

    dans la classe-mre Voiture, et il est possible de faire appel ce code de faon transparente : Peugeot ma_voiture; string mon_modele = ma_voiture.nom_du_modele(); ma_voiture.tourner_a_gauche(); // utilisation du code de la classe-mre ma_voiture.accelerer(); // utilisation du code de la classe-mre

    Continuons notre exemple, et supposons dsormais que lon dcide damliorer notre application

    en ajoutant de nouveaux modles : une Twingo propulsion (arrire), et une Alpine A110

    propulsion (arrire) galement.

    Comme nous sommes partis du principe que la classe Voiture tait traction (avant), il faut la

    modifier pour extraire le code correspondant cet aspect et crer deux classes distinctes

    (VoitureTraction et VoiturePropulsion) afin de factoriser le code responsable de la gestion

    de lacclration.

    class Voiture{ // Nous enlevons le code qui soccupe dacclrer. protected: Roue roue_avant_gauche; Roue roue_avant_droite; Roue roue_arriere_gauche; Roue roue_arriere_droite; public: void tourner_a_gauche() { roue_avant_gauche.pivoter_a_gauche(); roue_avant_droite.pivoter_a_droite(); } /* et autres mthodes similaires. . . */ }; class VoitureTraction : public Voiture { // lavant public: void accelerer() { roue_avant_gauche.tourner_plus_vite(); roue_avant_droite.tourner_plus_vite(); } /* et autres mthodes similaires. . . */ }; class VoitureTraction : public Voiture { // larrire public: void accelerer() { roue_arriere_gauche.tourner_plus_vite(); roue_arriere_droite.tourner_plus_vite(); } /* et autres mthodes similaires. . . */ }; class Peugeot : public VoitureTraction{ public: string nom_du_modele() { return Peugeot 204 ; } // etc. };

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 6 sur 17

    class Renault : public VoitureTraction{ public: string version() { return Renault 12 Gordini ; } // etc. }; class Twingo : public VoiturePropulsion{ // etc. }; class Alpine : public VoiturePropulsion{ // etc. };

    Le code commun Twingo et Alpine est bien factoris dans la classe VoiturePropulsion,

    tandis que le code partag par Peugeot et Renault est dans la classe VoitureTraction.

    Remarquons que le changement apport la classe Voiture ncessite une re-compilation

    de toutes ses classes filles.

    Notre application fonctionne correctement, et nous dcidons maintenant de rajouter un modle

    supplmentaire : Le 4x4 Citron C4 AirCross, qui est traction et propulsion

    Traction et Propulsion ? Mais alors de quelle classe devra-t-il hriter ? VoitureTraction ou

    VoiturePropulsion ? Les deux ?

    - Si le 4x4 hrite des deux classes, alors il sera dot de 8 roues ! En effet, 4 sont dtenues par la

    classe VoitureTraction, et 4 par la classe VoiturePropulsion.

    Non seulement une telle situation gaspille de la mmoire car la moiti des roues sont superflues,

    mais cest aussi une source de complexit inutile qui oblige jongler entre les attributs des deux

    classes mres.

    Cas particulier : si vous programmez en C++, vous savez peut-tre que ce langage offre la

    possibilit dutiliser un hritage spcial entre la classe VoitureTraction et la classe Voiture,

    que lon appelle lhritage virtuel. Nanmoins, lutilisation de lhritage virtuel est couteuse en

    performance, et ne constitue pas une solution viable car cela diminuerait les performances de

    toutes les classes qui hritent de VoitureTraction et pas seulement celles de notre classe 4x4.

    - Plutt que de faire hriter notre 4x4 des deux classes VoitureTraction et VoiturePropulsion,

    nous pourrions ne le faire hriter que de la classe VoitureTraction, et rcrire les mthodes

    dacclration pour ajouter la gestion des roues arrires. Cependant, cette solution nest pas

    satisfaisante car cela revient dupliquer le code de la classe VoiturePropulsion la main, or

    cest justement ce que nous voulons viter !

    Dans tous les cas, nous sommes bloqus : lhritage dimplmentation ne remplit pas ses

    promesses et la duplication de code semble invitable.

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 7 sur 17

    Lhritage dimplmentation en chec

    On voit ainsi que lhritage dimplmentation est maladroitement utilis dans le but de :

    rutiliser du code existant dans de nouvelles classes,

    factoriser du code commun plusieurs classes dans une classe-mre commune

    et que son utilisation entraine les inconvnients suivants :

    un couplage fort entre les deux classes qui oblige la classe-fille (et donc toute lapplication

    qui en dpend) recompiler chaque changement de la classe-mre,

    des hirarchies complexes et impossibles maintenir qui rendent la duplication de code

    invitable.

    Cest justement pour limiter lutilisation de lhritage dimplmentation que ladage prfrer la

    composition lhritage sest rpandu ; et nous verrons prochainement que la composition,

    lorsquelle est utilise de pair avec le polymorphisme, permet de rutiliser le code dobjets

    existants de faon satisfaisante.

    En clair, nombreux sont ceux qui voient dans lhritage un moyen de rutiliser

    des objets existants en spargnant davoir crire des mthodes de

    transfert[1]. Mais cest oublier que lhritage a pour unique vocation de

    permettre la rutilisation du code client via le polymorphisme, dont il est le

    vassal, comme nous allons le voir.

    Il est donc bien question dune mauvaise comprhension du rle de lhritage,

    dont lutilisation dnature va contresens des objectifs de la POO et produit

    du code rigide, qui entraine :

    un couplage fort, inutile entre deux classes,

    des hirarchies trop complexes,

    et terme : de la duplication de code.

    [1] voir chapitre sur la composition

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 8 sur 17

    La vritable raison dtre de lhritage : lhritage dinterface

    Comme nous lavons vu dans la partie prcdente, lhritage ne permet pas de rutiliser des objets

    existants. Il a en fait t conu pour accompagner le polymorphisme dans le but de permettre la

    rutilisation dun autre type de code : le code dapplication, cest dire le code qui utilise nos

    objets.

    Cas pratique

    Afin dillustrer lintrt du polymorphisme et de lhritage dinterface, voici un exemple o nous

    sommes chargs de lcriture dune fonction crypter_fichier capable dencrypter un fichier,

    selon un algorithme accessible depuis la fonction crypter_string : string crypter_string(string input); //fonction fournie void crypter_fichier(string adresse_du_fichier){ FILE fichier_source = ouvrir_fichier( adresse_du_fichier ); string contenu = lire_fichier( fichier_source ); string nouveau_contenu = crypter_string( contenu ); effacer_fichier( fichier_source ) ecrire_dans_fichier( fichier_source, nouveau_contenu ); }

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 9 sur 17

    Quelques jours plus tard, nous sommes chargs dcrire une nouvelle fonction capable de crypter des fichiers situs sur le rseau : void crypter_fichier_web(string adresse_web){ WEBFILE fichier_source = obtenir_fichier_distant( adresse_web ); string contenu = lire_fichier_web( fichier_source ); string nouveau_contenu = crypter_string( contenu ); WEBFILE nouveau_fichier = creer_avec_contenu( nouveau_contenu ); ecraser_fichier_distant( adresse_web , nouveau_fichier ); }

    Remarquons que lalgorithme de cette seconde fonction est en tout point similaire celui de la

    premire : ouvrir le fichier source ; calculer le contenu crypt ; remplacer le fichier source.

    Malheureusement, cause de dtails lis la nature des fichiers, il nous est impossible de

    rutiliser notre fonction initiale.

    Condamns rcrire la mme fonction encore et encore ?

    Heureusement non, grce au polymorphisme, qui permet de rutiliser la mme fonction dans les

    deux cas

    Le polymorphisme

    Le polymorphisme permet de manipuler deux objets qui se comportent de la mme faon sans

    avoir les distinguer. Par exemple, nous aimerions pouvoir crire une application capable de

    crypter tout type de fichier, en faisant abstraction des dtails ncessaires leur manipulation.

    Or, afin dtre en mesure de manipuler deux objets diffrents de la mme faon, il est ncessaire

    que ces deux objets comprennent, et soit capables dexcuter, les mmes instructions. Cela veut

    dire que ces objets doivent mettre disposition les mmes mthodes dans leurs interfaces

    respectives.

    Par exemple, les objets WebFile et DiskFile pourraient proposer les mthodes suivantes :

    ouvrir( adresse )

    obtenir_contenu()

    ecraser_contenu_avec( nouveau_contenu )

    class WebFile{ public: WebFile(string adresse); string obtenir_contenu(); void ecraser_contenu_avec( string nouveau_contenu ); }; class DiskFile{ public: DiskFile(string adresse); string obtenir_contenu(); void ecraser_contenu_avec( string nouveau_contenu ); };

    Il serait alors trs simple de rutiliser le mme code dapplication pour manipuler indiffremment

    ces deux objets :

    void crypter_fichier( UnType? fichier_source ){ string contenu = fichier_source.obtenir_contenu(); string nouveau_contenu = crypter_string( contenu ); fichier_source.ecraser_contenu_avec( nouveau_contenu ); }

  • La POO et le secret de lhritage | Julien E. Harbulot

    Page 10 sur 17

    Cependant, un problme se pose pour les utilisateurs de langages statiquement typs tels C++ et

    Java : quel type la fonction crypter_fichier doit-elle accepter en argument ? Si la fonction

    demande un argument de type WebFile, il sera impossible de lutiliser avec des instances de

    DiskFile, et rciproquement.

    La solution rside dans lutilisation de lhritage dinterface, qui dfinit un contrat, pass par un

    objet vis--vis de ses utilisateurs.

    Lhritage dinterface et le systme de type

    Lhritage dinterface est utilis dans les langages statiquement typs (C++, Java, etc.) pour

    satisfaire aux contraintes imposes par le systme de type. Il nest dailleurs a priori ncessaire

    que dans ces systmes.

    Il permet en effet diffrentes classes de dclarer quelles remplissent un mme contrat vis--vis

    de leurs utilisateurs, et donc quelles peuvent tre interchanges.

    Lhritage dinterface met en scne deux acteurs :

    une classe-mre responsable de dfinir le contrat, on lappelle Interface,

    et une classe-fille qui sengage respecter le contrat nonc.

    Dans notre tude de cas, lhritage dinterface est la clef qui permet de rutiliser la fonction

    crypter_fichier avec diffrents types de fichiers :

    class File{ // Dclaration de lInterface public: virtual string obtenir_contenu() = 0; virtual void ecraser_contenu_avec( string nouveau_contenu ) = 0; }; class DiskFile : public File{ /* Implmentation spcifique pour les fichiers locaux */ }; class WebFile : public File{ /* Implmentation spcifique pour les fichiers web */ }; void crypter_fichier( File& fichier_source ){...