La réutilisation et la non redondance sont un pré-requis pour le développement d’une base de code propre et scalable. Ce principe, valide en front-end ainsi qu’en backend-end, est l’objet de cet article.

Chez Deepki, nous développons la partie front-end de notre plateforme Deepki Ready à l’aide du framework VueJS. La forte croissance que la société connaît depuis quelques années nous confronte à des challenges techniques liés à la scalabilité et à l’architecture de notre base de code.

Afin de faire face à ces challenges, nous appliquons divers paradigmes de la programmation orientée objet (POO) dont la composition. Les discussions concernant ce principe sont de plus en plus fréquentes dans nos échanges et dans la relecture de code. Par conséquent, nous allons expliquer dans cet article la composition, les différentes options de composition de VueJS, de mettre en lumière leurs avantages, leurs inconvénients ainsi que l’avenir de la composition dans VueJS.

Always compose

La composition

La composition est un concept de POO qui modélise la relation entre deux classes. Elle implique la mise en collaboration de classes au sein d’une autre classe. La composition permet ainsi de réutiliser du code dans différentes parties d’une application.

Voici un example de la composition des composants en VueJS:

<!-- Dans ParentComponent.vue -->
<template>
    <ChildComponent1 />
    <ChildComponent2 />
</template>

Le ParentComponent est composé de deux composants enfants. Il ne s’agit pas d’un héritage entre objets. Le composant parent contrôle des propriétés et des events qui peuvent être transmis aux enfants. Cette composition est la base des frameworks par composants. Malheureusement, certaines contraintes nous obligent à concevoir la composition du code autrement.

Qu’est-qu’un mixin ?

Un mixin est un terme générique de la programmation orienté-objet. Il représente une classe qui possède des méthodes réutilisables dans d’autres classes. En JavaScript, les mixins sont un moyen de palier le manque d’héritage multiple. Ils peuvent être implémentés dans d’autres classes en copiant ses méthodes dans d’autres prototypes.

En VueJS, les mixins sont une manière flexible de distribuer des fonctionnalités réutilisables pour les composants Vue. Un objet mixin peut contenir n’importe quelle option de composant (methods, computed, data, etc). Lorsqu’un composant utilise un mixin, toutes les options du mixin seront imbriquées dans les options du composant.

Voici un exemple de l’utilisation des mixins en termes musicaux. Imaginons que nous avons deux composants : un composant Guitar.vue et un autre Piano.vue. Ce sont deux instruments de musique à cordes, avec beaucoup de similarités. Pourquoi donc ne pas créer un seul composant Instruments.vue ? La réponse est assez simple : imaginez un orchestre philharmonique représenté dans notre composant. Il devient tout de suite énorme, sa lecture serait très fastidieuse et par conséquent peu ou pas extensible. De plus, toute modification aurait un impact sur d’autres composants qui seraient composées d’Instruments.vue. Ainsi, on ne pourrait pas enlever ou ajouter des instruments à notre orchestre sans le faire sur les autres molécules/organismes composées d’Instruments.vue. En revanche, avec des atomes qui représentent individuellement un seul instrument, on peut créer autant de groupes musicaux autonomes que l’on veut.

Il est important de garder une approche atomique dans la composition de composants. Dans notre exemple, chaque instrument représente un atome et la composition de ces atomes dans un autre composant plus complexe serait une molécule et ces molécules pourraient créer des composants organismes. Pour plus d’informations sur l'atomic design je vous invite à lire cet article.

Composition atomique des composants VueJS

Étant donnée l’approche atomique, il semble judicieux de séparer les instruments en composants individuels. Mais dois-je écrire tous les accords dans chaque atome ? Vous avez compris, les mixins permettent d’éviter la redondance dans nos atomes :

// chordMixins.js
// notation anglaise des accords
export const playEMajorChordMixin = {
    created() {
        console.log("E, G#, B")
    }
}

export const playAMajorChordMixin = {
    created() {
        console.log("A, C#, E")
    }
}

Maintenant, je peux importer et réutiliser mes chordMixins dans n’importe quel instrument.

// Guitar.vue
<script>
    import { playEMAjorChordMixin } from '../mixins/chordMixins.js'
    export default {
        mixins: [playEMajorChordMixin, playAMajorChordMixin]
    }
</script>

Pour plus d’informations sur les mixins en VueJS, je vous invite à consulter leur documentation. Il est en effet important de connaître les spécificités dans leur comportement, comme par exemple, l’ordre d’exécution des mixins au sein d’un composant, les mixins globaux ou encore les factories de mixins.

Les limites des mixins

Les mixins peuvent facilement entrer en conflit avec les options d’un composant. Il faut être vigilant quand on nomme nos mixins. Un nom trop générique peut rapidement être en conflit avec une méthode ou une computed d’un composant et provoquer de comportements inattendus. Plus on utilise des mixins dans un composant, plus on risque d’avoir des comportements inattendus.

Les mixins doivent respecter une structure bien définie dans l’arborescence d’un projet. Dans le cas contraire, l’accès à leur logique et donc la compréhension du fonctionnement d’un composant devient compliquée. De manière générale, la lecture des composants utilisant beaucoup de mixins est fastidieuse car les propriétés, les données ou les méthodes du composant sont définies dans les mixins.

Le contrat entre un composant et ses mixins est implicite. Les mixins s’appuient sur la définition de certaines méthodes sur le composant, mais il n’y a aucun moyen de le voir à partir de la définition du composant.

Les mixins compliquent la lecture et par conséquent les refactorisations du code. Si vous définissez la méthode shouldComponentUpdate dans vos composants, vous pourriez avoir des problèmes si certains mixins ont besoin de leurs propres implémentations de shouldComponentUpdate pour être pris en compte.

