Javascript : l’héritage multiple

Temps de lecture : 6 min

Nous avons vu dans le précédent article, la manière d’implémenter les notions de classes abstraites et d’interfaces en JavaScript. J’ai également brièvement rappelé que l’héritage multiple n’existait pas en JavaScript, mais comme vous vous en doutez, il existe une façon de pallier à ce problème, c’est ce que nous allons voir dans cet article.

Rafraîchissons-nous la mémoire

Afin de s’attaquer au problème de l’héritage multiple, il est important de faire quelques rappels sur la notion de classe et d’héritage en JavaScript.

Les classes

Pour ceux qui ont découvert JavaScript avec l’arrivée ECMAScript 6, déclarer une classe se fait simplement, comme n’importe quel langage orienté objet, à l’aide du mot clé class :

Cette notation, bien que pratique, cache la vraie nature de JavaScript : JavaScript n’est pas un langage orienté objet, mais un langage orienté prototype. Voyons la définition de la programmation orientée prototype par notre cher ami Wikipédia :

La programmation orientée prototype est une forme de programmation orientée objet sans classe, basée sur la notion de prototype. Un prototype est un objet à partir duquel on crée de nouveaux objets.

Concrètement, on a un objet qui sert de modèle structurel permettant de définir des propriétés et des méthodes aux objets se basant sur celui-ci. Cet objet peut évoluer dynamiquement à l’exécution, contrairement à une classe qui est figée à la compilation.

Avant l’arrivée d’ECMAScript 6, la déclaration d’une “classe” se faisait comme suit :

Cette notation est plus explicite et permet de comprendre un peu mieux ce qui se cache derrière le mot clé class d’ECMAScript 6.

On a donc une fonction Shape qui est utilisée comme constructeur et un objet prototype attaché à celle-ci. Afin de comprendre à quoi sert l’objet prototype et comment est utilisé le constructeur en interne, nous allons voir grosso modo ce qui se passe lorsque l’on créer un objet à l’aide de l’opérateur new :

  • Un nouvel objet est créé à partir de l’objet prototype du constructeur;
  • Le constructeur est appelé en lui fournissant comme valeur this l’objet nouvellement créé;
  • L’objet est retourné.

L’implémentation naïve de l’opérateur new pourrait ressembler à ça :

Lorsqu’un objet est crée, celui possède automatiquement d’une propriété __proto__ qui est un objet pouvant contenir des propriétés et des méthodes. Dans le cas de l’utilisation de l’opérateur new, cette propriété __proto__ contient l’objet prototype du constructeur. Voyons ceci avec notre classe Shape:

La propriété __proto__ de notre instance possède bien les méthodes setHeight et setWidth de l’objet prototype du constructeur Shape.

Lors de l’appel d’une propriété ou d’une méthode d’un objet, JavaScript va chercher celle-ci sur l’objet en question, puis dans son prototype, puis dans le prototype du prototype et ainsi de suite, jusqu’à ce qu’il trouve la propriété ou la méthode en question ou qu’il aboutisse à un prototype null. C’est ce qu’on appelle la chaîne des prototypes.

C’est cette chaine des prototypes qui permet l’héritage en JavaScript.

L’héritage

Pour voir, en détails, comment l’héritage fonctionne nous allons reprendre l’exemple de notre classe Shape et créer une nouvelle classe Rectangle qui hérite de celle-ci :

  1. Nous créons un constructeur Rectangle;
  2. Nous appelons dans celui-ci, le constructeur Shape via la méthode call en lui passant comme premier paramètre this suivit des paramètres w et h;
  3. Nous créons un nouvel objet qui aura comme propriété __proto__ le prototype du constructeur Shape via la méthode Object.create puis nous l’affectons au prototype du constructeur Rectangle;
  4. Comme nous venons d’affecter le prototype de Shape avec le nouvel objet créé, la propriété constructor n’existe plus dans l’objet prototype  il faut donc recréer cette propriété en y affectant le bon constructeur à savoir Rectangle. (Je n’entrerais pas dans les détails de l’utilisation de cette propriété constructor , ce n’est pas utile pour la suite).

Nous créons ainsi la chaîne des prototypes, pour ceux qui aurait du mal à comprendre voici un schéma qui explique le fonctionnement :

 

Chaine des prototypes en JavaScript

 

Lorsque l’on créer une instance de Rectangle via l’opérateur new et que nous faisons appel à la fonction setWidth par exemple, JavaScript recherche cette fonction dans l’objet que nous venons de créer, puis dans le prototype de Rectangle, puis dans le prototype de Shape. Notre instance hérite donc bien des méthodes de Shape.

