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 remédier à 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 :

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

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 :

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

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ée 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 :

function newOperator(constr) {
var o = Object.create(constr.prototype);
constr.apply(o, Array.prototype.slice.call(arguments, 1));
return o;
}

Lorsqu’un objet est créé, celui possède automatiquement 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:

var shape = new Shape(10, 15);
shape.__proto__
/*
setHeight : ƒ (h)
setWidth : ƒ (w)
constructor : ƒ (w, h)
__proto__ : Object
*/

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 chaîne des prototypes qui permet l’héritage en JavaScript.

L’héritage

Pour voir, en détail comment l’héritage fonctionne nous allons reprendre l’exemple de notre classe Shapeet créer une nouvelle classe Rectanglequi hérite de celle-ci :

// 1
var Rectangle = function(w, h) {
Shape.call(this, w, h); // 2
};
Rectangle.prototype = Object.create(Shape.prototype); // 3
Rectangle.prototype.constructor = Rectangle; // 4
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
  1. Nous créons un constructeur Rectangle;
  2. Nous appelons dans celui-ci, le constructeur Shape via la méthode [call](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Function/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](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/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 auraient du mal à comprendre voici un schéma qui explique le fonctionnement :

Chaine des prototypes en JavaScript
Chaîne des prototypes en JavaScript

Lorsque l’on crée 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 :

class Rectangle extends Shape {
constructor(w, h) {
super(w, h);
}
getArea() {
return this.width * this.height;
}
}

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 :

let Drawable = {
draw() {
console.log('Draw a shape with width = ' + this.width + ' and height = ' + this.height);
}
};

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 :

class DrawableShape extends Shape {
// ...
}
Object.assign(DrawableShape.prototype, Drawable);

Nous utilisons la fonction [Object.assign](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/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 :

const drawableShape = new DrawableShape(5, 10);
drawableShape.draw(); // console.log : Draw a shape with width = 5 and height = 10

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 chaîne 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 :

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

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 :

class DrawableShape extends Drawable(Shape) {
// ...
}

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

let Moveable = function(superclass) {
return class extends superclass {
move() {
console.log('Move');
}
};
};
class MoveableDrawableShape extends Moveable(Drawable(Shape)) {
// ...
}
const shape = new MoveableDrawableShape(5, 10); // Les fonctions de Moveable, Drawable et Shape sont disponibles

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 de prototype des mixins
Chaîne de prototype des mixins

Par contre si l’on souhaite utiliser plusieurs mixins, cela peut rapidement devenir illisible. Nous allons 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 :

let mix = function(superclass) {
return {
with(...mixins) {
return mixins.reduce((c, mixin) => mixin(c), superclass || class {});
}
};
};

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 :

class MoveableDrawableShape extends mix(Shape).with(Moveable, Drawable) {
// ...
}

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

Pour aller 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)  :

let DrawableInterface = function(superclass) {
return class extends superclass {
draw() {
throw new Error('You must implement this function');
}
};
};

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’utilisation 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 !