PHP: héritage multiple
by Joris on avril 13, 2010
PHP est un langage qui ne supporte pas l’héritage multiple depuis le début de sa pseudo-implémentation objet. L’héritage multiple apporte bien des soucis aux architectes logiciels car le point fort et le point faible de l’héritage multiple est le fait qu’il soit multiple
.
Dans cet article, je vais vous montrer qu’il est possible de gérer plus ou moins dynamiquement l’héritage multiple grâce au concept d’agrégation, aux interfaces ainsi que les méthodes magiques de PHP.
Pour illustrer le plus simplement possible, je vous ai concocté un petit exemple concret qui permet de mettre en œuvre cet héritage multiple. Pour cela, il faut que vos classes implémentent toute une interface qui leur permettra d’avoir les même méthodes.

Le concept
La notion d’héritage est une spécialisation descendante des classes (analogiquement une généralisation montante) et permet de développer en respectant la méthode DRY (Don’t Repeat Yourself) mais aussi de mettre en application les principaux concepts de la POO comme la surcharge, le polymorphisme ou les notions de constructeurs et de destructeurs.
Encore une fois, le langage PHP ne permet pas de mettre réellement en application certaines de ces notions car par exemple, la surcharge de méthodes au sein d’une même classe n’est pas possible. Mais cela le devient grâce à aux méthodes magiques __call() et __callStatic().
L’implémentation d’interfaces permet de mettre en commun des méthodes publiques de certaines classes. Cette pratique très utile oblige les classes qui implémentent les même interfaces à être homogène d’une certaine manière afin d’apporter une sécurité d’interaction avec l’environnement d’utilisation des classes.
Donc pour faire un mini-bilan, la notion d’héritage/généralisation se profile plutôt verticalement dans l’espace alors que l’implémentation d’interfaces se profile horizontalement.
Mise en œuvre
Pour mettre en place un pseudo-héritage multiple en PHP, nous allons créer un simple héritage d’une super-classe qui aura dans son bagage de méthodes des accesseurs permettant de gérer une pile de « classes dont la classe fille héritera ». Nous utiliserons donc les plus communes méthodes magiques.
Nous prendrons bien évidemment une version PHP 5.3 afin de bénéficier des dernières fonctionnalités.
Les classes héritées seront agrégées dans la classe mère et doivent impérativement implémenter une interface commune afin de certifier les même signatures de méthodes. Une fois les classes agrégées, la méthode call s’occupera d’appeler toutes les méthodes ayant le même nom de la méthode d’appel… Ainsi de suite, l’ordre d’appel des méthodes sera le même que l’ordre d’héritage.
Classe Abstract_Foo
<?php abstract class Abstract_Foo { protected $_inheritances = array(); public function __isset($class_name) { return array_key_exists($class_name, $this->_inheritances); } public function __get($class_name) { try { if (!isset($this->$class_name)) { throw new Exception($class_name . ' does not exists!'); } return $this->_inheritances[$class_name]; } catch (Exception $e) { trigger_error($e->getMessage(), E_USER_WARNING); return false; } } public function __set($class_name, $instance) { try { if (isset($this->$class_name)) { throw new Exception(get_class($this) . ' already extends ' . $class_name); } if (!$instance instanceof $class_name) { throw new Exception('Unexpected instance of ' . get_class($instance) . ' instead of ' . $class_name); } $this->_inheritances[$class_name] = $instance; } catch (Exception $e) { trigger_error($e->getMessage(), E_USER_WARNING); } } public function __unset($class_name) { unset($this->_inheritances[$class_name]); } public function call($method_name, $args = null) { try { foreach ($this->_inheritances as $obj) { if (!is_callable(array($obj, $method_name))) { throw new Exception('Undefined method ' . get_class($obj) . '::' . $method_name . '()'); } $obj->$method_name($args); } } catch (Exception $e) { trigger_error($e->getMessage(), E_USER_WARNING); } } public function getApple() { var_dump(__METHOD__); $this->call(__FUNCTION__); } public function eatApple() { var_dump(__METHOD__); $this->call(__FUNCTION__); } } ?>
Classe Foo
<?php class Foo extends Abstract_Foo implements Interface_Frob { } ?>
Interface Interface_Frob
<?php interface Interface_Frob { public function getApple(); public function eatApple(); } ?>
Classe Bar
<?php class Bar implements Interface_Frob { public function getApple() { var_dump(__METHOD__); } public function eatApple() { var_dump(__METHOD__); } } ?>
Classe Baz
<?php class Baz implements Interface_Frob { public function getApple() { var_dump(__METHOD__); } public function eatApple() { var_dump(__METHOD__); } } ?>
Page de test
<?php function __autoload($class_name) { if (is_file($class_name . '.php')) { require_once $class_name . '.php'; } } $foo = new Foo(); $foo->Bar = new Bar(); $foo->Baz = new Baz(); $foo->getApple(); $foo->eatApple(); ?>
Résultat attendu
string 'Abstract_Foo::getApple' (length=22) string 'Bar::getApple' (length=13) string 'Baz::getApple' (length=13) string 'Abstract_Foo::eatApple' (length=22) string 'Bar::eatApple' (length=13) string 'Baz::eatApple' (length=13)
De cette manière-ci, vous pouvez mettre en place un pseudo-héritage multiple pour vos applications sans trop se prendre la tête en attendant que Traits soit implémenté. Évidemment, Traits est bien plus poussé car ce sera inclut dans la structure du langage alors qu’ici, il ne s’agit que d’une bidouille orientée objet.
Vivement PHP 5.4
.

