IX. Modularité▲
Ce chapitre concerne les méthodes d'organisation des programmes. Pour ceux dont la taille est modeste, la question de l'organisation est rarement un problème. Mais quand un programme grandit, il peut atteindre une taille imposante qui rend difficile le contrôle de sa structure et de son interprétation. Un tel programme commence assez facilement à ressembler à un plat de spaghettis, une masse informe dans laquelle tout semble relié à tout le reste.
Lorsque nous structurons un programme, nous faisons deux choses. Nous le divisons en plus petites parties appelées modules, chacune ayant un rôle spécifique, et nous spécifions les relations entre ces parties.
Dans le chapitre 8, en développant un terrarium, nous avons utilisé un grand nombre de fonctions décrites dans le chapitre 6. Ce chapitre définissait également quelques nouveaux concepts qui n'avaient rien de spécifique aux terrariums, comme les types clone et Dictionary. Toutes ces choses ont été ajoutées à l'environnement sans être organisées. Une façon de découper ce programme en modules pourrait être :
- pour commencer, un module FunctionalTools qui inclut les fonctions du chapitre 6 et n'a pas de dépendance ;
- ensuite, ObjectTools, contenant des choses comme clone et create, qui dépend de FunctionalTools ;
- Dictionary qui contient le type dictionnaire et dépend de FunctionalTools ;
- enfin, le module Terrarium qui dépend de ObjectTools et Dictionary.
Quand un module dépend d'un autre module, il utilise des fonctions ou des variables de ce module et fonctionne uniquement si le premier module est chargé.
C'est une bonne idée de s'assurer que les dépendances ne forment jamais une boucle. Non seulement les dépendances circulaires créent un problème technique (si les modules A et B dépendent l'un de l'autre, lequel doit être chargé en premier ?) mais elles rendent aussi les relations entre les modules moins évidentes et peuvent aboutir à une version modulaire de type spaghetti dont je parlais plus tôt.
La plupart des langages de programmation modernes ont un système de modules intégrés. Ce n'est pas le cas de JavaScript. Une fois encore, il nous faut inventer quelque chose nous-mêmes. Le plus évident pour commencer est de mettre chaque module dans un fichier différent. Cela permet de voir précisément à quel module appartient le code.
Les navigateurs chargent des fichiers JavaScript quand ils rencontrent une balise <script> avec un attribut src dans le HTML de la page Web. L'extension .js est utilisée habituellement pour les fichiers contenant du code JavaScript. Dans la console, on fournit un raccourci pour charger des fichiers grâce à la fonction load.
load
(
"FunctionalTools.js"
);
Dans certains cas, lancer des commandes dans le mauvais ordre provoquera des erreurs. Si un module essaie de créer un objet Dictionary alors que le module Dictionary n'a pas encore été chargé, il sera incapable de trouver le constructeur et échouera.
On pourrait croire que ça se règle facilement. On ajoute simplement quelques appels à load en haut du fichier d'un module pour charger tous les modules dont il dépend. Malheureusement, compte tenu du fonctionnement des navigateurs, appeler load ne provoque pas immédiatement le chargement d'un fichier donné. Le fichier sera chargé après l'exécution complète du fichier courant. C'est généralement trop tard.
Dans la plupart des cas, la solution pragmatique consiste à gérer les dépendances à la main : placer les balises script de vos documents HTML dans le bon ordre.
Il existe deux moyens d'automatiser (partiellement) la gestion des dépendances. Le premier consiste à conserver dans un fichier distinct les informations concernant les dépendances entre les modules. Ce fichier peut être chargé en premier et utilisé pour déterminer dans quel ordre charger les autres. Le second moyen consiste à ne pas utiliser de balise script (load crée et ajoute une telle balise par un mécanisme interne) mais à aller chercher le contenu du fichier directement (voir le chapitre 14) puis à utiliser la fonction eval afin de l'exécuter. Le chargement des scripts est alors instantané et donc plus facile à gérer.
eval, abrégé pour « evaluate » ou « évaluer », est une fonction intéressante. Si vous lui attribuez une valeur de chaîne (string), elle exécutera le contenu de la chaîne en tant que code JavaScript.
eval(
"print(
\"
je suis une chaîne à l'intérieur d'une chaîne !
\"
);"
);
Comme vous pouvez l'imaginer facilement, eval peut servir à faire des choses intéressantes. Du code peut créer du code et l'exécuter. La plupart du temps, cependant, les problèmes qui peuvent être résolus en utilisant astucieusement eval peuvent aussi l'être avec un usage astucieux de fonctions anonymes, lesquelles ont moins de chances de causer des problèmes bizarres.
Quand eval est appelé à l'intérieur d'une fonction, toutes les nouvelles variables deviennent des variables locales de cette fonction. Ainsi, quand une variation du load utilise eval en interne, le chargement du module Dictionary crée un constructeur Dictionary à l'intérieur de la fonction load, qui sera perdu dès que la fonction se termine. Il existe des contournements pour éviter ce problème mais ils sont plutôt mal fichus.
Survolons rapidement la première variante de gestion de dépendances. Elle nécessite un fichier spécifique pour les informations des dépendances, qui peut ressembler à ceci :
var dependances =
{
"ObjectTools.js"
:
[
"FunctionalTools.js"
],
"Dictionary.js"
:
[
"ObjectTools.js"
],
"TestModule.js"
:
[
"FunctionalTools.js"
,
"Dictionary.js"
]};
L'objet dependances contient une propriété pour chaque fichier qui dépend d'autres fichiers. Les valeurs des propriétés sont des tableaux de noms de fichier. Notez que nous ne pourrions pas utiliser ici un objet Dictionary, parce que nous ne pouvons pas être sûrs que le module Dictionary a déjà été chargé. Comme toutes les propriétés dans cet objet finiront en « .js », il y a peu de risques qu'elles interfèrent avec des propriétés cachées telles que __proto__ ou hasOwnProperty et un objet normal fonctionnera très bien.
Le gestionnaire de dépendances doit faire deux choses. D'abord il doit s'assurer que les fichiers sont chargés dans le bon ordre, en chargeant le fichier de dépendances avant les fichiers eux-mêmes. Et ensuite il doit vérifier qu'aucun fichier n'est chargé plusieurs fois, ce qui pourrait causer des problèmes et une sérieuse perte de temps.
var fichiersCharges =
{};
function require
(
fichier) {
if (
dependances[
fichier]
) {
var fichiers =
dependances[
fichier];
for (
var i =
0
;
i <
fichiers.
length;
i++
)
require
(
fichiers[
i]
);
}
if (!
fichiersCharges[
fichier]
) {
fichiersCharges[
fichier]
=
true;
load
(
fichier);
}
}
La fonction require peut maintenant être utilisée pour charger un fichier et toutes ses dépendances. Notez qu'il s'appelle lui-même de façon récursive pour gérer une dépendance (et les dépendances possibles de cette dépendance).
require
(
"TestModule.js"
);
test
(
);
Créer un programme sous la forme d'un jeu de petits modules bien conçus, cela implique souvent que ce programme va utiliser beaucoup de fichiers différents. Quand on programme pour le Web, avoir un tas de petits fichiers JavaScript sur une page tend à allonger son temps de chargement. Mais cela ne doit pas être un problème. Vous pouvez écrire et tester votre programme sous forme d'une série de petits fichiers, puis les réunir dans un seul et unique fichier plus gros au moment de « publier » le programme sur le Web.
Exactement comme un type d'objet, un module a une interface. Dans de simples modules consistant uniquement en une collection de fonctions, telle que FunctionalTools, l'interface est généralement constituée de toutes les fonctions qui sont définies dans le module. Dans d'autres cas, l'interface du module n'est qu'une petite partie des fonctions définies à l'intérieur. Par exemple, notre système de manuscrit-vers-HTML dans le chapitre 6 n'a besoin d'interface que pour une seule fonction, renduFichier (le sous-système pour créer le HTML serait un module distinct).
Pour les modules qui ne définissent qu'un seul type d'objet, comme Dictionary, l'interface de l'objet est identique à l'interface du module.
En JavaScript, les variables globales existent toutes ensemble en un seul endroit. Dans les navigateurs, cet endroit est un objet que l'on peut trouver sous le nom de window. Ce nom est un peu étrange, environment ou top auraient été de meilleurs choix mais puisque les navigateurs associent l'environnement JavaScript à une fenêtre (ou un cadre), quelqu'un a dû décider que window était un nom logique.
show
(
window
);
show
(
window
.
print
==
print
);
show
(
window
.
window
.
window
.
window
.
window
);
Comme on le voit dans la troisième ligne, le nom window est juste une propriété de cet objet d'environnement et il pointe vers lui-même.
Si un volume de code important est chargé dans un environnement, il utilisera beaucoup de noms de variables globales. Une fois que la masse de code devient trop importante pour être supervisée dans tous ses détails, il est très facile d'employer accidentellement un nom qui a déjà été utilisé pour autre chose. Ce qui cassera le code qui utilisait la valeur d'origine. La prolifération de variables globales est appelée pollution d'espace de noms et elle peut causer de sérieux problèmes en JavaScript -- le langage ne vous avertira pas si vous redéfinissez une variable déjà existante.
Il n'existe pas de moyen de se débarrasser entièrement de ce problème mais il peut être en grande partie résolu si l'on prend soin de provoquer le moins de pollution possible. Pour commencer, les modules ne devraient pas utiliser de variables globales pour des valeurs qui ne font pas partie de leur interface externe.
Si vous ne pouvez définir aucune fonction interne ni variable dans vos modules, ce n'est évidemment pas très pratique. Heureusement il existe une astuce pour contourner le problème. Nous écrivons tout le code du module à l'intérieur d'une fonction, et ajoutons finalement à l'objet window les variables qui font partie de son interface. Comme elles ont été créées dans la même fonction parente, toutes les fonctions du module peuvent se voir mutuellement mais le code extérieur au module ne le peut pas.
function moduleConstruitLeNomDuMois
(
) {
var noms =
[
"janvier"
,
"février"
,
"mars"
,
"avril"
,
"mai"
,
"juin"
,
"juillet"
,
"août"
,
"septembre"
,
"octobre"
,
"novembre"
,
"décembre"
];
function getNomDuMois
(
numero) {
return noms[
numero];
}
function getNumeroDuMois
(
nom) {
for (
var numero =
0
;
numero <
noms.
length;
numero++
) {
if (
noms[
numero]
==
nom)
return numero;
}
}
window
.
getNomDuMois =
getNomDuMois;
window
.
getNumeroDuMois =
getNumeroDuMois;
}
moduleConstruitLeNomDuMois
(
);
show
(
getNomDuMois
(
11
));
Ce programme crée un module très simple qui traduit les noms de mois en leur valeur numérique (comme on le fait avec Date, où Janvier est 0). Mais remarquez que moduleConstruitLeNomDuMois est encore une variable globale qui ne fait pas partie de l'interface du module. Par ailleurs nous devons répéter trois fois les noms de fonctions de l'interface. Pas génial.
On peut résoudre le premier problème en rendant la fonction du module anonyme et en l'appelant directement. Pour cela, nous devons ajouter une paire de parenthèses autour de la valeur de la fonction, sinon JavaScript va estimer que c'est une définition de fonction normale qui ne peut pas être appelée directement.
Le deuxième problème peut être réglé avec une fonction auxiliaire, provide, à laquelle on peut attribuer un objet qui contient les valeurs devant être exportées dans l'objet window.
function provide
(
valeurs) {
forEachIn
(
valeurs,
function(
nom,
valeur) {
window
[
nom]
=
valeur;
}
);
}
Grâce à cela, nous pouvons écrire un module comme celui-ci :
(
function(
) {
var noms =
[
"Lundi"
,
"Mardi"
,
"Mercredi"
,
"Jeudi"
,
"Vendredi"
,
"Samedi"
,
"Dimanche"
];
provide
({
getNomDuJour
:
function(
numero) {
return noms[
numero];
},
getNumeroDuJour
:
function(
nom) {
for (
var numero =
0
;
numero <
noms.
length;
numero++
) {
if (
noms[
numero]
==
nom)
return numero;
}
}
}
);
}
)(
);
show
(
getNumeroDuJour
(
"Mercredi"
));
Je ne conseille pas d'écrire des modules comme celui-ci dès le début. Pendant que vous êtes encore en train d'écrire du code, il est plus facile d'adopter la méthode plus simple que nous avons utilisée jusqu'à présent et de tout mettre au niveau supérieur. En faisant ainsi, vous pourrez vérifier les valeurs internes du module dans votre navigateur et les tester. Une fois qu'un module est plus ou moins terminé, il n'est pas très difficile de l'insérer dans une fonction.
Il existe des cas dans lesquels un module exportera tellement de variables que c'est une mauvaise idée de toutes les mettre dans un environnement de haut niveau en les rendant globales. Dans de tels cas, vous pouvez faire ce que fait l'objet standard Math et représenter le module en tant que simple objet dont les propriétés sont les fonctions et les valeurs qu'il exporte. Par exemple...
var HTML =
{
tag
:
function(
nom,
contenu,
proprietes) {
return {
name
:
nom,
properties
:
proprietes,
content
:
contenu};
},
lien
:
function(
cible,
texte) {
return HTML.tag
(
"a"
,
[
texte],
{
href
:
cible}
);
}
/* ... beaucoup d'autres fonctions produisant du HTML ... */
};
Lorsque vous avez besoin du contenu d'un tel module si souvent que cela devient pénible de devoir taper constamment du HTML, vous pouvez toujours le déplacer dans un environnement de plus haut niveau en utilisant provide.
provide
(
HTML);
show
(
lien
(
"http://download.oracle.com/docs/cd/E19957-01/816-6408-10/object.htm"
,
"Voilà comment fonctionnent les objets."
));
Vous pouvez même combiner les approches par fonction et par objet, en mettant les variables internes du module dans une fonction et en faisant en sorte que cette fonction retourne un objet qui contient son interface externe.
Quand on ajoute des méthodes à des prototypes standards comme ceux des Array et Object, un problème similaire à celui de la pollution des espaces de noms apparaît. Si deux modules décident d'ajouter une méthode map à Array.prototype, vous pourriez avoir un problème. Si ces deux versions de map ont exactement le même effet, les choses vont continuer à marcher mais seulement si vous avez de la chance.
Concevoir une interface pour un module ou un type d'objet est l'un des aspects les plus subtils de la programmation. D'un côté, vous ne voulez pas exposer trop de détails. Ils représenteraient une gêne lors de l'utilisation du module. D'un autre côté, vous ne voulez pas être trop simple et général, car cela pourrait rendre impossible l'utilisation du module dans des situations complexes ou spécialisées.
Parfois la solution consiste à fournir deux interfaces, l'une détaillée et de bas niveau pour les choses complexes, l'autre de haut niveau pour les cas les plus simples. Cette dernière peut habituellement être construite sans peine en utilisant les outils élaborés par la première.
Dans d'autres cas, il vous suffit de chercher un peu pour trouver la bonne idée sur laquelle vous allez bâtir votre interface. Comparez cela aux diverses approches de procédures héritées que nous avons vues dans le chapitre 8. En choisissant les prototypes comme le concept de base plutôt que les constructeurs, nous nous sommes arrangés pour rendre les choses bien plus faciles.
Le meilleur moyen de comprendre l'intérêt d'une bonne interface, c'est, malheureusement, d'utiliser de mauvaises interfaces. Lorsque vous en aurez marre de les subir, vous trouverez un moyen de les améliorer et vous apprendrez beaucoup en le faisant. Évitez de prétendre qu'une interface minable est « comme ça et puis c'est tout ». Réparez-la ou bien incluez-la dans une nouvelle interface meilleure (vous en trouverez un exemple dans le chapitre 12).
Il existe des fonctions qui réclament beaucoup d'arguments. Parfois cela veut simplement dire qu'elles sont mal conçues et on peut facilement y remédier en les scindant en plusieurs fonctions plus modestes. Mais dans d'autres cas, il n'y a pas de contournement possible. En particulier si ces arguments ont une valeur « par défaut » significative. Nous pourrions par exemple écrire encore une version étendue de serie.
function serie
(
debut,
fin,
pas,
longueur) {
if (
pas ==
undefined)
pas =
1
;
if (
fin ==
undefined)
fin =
debut +
pas * (
longueur -
1
);
var resultat =
[];
for (;
debut <=
fin;
debut +=
pas)
resultat.push
(
debut);
return resultat;
}
show
(
serie
(
0
,
undefined,
4
,
5
));
Il peut être difficile de se rappeler quel argument va à quel endroit, sans compter l'embêtement d'avoir à passer undefined comme second argument quand un argument longueur est utilisé. Nous pouvons rendre plus facile le passage d'arguments dans cette fonction en les incluant dans un objet.
function defaultTo
(
objet,
valeurs) {
forEachIn
(
valeurs,
function(
nom,
valeur) {
if (!
objet.hasOwnProperty
(
nom))
objet[
nom]
=
valeur;
}
);
}
function serie
(
args) {
defaultTo
(
args,
{
debut
:
0
,
pas
:
1
}
);
if (
args.
fin ==
undefined)
args.
fin =
args.
debut +
args.
pas * (
args.
longueur -
1
);
var resultat =
[];
for (;
args.
debut <=
args.
fin;
args.
debut +=
args.
pas)
resultat.push
(
args.
debut);
return resultat;
}
show
(
serie
({
pas
:
4
,
longueur
:
5
}
));
La fonction defaultTo est utile pour ajouter des valeurs par défaut à un objet. Elle copie les propriétés du deuxième argument dans le premier, en ignorant celles qui ont déjà une valeur.
Un module ou groupe de modules qui peut être utile dans plus d'un seul programme s'appelle généralement une bibliothèque. Pour de nombreux langages de programmation, un vaste choix de bibliothèques de qualité est disponible. Cela signifie que les programmeurs n'ont pas à tout recommencer depuis zéro à chaque fois, ce qui les rendrait moins productifs. Pour le JavaScript, malheureusement, le volume de bibliothèques disponibles n'est pas très important.
Cependant les choses s'améliorent depuis peu. Il existe un certain nombre de bonnes bibliothèques avec des outils « de base », des choses comme map et clone. D'autres langages ont tendance à fournir de telles choses, dont l'utilité est évidente, en tant que fonctionnalités standards intégrées au langage, mais pour le JavaScript vous devrez soit vous en créer une collection vous-même soit utiliser une bibliothèque. Il est recommandé d'utiliser une bibliothèque : c'est moins de travail et le code d'une bibliothèque a généralement été testé plus rigoureusement que ce que vous auriez écrit vous-même.
Pour s'occuper de ces outils de base, on trouve entre autres des bibliothèques « légères » : prototype, mootools, jQuery, et MochiKit. Il existe aussi de plus gros frameworks disponibles, qui font bien plus que de fournir des outils de base. YUI (par Yahoo), et Dojo semblent être les plus populaires dans cette catégorie. On peut les télécharger et les utiliser gratuitement. Mon favori est MochiKit mais c'est une question de goût personnel. Quand vous vous lancez sérieusement dans la programmation en JavaScript, c'est une bonne idée de jeter un coup d'œil sur la documentation de chacun d'eux, pour avoir une idée générale de la façon dont ils fonctionnent et de ce qu'ils permettent de faire.
Le fait qu'une boîte à outils de base soit presque indispensable pour faire des programmes un peu élaborés et qu'il en existe, par ailleurs, tellement de différentes suscitent un dilemme chez ceux qui écrivent des bibliothèques. Soit vous devez écrire une bibliothèque qui dépend d'une des boîtes à outils, soit vous écrivez vous-même les outils de base et les incluez dans une bibliothèque. La première option rend la bibliothèque difficile à utiliser pour ceux qui utilisent une boîte à outils différente. Et la seconde ajoute un bon paquet de code pas indispensable à la bibliothèque. Ce dilemme pourrait bien être une des raisons pour lesquelles il existe assez peu de bibliothèques JavaScript de bonne qualité et dont l'utilisation est répandue. Il est possible qu'à l'avenir de nouvelles versions d'ECMAScript et des modifications dans les navigateurs rendent les boîtes à outils moins nécessaires, ce qui résoudrait partiellement ce problème.