Notre exemple donnerait ceci en ECMAScript 6 :

Cette syntaxe est beaucoup plus simple, mais elle masque le véritable fonctionnement de l’héritage.

Après cette longue introduction, attaquons-nous au problème de l’héritage multiple.

L’héritage multiple

En JavaScript, l’héritage multiple n’existe pas, quand on voit le schéma précédent on comprend rapidement pourquoi : l’objet prototype, d’un constructeur, possède une seule propriété __proto__ qui pointe vers un unique prototype.

Il n’est donc pas possible d’avoir un “lien” vers plusieurs classes. Bien entendu, une solution existe pour ce souci : les mixins.

Mix… quoi?

Demandons à notre cher Wikipédia la définition d’un mixin :

En programmation orientée objet, un mixin ou une classe mixin est une classe destinée à être composée par héritage multiple avec une autre classe pour lui apporter des fonctionnalités. C’est un cas de réutilisation d’implémentation. Chaque mixin représente un service qu’il est possible de greffer aux classes héritières.

Concrètement, un mixin est un objet ou une classe, possédant des propriétés et/ou des méthodes, permettant d’enrichir une autre classe par le principe de composition.

Voyons un exemple, imaginons que l’on ait une classe Shape (comme vu précédemment) et une autre classe Drawable. Nous souhaitons maintenant avoir une nouvelle classe DrawableShape, pour cela nous allons utiliser un mixin :

Oui c’est tout simple ! On a simplement un objet avec une méthode draw. Il ne reste plus qu’à créer notre classe DrawableShape qui hérite de Shape et qui sera enrichi via notre mixin :

Nous utilisons la fonction Object.assign qui permet simplement de copier notre mixin dans l’objet prototype de notre nouvelle classe. Comme nous pouvons le voir dans l’exemple ci-dessous cela fonctionne parfaitement :

Contrairement à une classe, un mixin n’est pas destiné à être utilisé seul, mais avec une classe pour laquelle il étend ses méthodes.

Malheureusement, avec cette façon de faire, la chaîne des prototypes n’est pas respectée. On copie directement les propriétés et méthodes du mixin dans l’objet prototype qui est donc directement modifié.

Utilisation des classes ECMAScript 6

Voyons dès à présent une autre façon d’écrire un mixin tout en préservant la chaine des prototypes à l’aide des classes d’ECMAScript 6. L’idée derrière est d’écrire nos mixins non plus comme de simples objets, mais comme une fonction permettant de créer une classe (une factory). Voyons cela en reprenant notre mixin Drawable :

Notre fonction prend en paramètre une classe superclass et renvoie une nouvelle classe qui hérite de celle-ci.

On peut donc réécrire notre classe DrawableShape comme ceci :

C’est tout de même plus joli 🙂 .Il est également possible d’ajouter un nouveau mixin comme ceci :

L’idée est d’imbriquer les mixins entre eux, un peu comme des poupées russes et ainsi créer la chaîne des prototypes :

 

Chaine des prototypes en JavaScript

 

Par contre si l’on souhaite utiliser plusieurs mixins, cela peut rapidement devenir illisible. On va donc améliorer tout ça en utilisant une fonction qui permettra d’appliquer une liste de mixins sur une classe (non obligatoire) fournit en paramètre :

Cette fonction renvoi donc un objet avec une fonction with permettant de construire la chaîne des prototypes comme précédemment.

Réécrivons donc notre classe MoveableDrawableShape :

Comme on peut le voir, c’est beaucoup plus lisible.

Pour allez plus loin

On a vu dans cet article comment fonctionnait l’héritage en JavaScript, ainsi qu’une solution permettant de répondre au souci de l’héritage multiple à l’aide des mixins. Les mixins peuvent également être utilisés pour simuler des interfaces en JavaScript (voir mon article à ce sujet)  :

Je pense qu’après la lecture de cet article vous aurez une bonne vision du fonctionnement de l’héritage en JavaScript.

Pour les personnes souhaitant approfondir le sujet sur les mixins,  je vous invite à lire l’excellent article de Justin Fagnani à ce sujet : http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/

Voici également deux librairies qui permettent l’utilisations des mixins :

Bonus :

L’héritage est malheureusement trop souvent utilisé à tort et à travers. Bien souvent la composition est une meilleure alternative à son utilisation, je vous invite donc à lire ces articles (en anglais) :

Je pense que les prochains articles porteront sur les designs pattern en JavaScript. Ce sera une série de petits articles sur un design pattern en particulier. En attendant je vous souhaite de bonnes fêtes de fin d’année !  😀