9 comments
Merci pour cet article, j’ai un peu de mal avec ce genre de choses qui sont pourtant fondamentales dans les langages OO. J’espère que j’aurais tout assimilé avant que cela rentre vraiment en application sur PHP
.
Mais comme tu le dis, vivement la prochaine version de PHP !
by pocky on 13/04/2010 at 18:37. #
Bien que PHP ne soit pas un langage des plus strict et qu’il ne respecte pas tous les principes de la POO, le fait qu’il n’implémente pas l’héritage multiple est loin d’être une tare pour moi (et pourtant il en a !).
L’héritage multiple n’est implémenté que dans assez peu de langages OO, même chez les plus stricts, parce qu’il pose certains problèmes lors d’une utilisation avancée des principes OO. Il ne l’est pas en Java ni en C# par exemple.
Le problème se pose par exemple si un attribut ou une méthode porte le même nom dans les deux classes à hériter, lequel ou laquelle choisir ? Ou en cas d’utilisation du polymorphisme, quelle méthode appeler à partir des classes parentes ? Beaucoup de langages ont contourné le problème en empêchant tout simplement de faire de l’héritage multiple (Java propose par exemple d’utiliser plus amplement les interfaces), d’autres proposent des compromis (C++ par exemple) mais le problème reste tout de même présent.
Bref, à mon sens je ne trouve pas que ce soit réellement une bonne chose d’intégrer l’héritage multiple, surtout dans un langage aussi peu strict que PHP, qui de plus est interprété, ce qui peut réserver des surprise à l’exécution en cas d’utilisation du polymorphisme… Et je pense qu’il faudrait d’abord penser à coller plus aux principes fiables de la POO avant de penser à implémenter de telles choses.
Je ne dénigre pas l’article, cette solution fonctionne, mais je pense que la priorité n’est pas la.
Bonne continuation et comme tu dis, vivement la prochaine version de PHP
by Tim on 13/04/2010 at 19:49. #
Il y a juste un sérieux problème: il ne s’agit pas du tout d’héritage multiple!
Les diverses classes héritées n’ont pas à fournir la même interface. Ca n’est aucunement une condition nécessaire. A vrai dire, hériter d’interfaces (et d’implémentations) similaires est problématique au point que l’héritage multiple est prohibé dans la plupart des langages OOP. Tim en a parlé juste avant moi.
Quand, malgré tout, deux méthodes ou attributs portent le même nom dans des branches distinctes de la hiérarchie, des règles qui varient selon les langages sont appliquées pour que seule une méthode soit appelée. Dans l’implémentation proposée ici, toutes les méthodes sont appelées à la suite. Comportement hasardeux garanti, pour ne pas dire suicidaire
Pour les attributs (ou les properties), c’est la même chose: quand on accède à un attribut, on accède à UN attribut et non pas les N attributs qui ont le même nom dans la hiérarchie.
by metagoto on 14/04/2010 at 08:21. #
Metagoto m’a grillé.
Je n’ai qu’un mot à dire : Amen !
Par contre, j’ai fait de l’héritage multiple assez poussé en C++ pendant quelques années.
Je n’ai jamais rencontré les problèmes mentionnés.
Il faut dire que l’arborescence d’héritage était bien conçu et que fonctionnellement, le programme s’y prétait très bien, donc ceci explique peut être cela.
Et php 5.4 n’existe pas, et n’existera peut être jamais.
Pour l’instant, il existe un truc bizarre qui s’appelle php 5.3.99.
Et l’implémentation des Traits est encore sujette à débat, justement au niveau de ces histoires de résolution de nom au niveau des méthodes et des attributs.
N’en espérez pas trop, j’ai tout lieu de penser que la première version des Traits sera aussi « mauvaise » en terme de fonctionnalité que celle des closures de php 5.3.
Dernier point, vu que __call() et consors ne gére pas le passage d’argumentq par référence, la bidouille décrite dans le billet n’est pas applicable si l’une des méthodes utilisent des références.
by fch on 14/04/2010 at 09:21. #
Bonjour,
Je viens de lire par hasard cet article et je souhaite apporter un autre point de vue.
L’héritage multiple permet d’appliquer à une classe un comportement déjà implémenté dans plusieurs autres classes (DRY). On est d’accord.
Par exemple, j’ai une classe véhicule (abstraite pour l’occasion) et deux classes bateau et avion qui héritent de véhicule. Véhicule défini une méthode « avancer » qui est spécialisée par bateau et avion. J’ai une classe hydravion qui peut soit « avancer » comme « bateau » soit comme « avion »
Merci de considérer que ce n’est qu’un exemple
je sais bien qu’un hydravion n’a pas d’hélice sous marine ! 
Naïvement, non péjoratif, je suis tenté de faire de l’héritage multiple ou de créer une interface Vehicule spécifiant « avancer » et à implémenter dans avion, bateau et hydravion. Avec un héritage multiple, je peux implémenter des opérations dans véhicule disponible dans les classes filles. Avec une interface, je ne peux que spécifier un contrat qui sera implémentée dans les classes filles.
On peut arriver rapidement à une multiplication de code pour gérer « avancer » suivant le véhicule.
Pourtant, on pourrait considérer le problème autrement. Un véhicule « avance » en fonction d’un contexte (sur l’eau, sur la route, de nuit, dans le brouillard)
Un hydravion est un véhicule au même titre qu’un bateau et un avion. Hydravion, bateau, avion spécialisent véhicule. Dans la super classe véhicule, je définis certaines caractéristiques qui sont ensuite adaptée dans les classes filles. Par exemple, « a des roues, a des ailes, a des phares ».
Je peux créer une méthode avancer qui sélectionnera l’attitude à adopter en fonction du véhicule et du contexte cible ou déléguer cela à une classe spécialisée pour faire avancer un véhicule.
A cette unité de code, je fournis le véhicule, le contexte et elle sélectionne la meilleure façon de réaliser « avancer ».
L’unité de code en charge de « avancer », en fonction des caractéristiques du véhicule et du contexte, peut choisir le meilleur moyen pour réaliser l’action. Je peux même chainer certains comportement en fonction du contexte.
Finalement, j’ai centralisé mon code « avancer », il est générique, peut s’adapter, être étendu, je simplifie par rapport à de l’héritage multiple, je formalise beaucoup mieux qu’avec une interface, je peux chainer des actions génériques (allumer les phares de nuit si le véhicule en possède, etc)
Bon, c’est un exemple rapide mais par exemple cela prends tout son sens dans un calcul d’itinéraire. Au lieu que chaque véhicule calcule sont itinéraire en fonction de ces moyens (avec tout ce que cela implique), je m’adresse à une unité « calcul itineraire » qui, en fonction du véhicule que je lui donne et de ses caractéristiques, se charge de déterminer le meilleur itinéraire.
Bien sûr, on est obligé de mettre à jour l’unité « avancer » dès qu’un nouveau véhicule est disponible mais en se basant sur des comportements réutilisables, ce que l’on souhaite faire avec l’héritage ou les interfaces, on arrive rapidement à un système capable de faire avancer tous les véhicules. L’énorme avantage est que en centralisant, je peux mettre à jour mes fonctionnalités sans aller modifier plusieurs unité de code.
De « comment je fais pour », on passe à « dis moi que tu as et ce que tu veux et je te dirais comment faire ».
by gabriel on 14/04/2010 at 13:14. #
@Tim
.
Quand 2 classes mères généralisent une classe fille qui ont les même noms de méthodes/propriétés, c’est théoriquement l’ordre de déclaration des héritages qui permet de sélectionner quelle méthode/propriété à choisir.
Ici, j’ai exactement implémenté le même principe qu’en Java sauf qu’ici le typage n’est pas aussi poussé qu’en Java et ce rendant le mécanisme plus vulnérable.
Je suis tout à fait d’accord avec toi en ce qui concerne l’implémentation des grands principes OO avant celle de Traits
@Metagoto
.
Comme précédemment écrit à Tim dans ma réponse, l’utilisation d’interface n’est qu’un hack permettant une certaine robustesse dans cette pseudo-implémentation de l’héritage multiple. Bien entendu, dans les langages acceptant ce principe, l’utilisation d’interface n’est pas nécessaire voire même inutile.
Quant à l’appel successif des méthodes/propriétés, c’est simplement mon exemple qui fonctionne ainsi… Rien ne t’empêche de surcharger les méthodes de la classe abstraire dans ta classe concrète et de choisir d’appeler tel ou tel composant hérité
@fch
Je suis tout à fait d’accord en ce qui concerne ton point de vu sur l’approbation de Traits dans une prochaine version de PHP… Ne vaudrait-il pas mieux que Traits puisse être ajouté via une extension PECL plutôt ?
Concernant ma méthode call, comme écrit à Metagoto, ça n’est que mon exemple… Tu es libre de l’adapter afin de satisfaire tes besoins en terme de souplesse.
@gabriel
Ton concept ne me plait pas du tout… A quoi sert l’héritage (la spécialisation) si c’est pour se retrouver avec un bloc super compliqué et relativement spécialisé dans ta classe mère ?
Certes le contexte influe sur le contenu des actions a réaliser mais en aucun cas la classe parente devrait contenir du code spécialisé. Je pense qu’il y a toujours un moyen d’écrire du code DRY en éclatant bien les fonctionnalités mère/fille sinon l’analyse conceptuelle n’est pas bonne.
En utilisant l’héritage correctement et dans un bon contexte, on devrait toujours pouvoir spécialiser une action en la surchargeant…
by Joris on 15/04/2010 at 01:38. #
Je serais tout à fait d’accord avec toi si nous n’étions pas en train de parler d’héritage multiple. Dans une hiérarchie relativement simple, si le descendant apporte toujours quelque chose d’unique, ca ne sert à rien. A partir du moment où on envisage de l’héritage multiple cela a du sens.
Dans la réalité, il y aura peu de chance que la super classe embarque un tel code (cf mon exemple plus réaliste de recherche d’itinéraire) toutefois, dans la mesure où le comportement « avancer » est de la responsabilité du véhicule cela reste valable. Si on pousse plus loin mon exemple, comment résoudre « avancer » pour moto, camion, voiture, velo, etc. ?
Le cas que je décris réalise le motif stratégie et dans ce cas, il permet de se passer d’héritage multiple. De toutes façons (sans vouloir généraliser) l’héritage multiple apporte toujours une complexité à la conception et à l’évolution.
by gabriel on 15/04/2010 at 08:44. #
@Joris : Tu pourras essayer d’adapter tout ce que tu veux, jamais tu n’arriveras à faire passer une référence à __call().
Le langage ne le permet tout simplement pas, à moins d’autoriser les trucs genre $foo->bar(& $var) dans le php.ini via allow_call_time_by_reference.
by fch on 15/04/2010 at 09:13. #
@Joris
OK, mais si on apporte les correctifs que tu suggères, c’est toute ta démonstration et ton implémentation qui s’écroulent. (ça peut arriver à tout le monde
)
L’héritage multiple est à manier avec précaution. Ce n’est pas par hazard si cette feature est peu répandue. Dans un cadre où on souhaite faire du polymorphisme, on a tout intérêt à ce que les branches de la hiérarchie soient orthogonales au maximum. L’exemple avec Avion, Bateau, MachinAvecRoues est caractéristique: c’est la porte ouverte aux emmerdes. Bien souvent il vaut mieux privilégier la composition, l’aggregation et la delegation.
En C++, l’héritage multiple est principalement utilisé en metaprogrammation, c’est à dire que la hiérarchie est « linéarisée » à la compilation pour former un nouveau type qui ne sera que très peu voir pas du tout utilisé dans un cadre polymorphe. D’ailleurs, les Traits de php vont dans ce sens.
by metagoto on 15/04/2010 at 18:46. #