17
La POO et le secret de l’héritage Julien E. Harbulot | [email protected] Février 2015 Depuis la popularisation du paradigme de programmation fonctionnel, la programmation orientée objet (POO) est de plus en plus critiquée. Ces critiques reposent toutefois sur une utilisation inappropriée de la POO, et surtout de l'héritage, dont le rôle semble mal compris. Cet article fait le point sur l'héritage et sur son rôle vis-à-vis du polymorphisme. Nous verrons que la POO n'est pas morte, et qu'elle permet de réutiliser du code existant d'une façon inattendue.

La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | [email protected] Février 2015 Depuis la popularisation du paradigme de programmation

  • Upload
    vukhue

  • View
    221

  • Download
    0

Embed Size (px)

Citation preview

Page 1: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret

de l’héritage

Julien E. Harbulot | [email protected]

Février 2015

Depuis la popularisation du paradigme de programmation fonctionnel, la programmation orientée

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

inappropriée de la POO, et surtout de l'héritage, dont le rôle semble mal compris.

Cet article fait le point sur l'héritage et sur son rôle vis-à-vis du polymorphisme. Nous verrons que

la POO n'est pas morte, et qu'elle permet de réutiliser du code existant d'une façon inattendue.

Page 2: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

Sommaire

Qu’est-ce que l’héritage ? 3

Une utilisation encore trop courante : l’héritage d’implémentation 4

Cas pratique 4

L’héritage d’implémentation en échec 7

La véritable raison d’être de l’héritage : l’héritage d’interface 8

Cas pratique 8

Le polymorphisme 9

L’héritage d’interface et le système de type 10

L’héritage d’interface tient ses promesses 11

L’héritage d’interface permet d’inverser les dépendances 11

L’héritage, au-delà des interfaces 13

Le principe de substitution de Liskov 13

Cas pratique 13

L’héritage n’exprime pas la relation « est un » 14

Pour aller plus loin 14

La bonne méthode pour réutiliser des objets existants : la composition 15

L’héritage d’implémentation est source de rigidité 15

La composition permet d’utiliser l’inversion de dépendance 15

Les méthodes de transfert et la composition 16

POO et réutilisation de code : en bref 17

Page 3: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 3 sur 17

Qu’est-ce que l’héritage ?

L’héritage est un mécanisme de la programmation orientée objet (POO) qui permet de lier deux

classes, que l’on appelle classe-mère et classe-fille. Un tel lien entre deux classes a les

conséquences suivantes :

• la classe-fille dispose de toutes les méthodes et de tous les attributs de portée public et

protected de la classe-mère, et peut les utiliser comme s’il s’agissait des siennes,

• le type de la classe-fille peut être utilisé partout où celui de la classe-mère peut l’être,

• certaines méthodes de la classe-mère peuvent être ré-écrites dans la classe-fille. Ces

méthodes sont appelées méthodes virtuelles, et le mécanisme permettant une telle

modification s’appelle le polymorphisme.

L’héritage permet de réutiliser du code existant, de limiter la duplication, et donc de simplifier la

maintenance du code.

Nous verrons néanmoins que les méthodes pour y parvenir ne sont pas les premières auxquelles

l’on pense. En effet, il peut être utilisé de deux façons différentes :

• pour hériter de l’implémentation d’une classe existante, et ainsi réutiliser son code. On parle

alors d’héritage d’implémentation,

• pour hériter de l’interface d’une classe existante dans le but de permettre à la classe-fille d’être

substituée à la classe-mère de façon polymorphique. Cette utilisation s’appelle héritage

d’interface et rend possible la ré-utilisation de code d’une application qui manipule des

instances de la classe-mère en lui faisant manipuler des instances de la classe-fille, sans que

ce dernier n’ait besoin d’être modifié.

Page 4: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 4 sur 17

Une utilisation encore trop courante : l’héritage d’implémentation

L’héritage est parfois utilisé comme outil pour factoriser du code entre les objets. Lorsque plusieurs

objets utilisent le même code, une solution possible pour éviter la duplication est de placer ce code

dans une classe-mère commune : c’est l’héritage d’implémentation.

Cas pratique

Voici un exemple où l’on a factorisé le code commun aux classes Peugeot et Renault dans une

classe-mère appelée 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 méthodes similaires. . . */ void accelerer() { roue_avant_gauche.tourner_plus_vite(); roue_avant_droite.tourner_plus_vite(); }

Page 5: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 5 sur 17