7 réflexions au sujet de « Javascript : l’héritage multiple »

  1. WOW! merci beaucoup, super article, clair et instructif! 🙂

    J’ai un petit souci, quand je fait:


    const Drawable = function(superclass) {
    return class extends superclass {
    draw() {
    console.log(this.width, this.height);
    }
    };
    };

    class Shape {
    constructor(w, h) {
    this.width = w;
    this.height = h;
    }

    setWidth(w) {
    this.width = w;
    }

    setHeight(h) {
    this.height = h;
    }
    }

    class Rectangle extends Drawable(Shape) {
    constructor(w, h) {
    super(w, h);
    }

    getArea() {
    return this.width * this.height;
    }
    }

    const r = new Rectangle(5, 10);
    console.log(r.__proto__);
    console.log(r.__proto__.__proto__);
    console.log(r.__proto__.__proto__.__proto__);
    console.log(r.__proto__.__proto__.__proto__.__proto__);

    J’obtiens en console:


    Rectangle {}
    Shape {}
    Shape {}
    {}

    Du coup ca respecte pas trop le schema 🙁
    J’aurai voulu obtenir:


    Rectangle { constructor: [Function: Rectangle], getArea: [Function] }
    Drawable { constructor: [Function: Drawable], draw: [Function] }
    Shape { setWidth: [Function], setHeight: [Function] }
    {}

    Que j’ai reussi avec la vielle method:


    const Shape = function(w, h) {
    this.width = w;
    this.height = h;
    };

    Shape.prototype.setWidth = function(w) {
    this.width = w;
    };

    Shape.prototype.setHeight = function(h) {
    this.height = h;
    };

    const drawable = function(superclass) {
    const Drawable = function(w, h) {
    superclass.call(this, w, h);
    };

    Drawable.prototype = Object.create(Shape.prototype);

    Drawable.prototype.constructor = Drawable;

    Drawable.prototype.draw = function() {
    return console.log(this.width, this.height);
    };

    return Drawable;
    }

    const Rectangle = function(w, h) {
    drawable(Shape).call(this, w, h);
    };

    Rectangle.prototype = Object.create(drawable(Shape).prototype);

    Rectangle.prototype.constructor = Rectangle;

    Rectangle.prototype.getArea = function() {
    return this.width * this.height;
    };

    const r = new Rectangle(5, 10);
    console.log(r.__proto__);
    console.log(r.__proto__.__proto__);
    console.log(r.__proto__.__proto__.__proto__);
    console.log(r.__proto__.__proto__.__proto__.__proto__);

    Comment je peux obtenir le meme resultat via es6?
    Pourquoi en utilisant le mot clef class il me sort juste Rectangle {} par exemple sans les methodes de Rectangle?

    Merci beaucoup beaucoup 🙂

    1. Tu as essayé sur quel navigateur ? Car le code ES6 est correct, j’obtiens bien :


      { constructor: ƒ, getArea: ƒ }
      {constructor: ƒ, draw: ƒ }
      { constructor: ƒ, setWidth: ƒ, setHeight: ƒ }
      { constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ,  … }

        1. Je sais pas sur quel navigateur tu testes mais il se mouille pas { constructor: ƒ, getArea: ƒ }, il prenne pas le risque d ecrire en sorti {constructor: Rectangle, getArea: f} 😂

      1. OK 🙂 j’ai trouver mon probleme. En es6 je n’avais pas donner de nom de classe a Drawable. Du coup:

        const drawable = function(superclass) {
        return class Drawable extends superclass {
        draw() {
        console.log(this.width, this.height);
        }
        };
        };

        Ainsi j’obtiens bien

        Rectangle { }
        Drawable { }
        Shape { }
        {}

        Le probleme qu’il reste c’est qu’en mode “non es6” j’obtiens:

        Rectangle { constructor: [Function: Rectangle], getArea: [Function] }
        Drawable { constructor: [Function: Drawable], draw: [Function] }
        Shape { setWidth: [Function], setHeight: [Function] }
        {}

        La raison est plus simple que prevu, lorsqu’on cree une class es6, le descripteurs enumerable est false par default, si je veux le meme resultat il me faudra (pour Shape mais pour Rectangle et Drawable aussi):


        const Shape = function(w, h) {
        this.width = w;
        this.height = h;
        };
        Object.defineProperties(Shape.prototype, {
        setWidth: {
        value: function(w) {
        this.width = w;
        },
        enumerable: true
        },
        setHeight: {
        value: function(h) {
        this.height = h;
        },
        enumerable: true
        }
        })

        Pour obtenir:


        Rectangle { }
        Drawable { }
        Shape { }
        {}

        🙂 🙂

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.