Module pattern en JavaScript

Dans ce cours vous allez découvrir la mise en oeuvre le pattern 'module' en JavaScript qui est une bonne pratique pour structurer son code.

Aujourd'hui, le JavaScript est devenu incontournable surtout pour les développeurs web. Cependant, il reste encore trop méconnu et mal utilisé par la plupart des programmeurs, qui ne prennent pas vraiment le temps de l'étudier. Nombreux sont les projets web où le JavaScript est codé "comme on peut" dans un coin de la page HTML voire directement dans les éléments HTML.

Ce design pattern, que je vais vous présenter, vous permettra d'avoir une approche différente sur la façon de structurer le JavaScript dans vos projets afin d'améliorer l'évolutivité et la maintenabilité de votre code.

Bonne lecture ! :)

Il est recommandé d'être à l'aise avec JavaScript (et d'avoir de bonnes notions objets) afin d'aborder sereinement ce tutoriel.

Rappels

Je vous fais un petit récapitulatif des éléments que vous devez connaitre pour continuer la lecture.

Portée des variables

Les variables déclarées avec le mot-clé var dans une fonction, ne sont pas accessibles en dehors de celle-ci.

1
2
3
4
5
6
function maFonction(){
    var a = 10;

    //a est accessible ICI
}
//a n'est pas accessible ICI

Il est vivement conseillé de toujours utiliser le mot-clé var, si vous ne le faites pas la variable devient globale !

1
2
3
4
5
6
function maFonction(){
    a = 10; //ce qu'il ne faut PAS FAIRE !
}

maFonction();
console.log(a); // affichera 10 !! a est global

Les variables globales sont à éviter. D'une part, elles posent des problèmes de sécurité car rien ne vous garantit qu'elles ne seront pas écrasées ou modifiées par un autre script. Et d'autre part pour des raisons de performances, plus le scope (espace de définition) est loin plus le temps d'accès est long.

Fonctions dans des fonctions

En JavaScript les fonctions sont considérées comme des objets Function. Le langage supportant les closures on peut déclarer des fonctions dans des fonctions comme ceci :

1
2
3
4
5
6
7
8
9
function maFonction(){
    //création d'une fonction à portée privée
    function test(){
        console.log('Je suis une fonction contenue dans maFonction');
    }

    b();
}
maFonction();

Dans cet exemple il est important de noter que la fonction test() n'est pas accessible en dehors de maFonction(). Elle est encapsulée dans l'objet maFonction.

Fonctions anonymes

JavaScript est un langage objet et donc les variables peuvent pointer sur des objets. Nous avons vu qu'une fonction était un objet, donc il est possible de faire ceci :

1
2
3
4
5
var bonjour = function(){
    console.log("Je suis une fonction anonyme");
};

bonjour(); //affichera 'Je suis une fonction anonyme' dans la console

Ce code montre la création d'une variable bonjour qui se voit affecter une référence vers une fonction anonyme qui est déclarée tout de suite après. L'appel de la fonction se fait avec le nom de la variable suivi de parenthèses. On peut bien évidemment passer des arguments à la fonction.

Si la variable bonjour est détruite, ou mise à undefined, et qu'il n'existe aucune autre variable détenant la référence vers la fonction, celle-ci sera nettoyée par le garbage collector. Vous noterez que la fonction se termine par un ; car c'est une affectation et non une simple déclaration.

Le pattern module

Nous y voilà ! Le pattern module est une manière d'encapsuler du code dans un package ou namespace tout en permettant si besoin, un accès extérieur à certaines propriétés/fonctions. Nous ne verrons pas dans ce tutoriel comment instancier ce module.

Sans plus attendre voilà à quoi il ressemble :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var MODULE = (function(){
    var self = {};
    var variablePrivee = 10;

    self.attributPublic = "bonjour";

    function methodePrivee() {
        console.log('Je suis encapsulée !');
    }

    self.methodePublique = function(){
        console.log('Je suis accessible !');
    };

    return self;
})();

Quelques explications s'imposent…

Premièrement, vous pouvez remarquer qu'une fonction est créée et qu'elle est passée dans une variable nommée MODULE. Rien de nouveau, nous l'avons vu plus haut.

Oui, mais… pourquoi toutes ces parenthèses autour de la fonction ?

On appelle ça une self-invoking function, ce qui peut se traduire en français par fonction qui se lance automatiquement (auto-exécution). En résumé, la fonction est appelée automatiquement au moment de sa création. Cela permet d'initialiser entièrement les attributs qu'elle contient.

À l'intérieur de notre fonction on peut voir qu'une variable self est déclarée. Cette variable est TRÈS importante, elle est la clef du problème d'encapsulation que vous avez soulevé !

Le nom 'self' est un nom que j'utilise dans mes projets, vous pouvez bien évidemment nommer cette variable comme vous le souhaitez !

En fait, self est un objet (ou ensemble) qui ne contient rien à sa déclaration. Pendant la création du module, on y met plusieurs choses : attributPublic et methodePublique. Ces deux éléments appartiennent à l'objet self.

A la fin de la création du module, on retourne self.

Mais du coup, la variable MODULE ne contient pas vraiment une fonction mais le contenu de la variable self ?!

Oui ! Enfin, pas tout à fait, elle contient la référence vers la variable self. Et c'est pourquoi on peut faire ceci :

1
2
3
4
5
var MODULE = /* code ci-dessus */

MODULE.methodePublique(); // affiche 'Je suis accessible'

console.log(MODULE.attributPublic); // affiche 'bonjour'

Tout ce qui est ajouté à self est disponible en dehors du module, c'est public. Tout ce qui n'y est pas est local au module et n'est pas accessible en dehors, c'est privé.

Mais, le reste du module est détruit alors ?

Pas du tout, comme la variable MODULE contient une référence vers self et que self appartient au module, tout le contenu du module persiste en mémoire. Rien n'est détruit par le garbage collector ! On peut donc utiliser des fonctions privées (ou publiques) dans des fonctions publiques par exemple et inversement :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var MODULE = (function(){
    var self = {};

    function methodePrivee() {
        console.log('Je suis encapsulée !');
    }

    self.methodePublique = function(){
        methodePrivee();
        self.methodePubliqueDeux()
    };

    self.methodePubliqueDeux = function(){
        console.log('Je suis publique !');
    };

    return self;
})();