Les mixins sont de moins en moins maintenus dans les frameworks JavaScript. Dans React 0.13 les mixins ne sont plus supportés pour les classes ES6.

Vous trouverez plus d’informations sur les limites des mixins dans cet article écrit par Dam Abramov, l’un des ingénieurs à l’origine de React.

L’héritage avec extends

Cette option permet à un composant d’en étendre un autre, en héritant de ses options de composant. Du point de vue de l’implémentation, extend est similaire à mixins. Le composant spécifié par extend sera traité comme s’il s’agissait du premier mixin. Cependant, les extends et les mixins expriment des intentions différentes. L’option mixins est principalement utilisée pour composer des morceaux de fonctionnalités, tandis que extend concerne principalement l’héritage.

const PianoComponent = {
    playMixolidianScale () {
        return ["G, A, B, C, D, E, F, G"]
    },
    playIonianScale () {
        return ["G, A, B, C, D, E, F#, G"]
    },
}

const JazzTrioComponent = {
    extends: PianoComponent

    playSongOnMixolidianMode () {
        return this.PianoComponent.playMixolidianScale()
    },
}

L’héritage des méthodes d’un composant dans un autre composant se traduit par un couplage fort entre eux. Cela peut engendrer des contraintes de scalabilité du composant. Dans l’exemple ci-dessus, le JazzTrioComponent est fortement dépendant de PianoComponent, ainsi toute modification des fonctions de PianoComponent peuvent engendrer des changements dans le comportement du trio. Toutes les méthodes d’extension des composants peuvent ajouter de la complexité et de la verbosité au composant. Avant de décider d’implémenter extends dans un composant, il est judicieux de vérifier si d’autres design patterns répondent de façon plus appropriée au besoin.

L’injection de dépendance : Provide / inject

Ces options sont utilisées ensemble pour permettre à un composant ancêtre de servir d’injecteur de dépendance pour tous ses descendants, quelle que soit la profondeur de la hiérarchie des composants, tant qu’ils se trouvent dans la même chaîne parent.

L’option provide doit être un objet ou une fonction qui renvoie un objet. Cet objet contient les propriétés disponibles pour l’injection dans ses descendants. L’option inject peut être un array de string ou un objet.

 // parent component providing "standardTuning"
const GuitarComponent = {
    provide: {
        standardTuning: ["E", "A", "D",  "G", "B", "E"]
    }
}

// child component injecting "standardTuning"
const BassComponent = {
    inject: ["standardTuning"],

    tuneFourStringsBase() {
        return this.standardTuning.slice(0, 4)
    },
    tuneSixStringsBase() {
        return this.standardTuning
    },
}

Néanmoins, cela ne fonctionnera pas si nous essayons de faire un provide sur une propriété d’instance de composant. Pour accéder aux propriétés d’instance du composant, nous devons convertir provide en une fonction renvoyant un objet.


app.component("GuitarComponent", {
    data() {
        return {
            standardTuning: ["E", "A", "D",  "G", "B", "E"]
        }
    },

    // cela ne marchera pas
    provide() {
        standardTuningLength: this.standardTuning.length
    },

    // pour renvoyer des propriétés d'instance de composant, 
    // il faut une fonction qui les renvoie
    provide() {
        return {
            standardTuningLength: this.standardTuning.length
        }
    },

Cette nouvelle option a le même problème de lecture et de compréhension de code des mixins. Il est en effet pratique de pouvoir injecter un provider à tout endroit de la hiérarchie des composants. En revanche, lorsque que le nombre de composants devient trop important, la compréhension du code et les opérations de debugging seront très fastidieuses.

Finalement, l’utilisation de provide/inject peut être en contradiction avec le pattern d’atomic design. Lorsqu’on provide de la logique d’un parent vers ses enfants, on est en train d’ajouter implicitement de la complexité aux enfants. Souvenez-vous, les atomes doivent être le plus simple possible et ils ne doivent pas contenir l’intelligence des molécules.

Conclusion

Comme on l’a vu précédemment, la création de composants VueJS ainsi que des options de composition (mixins, provide/inject, extends…) nous permet d’extraire des parties répétables dans des morceaux de code réutilisables. Cela seul peut amener notre application assez loin en termes de maintenabilité et de flexibilité. Cependant, notre expérience collective a prouvé que cela n’est pas suffisant, surtout lorsque votre application devient vraiment volumineuse. Dans ce cas, le partage et la réutilisation du code deviennent particulièrement importants.

Pour finir, voici quelques conclusions. Tout d’abord, toute application scalable et facilement maintenable doit respecter une approche atomique dans le “design” des composants. Pensez à des composants les plus petits possibles. Ces composants vous permettrons par la suite de créer les objets complexes dont vous avez besoin dans votre projet. Ensuite, pensez à utiliser les différentes options de composition que les frameworks mettent à disposition. Malgré les limites qu’on a pu décrire précédemment, n’hésitez pas à utiliser les mixins ou les autres options (héritage, injection de dépendance). Il faut éviter la redondance et ces options vous permettent de réutiliser le code au lieu de l’écrire plusieurs fois à différents endroits. Enfin, les options de composition ne suffiront pas à répondre à votre besoin lorsque votre projet atteindra une taille importante.

Nous n’approfondirons pas la composition API maintenant car elle fera l’objet d’un autre article. Néanmoins, évoquer cette API nous permet d’anticiper la suite de la composition dans les frameworks front-end les plus utilisés tels que VueJS ou React.