/* et autres méthodes 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 l’exemple ci-dessus, les classes Peugeot et Renault partagent du code qui a été factorisé

dans la classe-mère Voiture, et il est possible de faire appel à ce code de façon transparente : Peugeot ma_voiture; string mon_modele = ma_voiture.nom_du_modele(); ma_voiture.tourner_a_gauche(); // utilisation du code de la classe-mère ma_voiture.accelerer(); // utilisation du code de la classe-mère

Continuons notre exemple, et supposons désormais que l’on décide d’améliorer notre application

en ajoutant de nouveaux modèles : une Twingo à propulsion (arrière), et une Alpine A110 à

propulsion (arrière) é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 créer deux classes distinctes

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

de l’accélération.

class Voiture{ // Nous enlevons le code qui s’occupe d’accélérer. 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 méthodes similaires. . . */ }; class VoitureTraction : public Voiture { // à l’avant public: void accelerer() { roue_avant_gauche.tourner_plus_vite(); roue_avant_droite.tourner_plus_vite(); } /* et autres méthodes similaires. . . */ }; class VoitureTraction : public Voiture { // à l’arrière public: void accelerer() { roue_arriere_gauche.tourner_plus_vite(); roue_arriere_droite.tourner_plus_vite(); } /* et autres méthodes similaires. . . */ }; class Peugeot : public VoitureTraction{ public: string nom_du_modele() { return « Peugeot 204 »; } // etc. };

Page 6: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | 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 nécessite une re-compilation

de toutes ses classes filles.

Notre application fonctionne correctement, et nous décidons maintenant de rajouter un modèle

supplémentaire : Le 4x4 Citroën C4 AirCross, qui est à traction et à propulsion…

Traction et Propulsion ? Mais alors de quelle classe devra-t-il hériter ? VoitureTraction ou

VoiturePropulsion ? Les deux ?

- Si le 4x4 hérite des deux classes, alors il sera doté de 8 roues ! En effet, 4 sont détenues par la

classe VoitureTraction, et 4 par la classe VoiturePropulsion.

Non seulement une telle situation gaspille de la mémoire car la moitié des roues sont superflues,

mais c’est aussi une source de complexité inutile qui oblige à jongler entre les attributs des deux

classes mères.

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

possibilité d’utiliser un héritage spécial entre la classe VoitureTraction et la classe Voiture,

que l’on appelle l’héritage virtuel. Néanmoins, l’utilisation de l’héritage virtuel est couteuse en

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

toutes les classes qui héritent de VoitureTraction et pas seulement celles de notre classe 4x4.

- Plutôt que de faire hériter notre 4x4 des deux classes VoitureTraction et VoiturePropulsion,

nous pourrions ne le faire hériter que de la classe VoitureTraction, et réécrire les méthodes

d’accélération pour ajouter la gestion des roues arrières. Cependant, cette solution n’est pas

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

c’est justement ce que nous voulons éviter !

Dans tous les cas, nous sommes bloqués : l’héritage d’implémentation ne remplit pas ses

promesses et la duplication de code semble inévitable.

Page 7: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 7 sur 17

L’héritage d’implémentation en échec

On voit ainsi que l’héritage d’implémentation est maladroitement utilisé dans le but de :

• réutiliser du code existant dans de nouvelles classes,

• factoriser du code commun à plusieurs classes dans une classe-mère commune…

…et que son utilisation entraine les inconvénients suivants :

• un couplage fort entre les deux classes qui oblige la classe-fille (et donc toute l’application

qui en dépend) à recompiler à chaque changement de la classe-mère,

• des hiérarchies complexes et impossibles à maintenir qui rendent la duplication de code

inévitable.

C’est justement pour limiter l’utilisation de l’héritage d’implémentation que l’adage « préférer la

composition à l’héritage » s’est répandu ; et nous verrons prochainement que la composition,

lorsqu’elle est utilisée de pair avec le polymorphisme, permet de réutiliser le code d’objets

existants de façon satisfaisante.

En clair, nombreux sont ceux qui voient dans l’héritage un moyen de réutiliser

des objets existants en s’épargnant d’avoir à écrire des méthodes de

transfert[1]. Mais c’est oublier que l’héritage a pour unique vocation de

permettre la réutilisation du code client via le polymorphisme, dont il est le

vassal, comme nous allons le voir.

Il est donc bien question d’une mauvaise compréhension du rôle de l’héritage,

dont l’utilisation dénaturée va à contresens des objectifs de la POO et produit

du code rigide, qui entraine :

• un couplage fort, inutile entre deux classes,

• des hiérarchies trop complexes,

• et à terme : de la duplication de code.

[1] voir chapitre sur la composition

Page 8: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 8 sur 17

La véritable raison d’être de l’héritage : l’héritage d’interface

Comme nous l’avons vu dans la partie précédente, l’héritage ne permet pas de réutiliser des objets

existants. Il a en fait été conçu pour accompagner le polymorphisme dans le but de permettre la

réutilisation d’un autre type de code : le code d’application, c’est à dire le code qui utilise nos

objets.

Cas pratique

Afin d’illustrer l’intérêt du polymorphisme et de l’héritage d’interface, voici un exemple où nous

sommes chargés de l’écriture d’une fonction crypter_fichier capable d’encrypter 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 ); }