MODULE.methodePublique(); // affiche 'Je suis encapsulée' puis 'Je suis publique'

Une fonctionnalité, un module

D'une manière générale, il faut éviter au maximum de mettre tout votre code dans l'espace global ou même dans un seul module. C'est comme si les maisons n'avaient qu'une seule grande pièce et que tout était mis un peu n'importe où. Découper son espace en sous-espace est une bonne pratique qu'il faut respecter et cela, quelque soit le langage.

C'est pourquoi il est conseillé de créer un module par fonctionnalité ou ensemble de fonctionnalités qui ont un lien entre elles. Voilà un exemple de ce qui peut se faire :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//PREMIER MODULE

// Gestionnaire de logs
var Logger = (function(){
    var self = {};
    var logger = new Array(); // attribut privé

    //méthode privée
    function displayLog(log){
        console.log(log.module + " : " + log.message);
    }


    //methodes publiques

    self.log = function(moduleName, msg){
        var log = {module: moduleName, message: msg};
        displayLog(log);
        logger.push(log);
    };

    self.showAll = function(){
        for(var i = 0; i < logger.length; i++)
            displayLog(logger[i]);
    };

    return self;
})();


// DEUXIEME MODULE

// Gestionnaire d'un div particulier (inscription, connexion,...)
var DivManager = (function(){
    var self = {};
    var div = undefined;

    // si l'on souhaite retarder la récupération de l'élément on peut
    // faire les opérations dans une fonction qui sera appelée dans le code.
    // C'est un choix personnel, mais vous pouvez tout à fait déclarer directement
    // la variable var div = document.getElementById(...) au dessus.
    self.init = function(){
        div = document.getElementById('monDiv');
        div.onclick = onClick;
    };

    function onClick(){
        //utilisation d'un autre module :)
        Logger.log("DivManager", "Le bloc a été cliqué");
    }


    return self;
})();


