Javascript : Classes abstraites et interfaces

Interface et classe abstraites

Nous allons aujourd’hui nous attaquer à la notion d’interface et de classe abstraite en Javascript. Ces deux notions fondamentales en programmation orientée objet n’existent pas en Javascript, nous allons donc voir comment implémenter ces deux notions tout au long de cet article.

Une classe abstraite, c’est quoi au juste ?

Une classe abstraite en programmation orientée objet se définit comme suit (merci Wikipédia) :

Une classe abstraite est une classe dont l’implémentation n’est pas complète et qui n’est pas instanciable. Elle sert de base à d’autres classes dérivées (héritées).

Prenons un exemple pour illustrer ce concept. Imaginons que l’on ait besoin d’écrire des classes permettant de lire plusieurs types de fichiers de configuration. On pourrait par exemple avoir une classe qui s’occupe de lire un fichier XML et une autre un fichier JSON.

Ces deux classes partagent des fonctionnalités communes (vérification de l’existence et récupération d’une propriété par exemple), la seule chose qui diffère entre ces deux classes est la lecture du fichier.

Voyons comment écrire une classe abstraite fournissant ces fonctionnalités communes :

La classe AbstractConfig respecte bien la définition d’une classe abstraite, celle-ci ne peut pas être instanciée :

La propriété constructor permet de renvoyer une référence à la classe de l’instance (cf constructor). On vérifie donc que la référence n’est pas la classe AbstractConfig, si c’est le cas, on lève une exception.

L’implémentation est également incomplète :

Il incombe donc aux classes dérivées d’implémenter cette fonction :

La classe XmlConfig hérite de la classe abstraite AbstractConfig et implémente la fonction parse (j’ai omis volontairement le code, ce n’est pas ce qui est important ici).

Petit rappel, en Javascript l’héritage multiple n’existe pas, une classe ne peut hériter que d’une seule autre classe.

Comme nous l’avons vu, la création d’une classe abstraite est relativement simple, voyons maintenant les interfaces.

Et une interface c’est quoi ?

Une interface, en programmation orientée objet, peut-être définit comme suit :

Une interface définit un ensemble de méthodes sans leurs implémentations

La différence entre une classe abstraite et une interface est qu’une classe abstraite peut avoir des méthodes concrètes qui implémentent des fonctionnalités générales ou communes, elle sert donc à factoriser du code. Une interface quant à elle définit uniquement les méthodes à implémenter, elle permet de définir un contrat : chaque classe implémentant l’interface sera tenue d’implémenter les méthodes de l’interface.

Prenons l’exemple du précédent article, dans lequel nous avons une classe Mailer qui attend comme paramètre du constructeur un objet logger avec une méthode log :

Afin de garantir que notre objet logger possède bien une méthode log, celui-ci devra implémenter une interface qui définit cette méthode log.

Dans d’autres langages comme PHP par exemple, une interface se déclare comme suit :

et s’implémente de cette manière :

Mais comme je l’ai dit, malheureusement Javascript n’a pas de notion d’interface. Mais plusieurs solutions existent pour simuler cette notion, voyons l’une d’entre elles :  le duck typing.

Le duck quoi?

Derrière ce nom étrange se cache un concept tout simple, le duck typing fait référence au test du canard qui dit :

Si ça ressemble à un canard, si ça nage comme un canard et si ça cancane comme un canard, c’est qu’il s’agit sans doute d’un canard.

L’idée derrière est que plutôt de se préoccuper du type d’un objet, on se préoccupe de son comportement (de ses méthodes).

Reprenons l’exemple précédent, notre classe Mailer doit attendre un objet qui possède une méthode log au final peu importe le type de cet objet du moment qu’il possède cette méthode :

En reprenant le test du canard, on peut dire “Si le paramètre logger possède une méthode log c’est qu’il s’agit sans doute d’un logger“.

Cette technique permet donc de “simuler” la notion d’interface. On vérifie simplement l’existence des méthodes, mais notre code pose un souci : on doit à chaque fois vérifier dans le constructeur que le paramètre reçu possède bien les méthodes attendues, et ce pour chacune des classes qui utiliseront un logger. Ici, nous avons qu’une seule méthode (log), mais si l’on a plusieurs méthodes, dites bonjour à la duplication de code.

Encapsulons donc cette vérification. Dans un premier temps, créons une classe permettant de définir une interface :

Le constructeur prend en paramètre le nom de l’interface ainsi qu’un nombre indéfini de chaînes de caractères contenant les noms des méthodes de l’interface (Cf Rest parameters).

Ajoutons maintenant la fonction permettant de vérifier qu’un objet implémente bien une interface :

La méthode checkImplements prend en paramètre l’objet à vérifier ainsi qu’un nombre indéfini d’instances de la classe Interface. Elle vérifie simplement que l’objet possède bien les méthodes des interfaces qui sont passées en paramètres.

Reprenons donc notre exemple précédent et créons une interface de logger :

Vérifions maintenant dans notre classe Mailer que l’objet passait en paramètre du constructeur implémente bien notre interface Logger :

Si nous passons en paramètre un objet qui ne contient pas une méthode log, une exception sera levée avec le message suivant :

Il est également possible de vérifier qu’une classe implémente bien une interface lors de son instanciation :

Notre exemple est tout simple, notre interface Logger possède qu’une seule méthode log, mais il possible de définir plusieurs méthodes comme suit :

C’était simple non?

Les classes abstraites et interfaces ont chacune une fonction bien distincte : l’une sert à factoriser du code, tandis que l’autre à définir des contrats. Bien que le langage Javascript ne possède pas ces deux notions, nous avons vu qu’il était assez simple de les implémenter.

Les solutions ont été écrites pour Node.js, mais pourront très simplement être utilisées pour du Javascript côté front par exemple.

J’ai brièvement rappelé que l’héritage multiple n’existait pas en Javascript, nous verrons dans le prochain article une solution qui permet entre autres de remédier à ce problème (je vous laisse deviner sur quoi il portera   😉 ).


Annonces partenaire

Je suis lead developer dans une boîte spécialisée dans l'univers du streaming/gaming, et en parallèle, je m'éclate en tant que freelance. Passionné par l'écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J'adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs. Si vous avez envie de papoter, n'hésitez pas à me retrouver sur Twitter, m'envoyer un petit email ou même laisser un commentaire.

6 commentaires

  1. Simplement merci !
    Cet article répond parfaitement à la question que je me posais.
    Cela devrait me servir dans un projet JS que je suis en train de préparer.

    J’ai trouvé un projet sur npm permettant de “simuler” les interfaces de manière un peu différente :
    https://www.npmjs.com/package/interface
    Mais il est bon de comprendre comment cela fonctionne…

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

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