Page 9: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 9 sur 17

Quelques jours plus tard, nous sommes chargés d’écrire une nouvelle fonction capable de crypter des fichiers situés sur le réseau : 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 l’algorithme de cette seconde fonction est en tout point similaire à celui de la

première : ouvrir le fichier source ; calculer le contenu crypté ; remplacer le fichier source.

Malheureusement, à cause de détails liés à la nature des fichiers, il nous est impossible de

réutiliser notre fonction initiale.

Condamnés à réécrire la même fonction encore et encore ?

Heureusement non, grâce au polymorphisme, qui permet de réutiliser la même fonction dans les

deux cas…

Le polymorphisme

Le polymorphisme permet de manipuler deux objets qui se comportent de la même façon sans

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

crypter tout type de fichier, en faisant abstraction des détails nécessaires à leur manipulation.

Or, afin d’être en mesure de manipuler deux objets différents de la même façon, il est nécessaire

que ces deux objets comprennent, et soit capables d’exécuter, les mêmes instructions. Cela veut

dire que ces objets doivent mettre à disposition les mêmes méthodes dans leurs interfaces

respectives.

Par exemple, les objets WebFile et DiskFile pourraient proposer les méthodes 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 très simple de réutiliser le même code d’application pour manipuler indifféremment

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 ); }

Page 10: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 10 sur 17

Cependant, un problème se pose pour les utilisateurs de langages statiquement typés 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 l’utiliser avec des instances de

DiskFile, et réciproquement.

La solution réside dans l’utilisation de l’héritage d’interface, qui définit un contrat, passé par un

objet vis-à-vis de ses utilisateurs.

L’héritage d’interface et le système de type

L’héritage d’interface est utilisé dans les langages statiquement typés (C++, Java, etc.) pour

satisfaire aux contraintes imposées par le système de type. Il n’est d’ailleurs a priori nécessaire

que dans ces systèmes.

Il permet en effet à différentes classes de déclarer qu’elles remplissent un même contrat vis-à-vis

de leurs utilisateurs, et donc qu’elles peuvent être interchangées.

L’héritage d’interface met en scène deux acteurs :

• une classe-mère responsable de définir le contrat, on l’appelle Interface,

• et une classe-fille qui s’engage à respecter le contrat énoncé.

Dans notre étude de cas, l’héritage d’interface est la clef qui permet de réutiliser la fonction

crypter_fichier avec différents types de fichiers :