DivManager.init(); //on appelle la fonction du module pour initialiser le click

On constate qu'il y a deux modules, un qui se charge uniquement de la gestion des logs (enregistrement, affichage etc.) et un autre qui est dévoué à la gestion d'un élément HTML (dans notre cas de la récupération d'une div et de l'ajout d'un écouteur d’événement).

Sous-modules

Pour chaque fonctionnalité il est conseillé de créer un module, mais généralement le module peut grandir très vite et devenir rapidement difficile à relire pour des programmeurs externes. C'est pourquoi il est primordial d'utiliser des sous-modules quand le cas se présente.

Les sous-modules sont des sous-ensembles du module principal. L'écriture est la suivante :

1
2
3
4
5
6
7
8
var ModuleParent =  (function(){
    /* module */
})();


ModuleParent.SousModule = (function(){
    /* sous module enfant */
})();

On déclare un nouvel élément dans ModuleParent (qui est un objet ou ensemble) qui contient lui-même un module et ses propriétés. Le sous-module n'a pas accès aux fonctionnalités de l'élément parent, car les deux ont leur propre espace de noms. Si l'enfant doit communiquer avec le parent, il faut que les méthodes et attributs soient publics. Ce n'est pas de l'héritage ! C'est uniquement un découpage du code en fonctionnalités.

Un exemple parlant de communication parent/enfant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Gestionnaire d'un div particulier (inscription, connexion,...)
var DivManager = (function(){
    var self = {};
    self.div = undefined; //notez le changement, div appartient à self.

    self.init = function(){
        self.div = document.getElementById('monDiv');
        DivManager.Events.init();
    };

    return self;
})();


DivManager.Events = (function(){
    var self = {};

    self.init = function(){
        // on récupère div qui est public et on lui affecte un écouteur (click)
        DivManager.div.onclick = onClick;

        //ici je peux ajouter d'autres écouteurs
    };

    function onClick(){
        console.log("J'ai été cliqué !");
    }

    // je peux ajouter ici les fonctions spécifiques à de nouveaux écouteurs


    return self;
})();



DivManager.init(); //on appelle la fonction du module pour initialiser l'ensemble

On peut voir qu'on a externalisé le code qui gère les évènements sur la div. On évite ainsi de surcharger de fonctionnalités DivManager et le code s'en trouve plus lisible. Il est également plus facile de modifier le code, même 6 mois plus tard, car les noms sont explicites et précis.

Veillez à choisir correctement les noms des modules et des sous-modules. Ils sont essentiels pour facilement vous y retrouver. Également, prenez l'habitude de mettre une majuscule pour nommer les modules, cela vous permettra de différencier rapidement un module d'une fonction.

Attention à ne pas déclarer un sous-module AVANT un module. Le module parent (la variable) serait non défini.

Pour finir cette partie, je vous conseille de mettre chaque module (et ses sous-modules) dans des fichiers indépendants nommés par le nom du module. D'une part cela vous évitera d'avoir trop de code au même endroit et d'autre part vous verrez facilement si un nom de module existe déjà. Ceci est valable en développement, en production il peut être intéressant, selon vos besoins, de réduire le nombre de fichiers et de les compresser.

Un module parent

La multiplicité des bibliothèques, frameworks ou extensions JavaScript au sein d'un même projet peut entraîner un problème très important : l'écrasement des espaces de noms. Ceci arrivera inévitablement si votre module se nomme pareil qu'une bibliothèque que vous utilisez (ou pire encore, vous écraserez la bibliothèque). Pour éviter cette situation, il est vivement conseillé d'utiliser un module parent qui contiendra l'ensemble de vos modules. Celui-ci aura un nom unique qui pourra facilement être modifié s'il entre en conflit avec d'autres extensions.

Le fonctionnement est similaire aux sous-modules. L'idée est simplement de créer un module parent qui englobera l'ensemble des modules enfants. Le chargement de vos fichiers pouvant intervenir dans un ordre aléatoire, il faut faire attention à bien vérifier l'existence du parent avant de tenter d'y ajouter un enfant. Voilà comment procéder pour un parent se nommant Application :

1
var Application = Application || {};

Ce code signifie : Si la variable Application existe tu la récupères, sinon tu déclares un nouvel ensemble vide. Il doit être mis en haut de chaque fichier de module. Il permet de s'assurer qu'on ne va pas tenter de créer un sous-ensemble sans que le parent ne soit initialisé. De plus, il évite d'écraser Application si celui-ci existe déjà et qu'il contient d'autres modules qui ont été chargés avant.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// fichier div-manager.js

var Application = Application || {};

// on peut créer le module enfant
Application.DivManager = (function(){ 
    /* ... */ 
})();


// et des sous modules...

Application.DivManager.Events = (function(){
    /* ... */
})();

Extensions de modules

Le problème des modules c'est qu'ils sont déclarés dans un seul fichier. Dans le cas de très gros projets avec beaucoup de développeurs, il peut être intéressant de pouvoir ajouter des fonctionnalités sans modifier le fichier principal. Il existe une façon d'étendre les modules, et cela avec assez peu de changements.

Je ne parle pas de sous-modules mais bien de l'ajout de fonctionnalités dans un module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var Application = Application || {};

Application.DivManager = (function(self){

    self.nouvelleMethode = function(){
        // fonctions à ajouter au module
    };


    return self;
})(Application.DivManager || {});

On peut voir sur cet exemple que l'on passe directement en paramètre le module à étendre (ou un ensemble vide si celui-ci n'existe pas). Ce paramètre est directement utilisé par le module à la place du self que l'on déclarait au départ.

En procédant ainsi, si le module n'existe pas, il est créé !

Si vous utilisez ce modèle de conception, tous les modules doivent être déclarés de la sorte et self ne doit plus être déclaré dans le corps du module (au risque d'écraser toute extension passée).

Le module qui a été étendu par l'exemple ci-dessus doit, par conséquent, être déclaré de la sorte :

1
2
3
4
5
6
7
8
var Application = Application || {};

Application.DivManager = (function(self){

    /* toutes les fonctions du module */

    return self;
})(Application.DivManager || {});

Cette façon de procéder permet un chargement parallèle des fichiers JavaScript. Le module n'a plus besoin d'être créé en amont, il sera créé soit dans le fichier principal soit par la première extension qui tentera de l'étendre.

Enfin, on peut bien évidemment étendre les sous-modules de la même manière en faisant attention à bien vérifier l'existence de leur parent !


J'espère que vous avez pris plaisir à lire ce cours et surtout que vous avez apprécié découvrir ce modèle de conception.

Merci beaucoup à Arius pour sa relecture ainsi qu'aux bêta-lecteurs. :)

19 commentaires

Le self n'est pas un standard du Javascript, t'as sans doute appris le Python juste avant. Mais on préfère écrire this qui est commun à l'AS3 et au Java. Sans compte que dans les IDE, t'auras en prime le highlight sur ta syntaxe.

Sinon très bon tutoriel. J'ai déjà adopté ce pattern depuis quelques années. :)

+0 -0

@Yarflam: le self est surtout là pour éviter les problèmes de portée de la variable this.

Akryum

Quel intérêt ? Pour ne pas pouvoir le modifier ? On parle d'un langage client. Le hacker peut s'amuser à créer un fork de ton objet en dupliquant window puis en effectuant un deep copy depuis ton objet. Le tour est joué, c'est remplacé. :)

Exemple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var objetThis = (function () {
    this.value = "ok";
    this.test = function () {
        console.log(this.value); };
    return this;
})();

var objetSelf = (function () {
    var self = {};
    self.value = "ok";
    self.test = function () {
        console.log(self.value); };
    return self;
})();

Object.prototype.addDeepCopy = function (oldObj) {
    if(oldObj !== undefined && typeof oldObj == "object") {
        for(var property in oldObj) {
            this[property] = Object.addDeepCopy(oldObj[property]);
        }
    }
    return this;
};

var objetNewSelf = window;
objetNewSelf.addDeepCopy(objetSelf);
if(objetNewSelf == objetThis) {
    console.log("La copie est parfaite !");
} else {
    console.log("Une erreur est survenu !");
}
+0 -0

Merci pour vos retours :)