class File{ // Déclaration de l’Interface public: virtual string obtenir_contenu() = 0; virtual void ecraser_contenu_avec( string nouveau_contenu ) = 0; }; class DiskFile : public File{ /* Implémentation spécifique pour les fichiers locaux */ }; class WebFile : public File{ /* Implémentation spécifique pour les fichiers web */ }; void crypter_fichier( File& fichier_source ){ string contenu = fichier_source.obtenir_contenu(); string nouveau_contenu = crypter_string( contenu ); fichier_source.ecraser_contenu_avec( nouveau_contenu ); }

Remarquons qu’il est très simple de faire évoluer ce code pour prendre en compte les fichiers

situés sur un DVD, puisqu’il suffit d’ajouter une classe DVDFile, que l’on pourra utiliser avec la

fonction crypter_fichier sans avoir besoin de la modifier.

Remarquons de plus que la relation « A hérite de B » devrait être lue comme « A spécialise B »,

ou encore comme « A injecte son code dans B ».

En effet, on peut considérer l’Interface File comme une classe :

• qui n’a pas d’implémentation au moment de la compilation,

• et qui utilise le code d’une de ses classes filles (DiskFile, WebFile, etc.) au moment de

l’exécution.

Page 11: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 11 sur 17

L’héritage d’interface tient ses promesses

La réutilisation de code est ainsi rendue possible par la POO, qui propose des mécanismes

permettant à du code existant de manipuler de façon générique différents objets, à travers une

même interface.

Les avantages d’une telle approche sont multiples, car non seulement la POO limite la

duplication de code — comme nous l’avons vu avec l’exemple de la fonction crypter_fichier

— mais elle rend aussi possible l’extension des fonctionnalités du code existant en lui

permettant de manipuler de façon transparente de nouvelles classes (telles DVDFile).

Ces mécanismes s’utilisent de pair pour répondre à différentes contraintes :

• le polymorphisme permet l’utilisation générique de différents objets, et donc la

réutilisation d’un même code d’application avec ces objets ;

• en parallèle, l’héritage d’interface rend possible la mise en oeuvre du polymorphisme

dans les langages statiquement typés. Son utilisation permet d’informer le compilateur qu’une

classe respecte une Interface donnée, et donc qu’elle peut être utilisée de façon

polymorphique.

En résulte un code qui est à la fois :

• capable de bénéficier des vérifications apportées par le système de type ;

• ouvert à l’extension, et fermé à la modification.

L’héritage d’interface permet d’inverser les dépendances

Si la POO rend possible l’écriture d’un code souple, c’est parce qu’elle permet d’inverser les

dépendances de compilation et d’exécution ; c’est à dire que que le code qui manipule un objet

peut être compilé indépendamment de cet objet.

En d’autres termes, la POO permet de dissocier :

• le fait que la fonction crypter_fichier dépend des classes qu’elle manipule lors de

l’exécution (par exemple, elle peut manipuler la classe DVDFile),

• et le fait qu’elle puisse être compilée avant même que ces classes ne soient écrites (par

exemple, la classe DVDFile peut avoir été écrite et compilée après la fonction

crypter_fichier) !

Les schémas suivants illustrent l’inversion de dépendance rendue possible par l’utilisation

conjointe de l’héritage et du polymorphisme :

Schéma 1 : Sans polymorphisme, l’application (A) dépend de l’objet (B) qu’elle utilise.

En conséquence, toute modification apportée à (B) oblige une re-compilation de (A).

Page 12: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 12 sur 17

Schéma 2 : Grâce au polymorphisme, l’application (A) ne dépend plus de l’objet (B) qu’elle utilise, mais seulement

d’une Interface (I). L’implémentation de (B) peut changer sans que (A) n’ait besoin d’être re-compilée.

Remarquons que l’inversion de dépendance n’a pas pour seul avantage de permettre l’extension

du code existant. Cette dernière permet aussi de modifier l’implémentation des objets

existants sans que cela n’impose une re-compilation du code qui les utilise.

En effet, étant donné que le code d’application ne dépend que d’une Interface, libre au

programmeur de modifier en secret la partie privée de ces objets dans laquelle résident les détails

de leur implémentation. Cette dichotomie entre interface publique d’une part et implémentation

privée d’autre part s’appelle l’encapsulation et constitue le troisième fer de lance de la POO.

En clair, la POO repose sur les trois notions fondamentales suivantes :

• l’héritage d’interface,

• le polymorphisme,

• et l’encapsulation,

qui, lorsqu’elles sont utilisées conjointement, rendent possible l’écriture de code :

• facilement réutilisable,

• ouvert à l’extension, mais fermé à la modification.

L’héritage est donc bien le vassal du polymorphisme, dont il rend l’utilisation possible au sein des

langages statiquement typés.

La relation « A hérite de B » permet non seulement d’informer le compilateur que A présente la

même interface que B, mais plus encore, il s’agit d’un contrat passé par la classe-fille A envers ses

utilisateurs.

Ce contrat établit un principe de substituabilité qui dépasse la simple déclaration d’interface

; c’est pourquoi, contrairement à la croyance populaire, la relation « A est un B » n’est pas

synonyme de « A hérite de B ».

Page 13: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 13 sur 17

L’héritage, au-delà des interfaces

Le principe de substitution de Liskov

“What is wanted here is something like the following substitution property: If for each object o1 of

type S there is an object o2 of type T such that for all programs P defined in terms of T, the

behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.”

Barbara Liskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (1988).

L’héritage est un contrat bien plus fort qu’un simple engagement à définir une interface publique.

Il indique en effet qu’une classe (la classe-fille) peut être substituée à une autre (la classe-mère).

Par conséquent, la classe-fille doit non seulement proposer une implémentation des méthodes

définies par l’Interface, mais ces méthodes doivent aussi se comporter de façon similaire à ce qui

est attendu, c’est à dire respecter les mêmes pré- et post-conditions1 .

L’exemple classique d’illustration du principe de substitution de Liskov est celui du carré et du

rectangle.

Cas pratique

Supposons que l’on dispose d’une Interface définissant le comportement d’un rectangle :

class Rectangle{ public: virtual void remplacer_largeur_par (unsigned int nouvelle_largeur ) = 0; virtual void remplacer_longueur_par(unsigned int nouvelle_longueur) = 0; /* et autres méthodes . . .*/ };

Comme le stipulent les principes de responsabilité unique (SRP) et de moindre surprise (PoLA), la

méthode remplacer_largeur_par(…) n’a pour seule fonction que de modifier la largeur du

rectangle.

Il serait d’ailleurs absurde et totalement inattendu que cette méthode modifie la longueur du

rectangle, ou tout autre attribut du rectangle.

Supposons de plus que nous ayons à notre disposition une application capable de manipuler des

rectangles : unsigned int calculer_laire_de ( Rectangle& mon_rectangle ); void dessiner_sur_lecran ( Rectangle& mon_rectangle ); /* et autres fonctions . . .*/

Afin de pouvoir réutiliser cette application avec notre classe Carré, nous pourrions être tentés de

faire hériter la classe Carré de la classe Rectangle.

Néanmoins, il faudrait alors que la classe Carré fournisse une implémentation des méthodes

remplacer_largeur_par(…) et remplacer_longueur_par(…).

1 voir rubrique « Pour aller plus loin »

Page 14: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 14 sur 17

Or, comme par définition un carré est un rectangle dont la largeur vaut la longueur, cela implique

que la méthode remplacer_largeur_par(…) devrait mettre à jour non seulement la largeur, mais

aussi la longueur du carré ! Alors même que cette méthode n’est pas censée modifier un

autre attribut que la largeur…

Un tel comportement ne respecte pas le principe de substitution et sera souvent source de bugs

insidieux dans l’application. Par conséquent, la classe Carré ne doit pas hériter de la classe

Rectangle, et ce en dépit de l’assertion vraie qui dit que : « un carré est un rectangle ».

L’héritage n’exprime pas la relation « est un »

Nous avons vu dans le cas pratique ci-dessus que l’héritage ne doit pas être utilisé pour

exprimer la relation « est un ». Par exemple, un carré est un rectangle, mais la classe Carré ne

doit pas hériter de la classe Rectangle.

Cela s’explique par le fait que généralement, les représentants n’entretiennent pas les mêmes

rapports que les objets qu’ils représentent.

Par exemple, deux personnes qui engagent une procédure de divorce seront chacune représentée

par un avocat. Ces deux personnes sont mariées, mais il est peu probable que leurs représentants

— c’est à dire les avocats — le soient aussi.

De la même façon, les classes peuvent parfois représenter des objets du monde réel, mais il est

peu probable qu’elles entretiennent les mêmes rapports que ces derniers.

Pour aller plus loin

En résumé, l’héritage rend possible l’utilisation polymorphique de différents objets en définissant

un contrat passé par ces derniers vis-à-vis de leurs utilisateurs. Non seulement ce contrat définit

explicitement une liste de méthodes publiques que ces objets doivent implémenter, mais il

implique aussi le respect implicite des pré- et post-conditions liées à ces méthodes afin de garantir

que ces objets puissent être substitués les uns aux autres.

Au sujet des pré-/post-conditions, l’on pourra consulter l’article Wikipédia suivant :

• [1] Programmation par contrat - http://fr.wikipedia.org/wiki/Programmation_par_contrat

Et pour approfondir la notion de substituabilité, l’on pourra se rapporter à l’article suivant :

• Liskov substitution principle - http://www.objectmentor.com/resources/articles/lsp.pdf

Rappelons de plus que l’héritage n’est conçu ni pour modéliser la relation « est un », ni pour

permettre la réutilisation du code d’objets existants.

Mais alors comment réutiliser correctement le code des objets existants ?

Page 15: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 15 sur 17

La bonne méthode pour réutiliser des objets existants : la composition

L’héritage d’implémentation est source de rigidité

Nous avons déjà vu que l’héritage était souvent utilisé à tort pour factoriser du code commun à

plusieurs objets, ou pour réutiliser le code d’objets existants.

L’utilisation de l’héritage d’implémentation provoque la mise en place de hiérarchies complexes.

Elle induit en outre un couplage fort entre la classe-mère et la classe-fille, alors que ce couplage

pourrait être évité. En résulte un code difficile à changer et des re-compilations superflues.

Le couplage fort qui est induit par l’héritage s’explique par le fait que lorsqu’une classe-fille hérite

d’une classe-mère, elle est dotée de tout le code de la classe mère. Par conséquent, tout

changement apporté au code de la classe-mère entraine un changement implicite du code de la

classe-fille. C’est à dire que lorsque la classe-mère change, il faut re-compiler la classe-fille pour

prendre en compte ces changements.

Ce couplage fort est à contre-sens des objectifs de la POO, qui a pour but de favoriser la

réutilisation de code et de limiter la propagation des changements. C’est pourquoi l’héritage

d’implémentation ne peut pas être considéré comme une pratique orientée-objet.

La réutilisation d’objets existants reste cependant possible en POO grâce à la composition, qui

exprime au mieux cette intention. En effet, qu’est-ce que la composition, sinon le fait de fournir un

objet M à un objet F dans le but d’en permettre la (ré-)utilisation ?

La composition permet d’utiliser l’inversion de dépendance

D’une part, la composition modélise au mieux les intentions du programmeur qui souhaite réutiliser

un objet existant ; d’autre part, elle peut aussi être utilisée de pair avec le polymorphisme, dans le

but de limiter la propagation des changements.

En effet, nous avons vu que l’héritage d’interface permettait de découpler deux objets via

l’introduction d’une Interface.

Page 16: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 16 sur 17

Pour rappel, voici une illustration de ce phénomène que l’on nomme inversion de dépendance :

Schéma 1 :

Sans polymorphisme, l’objet (A) dépend de l’objet (B) qu’il

utilise.

Schéma 2 :

Grâce au polymorphisme, l’objet (A) ne dépend plus de

l’objet (B) qu’il utilise, mais seulement d’une Interface (I).

La composition permet ainsi de réutiliser des objets existants sans introduire de couplage fort,

lorsqu’elle est utilisée conjointement avec le polymorphisme.

Les méthodes de transfert et la composition

Certains programmeurs préfèrent utiliser l’héritage d’implémentation plutôt que la composition car

cette dernière ne permet pas l’import automatique des méthodes publiques de l’objet utilisé dans

l’interface publique de l’objet qui l’utilise. Dans l’état actuel de nos langages de programmation,

son utilisation requiert en effet l’écriture de méthodes de transfert.

Voici un exemple de méthode de transfert :

class ObjetUtilise{ public: virtual void methode_interessante(Type argument1, AutreType argument2) = 0; }; class ObjetUtilisateur{ public: ObjetUtilisateur(ObjetUtilise& outil); void methode_interessante(Type argument1, AutreType argument2){ //<— Méthode de transfert outil.methode_interessante(argument1, argument2); } };

L’obligation d’écrire une méthode de transfert est un détail d’implémentation. Elle découle d’une

lacune des langages de programmation actuels — qui pêchent par manque de

fonctionnalités — et non d’une faiblesse de la POO.

En C++, par exemple, il est possible de pallier cette lacune via l’utilisation d’une Macro et des

type_traits, en attendant que cette fonctionnalité soit ajoutée au langage.

class ObjetUtilisateur{ public: ObjetUtilisateur(ObjetUtilise& outil);

USING( outil, methode_interessante ); };

Page 17: La POO et le secret de l’héritage · La POO et le secret de l’héritage Julien E. Harbulot | web@julienh.fr Février 2015 Depuis la popularisation du paradigme de programmation

La POO et le secret de l’héritage | Julien E. Harbulot

Page 17 sur 17

Pour plus d’information à ce sujet, l’on consultera utilement l’adresse suivante :

• https://bitbucket.org/Gauss_/method-forwarding-using-compositon-instead-of-inheritance

En conclusion, la composition est la meilleure façon du réutiliser des objets

car elle :

• renseigne immédiatement sur les intentions du programmeur,

• n’induit pas de couplage fort,

• favorise un design souple et maintenable.

Son utilisation va dans le sens des objectifs de la POO et ne constitue en rien

un aveu de faiblesse de cette dernière. Au contraire, l’utilisation de la

composition est une bonne pratique de programmation orientée-objet qui

favorise l’utilisation du polymorphisme.

POO et réutilisation de code : en bref

Réutilisation des objets existants

• mauvaise méthode : héritage d’implémentation

• bonne méthode : composition + méthode de transfert (forwarding)

Réutilisation du code client (celui qui utilise des objets)

• bonne méthode : héritage d’interface (donc polymorphisme)