@Yarflam : Je viens du monde PHP et non du Python :) Concernant l'utilisation de self à la place de this c'est surtout car this n'est pas 'propre' à mes yeux. Il contient déjà pas mal d'objets/références (externes), et pour le debug, faire un console.log(this) est juste contre-productif. Après c'est personnel :) Pour le reste je te rejoins sur tes remarques.

@Spoke : this est propre, il faut juste savoir à quoi il correspond en fonction du contexte.

Un petit article sur lequel je suis tombé ici résume très bien le truc : http://bjorn.tipling.com/all-this

Au passage, nommer des objets avec une majuscule, là ça me paraît super dangereux (surtout pour la maintenance par un tiers) : c'est une notation pour les class en général, pas les variables. ;)

Rien n'empêche d'utiliser ce pattern avec du CommonJS. Ça n'a rien à voir.

Tu peux très bien faire :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var MonModule = (function() {
    var self = {};
    var unTrucPrive = 12;

    self.unTrucPublique = function() { return unTrucPrive; };

    return self;
})();

module.exports = MonModule;

puis

1
2
3
var MonModule = require('./MonModule');

console.log(MonModule.unTrucPublique());

ou encore en AMD

1
2
3
require('MonModule', function(MonModule) {
    console.log(MonModule.unTrucPublique());
});

AMD, CommonJS, les modules d'ECMAScript 6… sont des manières de gérer les dépendances. Ce qui est présenté dans le tuto est un pattern pour gérer l'architecture du code.

+0 -0

Bonjour !

Je crains malheuresement ne pas saisir la différence entre ce pattern et faire simplement des classes :

1
2
3
4
5
6
function App () {
    this.attrPublique = '...';
    var attrPrive = 42;
}

app = new App ();

Y aurait-il quelqu'un pour m'éclairer ? Merci !

Ok mais donc pourquoi utiliser ce pattern plutôt qu'une classe instanciable ?

Zzortell

L'intérêt c'est de pouvoir créer un package avec un objectif précis. Par exemple, tu souhaites créer une API qui met à jour les données du site dès que l'utilisateur est connecté. Tu n'as pas besoin de créer plusieurs instances ! Une seule suffit. Du coup, tu vas définir ton module (ou package) dès le début avec ses fonctionnalités. Une fois que la page sera chargée,tu appelles ton module avec la fonction init() et ça chargera les paramètres / événements respectifs.

En PHP, on utilise la propriété static Maclass { public static function init() { ... } } et on appelle la fonction avec des doubles points Maclass::init();.

Ce qui est assez cool, avec ce pattern, c'est que tu peux assez rapidement créer un injecteur de dépendance. Par exemple, à partir de ce cours, j'ai assez vite réussi à créer un système qui permet de loader les modules, mais de plus, à chaque fois que tu instancies un module, les autres modules dont il a besoin lui sont fourni par le gestionnaire de modules. De plus, ce genre de système permet de bien s'assurer qu'un module n'est instancié qu'une seule fois :). En tout cas, merci pour ce cours ;)

Bonjour à tous,

Je dépoussière ce tuto d'un peut plus d'un an pour vous demander de l'aide. En premier lieu, merci pour ce tuto très clair. Je pense avoir tous compris, cependant, je me heurte a un problème que je n'arrive pas a résoudre.

Etant pas expert en Javascript, mais cherchant à m'améliorer et surtout coder de manière plus propre, j'ai cherché un tuto pour créer une app modulaire. Donc ce tuto correspond parfaitement.

Seulement lorsque j'essaye d'étendre un module depuis un autre fichier, je n'y arrive pas.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * 
 * FileManager.js : 
**/

'use strict';
let NNM = NNM || {};

NNM.FileManager = (function(self, extentionName) {
  self.toto = 'Yo Toto';
  console.log(NNM);
  console.log(self);
  return self;
})(NNM.FileManager || {}, 'FileManager');

export default NNM;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * 
 * TMDBLink.js : 
**/

'use strict';
let NNM = NNM || {};
NNM.FileManager = (function(self, extentionName) {
  console.log(NNM);
  console.log(extentionName);
  self.titi = 'Yup TiTi';
  return self;
})(NNM.FileManager || {}, 'SelectMovieExtention');

export default NNM;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 
 * Core.js : 
**/

'use strict';
let NNM = NNM || {};

let socket = io();

// ES6 Static Legacy Import
import 'FileManager.js';


// API Dynamic Loader
socket.on('ModulesToLoad', function (data) {
  let temp = JSON.parse(data);
  temp.NameModules.forEach (function(module, index) {
    System.import(module);
  });
});

Pour résumer, j'ai un fichier Code.js qui est la pour gérer mes module (pour l'instant il ne fait que les imports), du fait que j'utilise la norme ES6 me permet de faire des imports static, mais pas encore (et peut-être jamais) les import dynamique. Pour cette raison et parce que les import ES6, ne sont pas encore gérer par les navigateurs, j'utilise un transpilleur pour convertir du ES6 en ES5 (system.js).

Comme expliqué plus haut, mon code ne permet pas d'étendre un module, il recrée un nouveau. Je pense que cela vient du fait que le module n'arrive pas au code qui doit étendre le module.

J'ai essayer dans un même fichier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
'use strict';
let NNM = NNM || {};

// Module original
NNM.FileManager = (function(self, extentionName) {
  self.toto = 'Yo Toto';
  console.log(NNM);
  console.log(self);
  return self;
})(NNM.FileManager || {}, 'FileManager');

// étend le module original
NNM.FileManager = (function(self, extentionName) {
  self.titi = 'Yup ... TiTi';
  console.log(self);
  return self;
})(NNM.FileManager || {}, 'FileManager');

Donc pour moi le problème, est bien que mon module NNM n'est pas passé en paramètre lors du chargement des modules qui vont soit créer un sous module (NNM.FileManager) soit étendre un sous module (NNM.FileManager).

J'ai cherché sur le net, et sur la doc de system.js, je n'ai pas trouvé comment lors d'un import ,passer en paramètre mon module NNM.

Dans le tuto, je n'ai pas vu non plus d'explication. Quelqu'un aurait une réponse a ma problématique ?

Merci d'avance.

+0 -0

Merci Yarflam pour ton retour, voici donc le lien du Post que j'ai créer comme tu me la conseiller: Lien vers le Post.

Sinon pour répondre à ta question

C'est quelle application qui lit ton Javascript ? Sinon je pense que tu peux ouvrir un topic sur le forum et indiquer le lien dans un prochain message ici.

Yarflam

Cette Application est destiné aux navigateurs (en excluant IE bien entendu ;), pour l'instant je me base sur Google Chrome et Google Chrome Canary pour le Développement.

Hello, Même si je consulte souvent le ZdS, c'est mon 1er post ^^ Je tiens à remercier l'auteur de se sujet, car même à mon simple niveau amateur cet article m'a enfin permis de travailler de manière lisible et organisée. Fini les patés de fonctions misent les unes en dessous des autres.Tout est bien décomposé maintenant et mal organisé on se perd facilement avec ce langage. Voilà c'est une belle trouvaille, adoptée :)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte