Moduli
By Sergio Spina
- 10 minutes read - 2124 wordsI moduli sono il principale strumento per la strutturazione e la organizzazione del codice; il modulo consente di applicare il principio di incapsulazione di dati e procedure che è così importante nella programmazione contemporanea, non solo in JavaScript. Per mezzo della incapsulazione non solo si raccolgono dati e codice logicamente collegati in un unica struttura utilizzabile come un unicum (ad esempio si possono raccogliere in un modulo tutte le procedure e i dati relativi all’accesso a un database, oppure tutte le procedure relative alla gestione di particolari strutture dati), ma si possono anche nascondere tutti quei dettagli implementativi (variabili, costanti, procedure) di cui è bene evitare la modificabilità all’esterno del modulo stesso.
In questo articolo vedremo innanzitutto cosa si intende per modulo in JavaScript, poi i modi differenti nei quali questo concetto viene applicato in Node e in ES.
Cosa NON È un modulo
Un modulo è una struttura dati contenente dati (anche chiamati stato del modulo) e procedure (anche chiamati metodi). I metodi devono accedere ai dati per leggerli e modificarli, e i dati devono essere inaccessibili dall’esterno del modulo. Se manca anche una sola di queste caratteristiche non siamo più di fronte ad un modulo ma ad un’altra struttura dati: il namespace o la struttura dati vera e propria.
Namespace
Se una struttura contiene solo procedure ma nessun dato cui le procedure accedano in lettura o scrittura non può essere considerata un modulo ma un namespace, una struttura molto semplice e statica, che si limita a mettere a disposizione una serie di metodi logicamente collegati ad un certo scopo. Ad esempio:
1function stringUtils() {
2 function reverse(string) {
3 return string
4 .split('')
5 .reverse()
6 .join('')
7 }
8
9 function substitute(string, char1, char2) {
10 return string
11 .split('')
12 .map(char => char === char1 ? char2 : char)
13 .join('')
14 }
15
16 function exclude(string, char) {
17 return string
18 .split('')
19 .filter(currentChar => currentChar !== char)
20 .join('')
21 }
22
23 return {
24 reverse,
25 substitute,
26 exclude
27 }
28}
29
30let utils = stringUtils()
31
32console.log(utils.reverse('ladder'))
33// ===> reddal
34console.log(utils.substitute('stopper', 't', 'l'))
35// ===> slopper
36console.log(utils.exclude('london', 'n'))
37// ===> lodo
In questo esempio stringUtils
è un semplice namespace, trattandosi di una struttura che si limita a mettere a disposizione una serie di metodi volti a modificare in vario modo le stringhe che vengono passate come parametri.
Struttura dati
Se una struttura contiene procedure e dati cui le procedure accedono, ma i dati non sono nascosti all’interno della struttura, non vale la pena di parlare di modulo, trattandosi invece di una semplice struttura dati. Vediamo un esempio:
1const animalVerses = {
2 animals: [
3 {
4 id: 1,
5 name: 'chicken',
6 verse: 'cluck'
7 },
8 {
9 id: 2,
10 name: 'zebra',
11 verse: 'neigh'
12 },
13 {
14 id: 3,
15 name: 'penguin',
16 verse: 'chirp'
17 },
18 {
19 id: 4,
20 name: 'dolphin',
21 verse: 'whistle'
22 },
23 ],
24
25 getVerse(id) {
26 let animal = this.animals.find(a => a.id === id)
27 return animal.verse
28 },
29
30 setVerse(id, verse) {
31 let animal = this.animals.find(a => a.id === id)
32 let newAnimal = {
33 ...animal,
34 verse: verse
35 }
36 this.animals = this.animals.map(a => a.id !== id ? a : newAnimal)
37 }
38}
39
40console.log(animalVerses.animals)
41// ===> ... come ci si aspetta
42console.log(animalVerses.getVerse(3))
43// ===> chirp
44animalVerses.setVerse(4, 'bark')
45console.log(animalVerses.animals)
46// ===> [ ..., { id: 4, name: 'dolphin', verse: 'bark' }, ...]
Qui animalVerses
è costituito sia da dati (l’array animals
), sia da metodi che accedono a quei dati in lettura e in scrittura; ma, come si vede dal primo console.log
in coda all’esempio, i dati non sono nascosti all’interno della struttura dati, sono accedibili dall’esterno sia in lettura che in scrittura. Quindi animalVerses
non è un modulo.
Il modulo “classico”
Dunque un modulo deve contenere dati (lo stato del modulo), i dati devono essere nascosti all’interno del modulo, e deve contenere le procedure (o metodi) per accedere a quei dati. Vediamo un esempio:
1const animalVerses = (function manageAnimals() {
2 let animals = [
3 {
4 id: 1,
5 name: 'chicken',
6 verse: 'cluck'
7 },
8 {
9 id: 2,
10 name: 'zebra',
11 verse: 'neigh'
12 },
13 {
14 id: 3,
15 name: 'penguin',
16 verse: 'chirp'
17 },
18 {
19 id: 4,
20 name: 'dolphin',
21 verse: 'whistle'
22 },
23 ];
24
25 function getVerse(id) {
26 let animal = animals.find(a => a.id === id)
27 return animal.verse
28 }
29
30 function setVerse(id, verse) {
31 let animal = animals.find(a => a.id === id)
32 let newAnimal = {
33 ...animal,
34 verse: verse
35 }
36 animals = animals.map(a => a.id !== id ? a : newAnimal)
37 }
38
39 function printAnimals() {
40 console.log(animals)
41 }
42
43 const publicInterface = {
44 getVerse,
45 setVerse,
46 printAnimals
47 }
48
49 return publicInterface
50})()
51
52console.log(animalVerses.getVerse(3))
53// ===> chirp
54animalVerses.setVerse(4, 'bark')
55animalVerses.printAnimals()
56// ===> [ ..., { id: 4, name: 'dolphin', verse: 'bark' }, ...]
57
58console.log(animalVerses.animals)
59// ===> undefined
60
61animalVerses.printAnimals()
62// ===> ... come ci si aspetta
Il modulo vero e proprio è manageAnimals
che, con la tecnica del IIFE
(Immediately Invoked Function Expression), assegna alla variabile animalVerses
l’oggetto publicInterface
. Quest’ultimo oggetto espone l’interfaccia pubblica del modulo, ossia i metodi che accedono allo stato del modulo (l’oggetto animals
) in lettura e in scrittura.
Gli ultimi due console.log
dell’esempio evidenziano il fatto che lo stato del modulo è nascosto e inaccessibile dall’esterno se si usa un sistema diretto (primo log
) mentre è leggibile per mezzo di un apposito metodo facente parte dell’interfaccia pubblica.
Ovviamente si può definire un modulo senza usare un IIFE
e utilizzare istanze del modulo definite ad hoc, ad esempio:
1function manageAnimals() {
2 let animals = [
3 {
4 id: 1,
5 name: 'chicken',
6 verse: 'cluck'
7 },
8 {
9 id: 2,
10 name: 'zebra',
11 verse: 'neigh'
12 },
13 {
14 id: 3,
15 name: 'penguin',
16 verse: 'chirp'
17 },
18 {
19 id: 4,
20 name: 'dolphin',
21 verse: 'whistle'
22 },
23 ];
24
25 function getVerse(id) {
26 let animal = animals.find(a => a.id === id)
27 return animal.verse
28 }
29
30 function setVerse(id, verse) {
31 let animal = animals.find(a => a.id === id)
32 let newAnimal = {
33 ...animal,
34 verse: verse
35 }
36 animals = animals.map(a => a.id !== id ? a : newAnimal)
37 }
38
39 function printAnimals() {
40 console.log(animals)
41 }
42
43 const publicInterface = {
44 getVerse,
45 setVerse,
46 printAnimals
47 }
48
49 return publicInterface
50}
51
52let animalVerses_A = new manageAnimals();
53let animalVerses_B = new manageAnimals();
54
55console.log(animalVerses_A.getVerse(3))
56// ===> chirp
57console.log(animalVerses_B.getVerse(4))
58// ===> whistle
59
60animalVerses_A.setVerse(4, 'bark')
61animalVerses_B.setVerse(4, 'buzz')
62animalVerses_A.printAnimals()
63// ===> [ ..., { id: 4, name: 'dolphin', verse: 'bark' }, ...]
64animalVerses_B.printAnimals()
65// ===> [ ..., { id: 4, name: 'dolphin', verse: 'buzz' }, ...]
66
67console.log(animalVerses_A.animals)
68// ===> undefined
69console.log(animalVerses_B.animals)
70// ===> undefined
71
72animalVerses_A.printAnimals()
73animalVerses_B.printAnimals()
74// ===> ... come ci si aspetta
Qui viene definito il modulo manageAnimals
con il quale vengono definite le due diverse istanze animalVerses_A
e animalVerses_B
, ognuna delle quali ha un proprio e diverso stato animals
che infatti, come si vede dai vari console.log
, vengono gestiti in modo diverso.
Il modulo in Node
Sia in Node che in ES il concetto di modulo è collegato a quello di file
: un file contiene un solo modulo, ogni modulo è contenuto in un suo proprio file.
Possiamo trasformare il modulo dell’esempio precedente nel formato Node nel modo seguente:
1// === file animalVerses.js =======
2//
3let animals = [
4 {
5 id: 1,
6 name: 'chicken',
7 verse: 'cluck'
8 },
9 {
10 id: 2,
11 name: 'zebra',
12 verse: 'neigh'
13 },
14 {
15 id: 3,
16 name: 'penguin',
17 verse: 'chirp'
18 },
19 {
20 id: 4,
21 name: 'dolphin',
22 verse: 'whistle'
23 },
24];
25
26function getVerse(id) {
27 let animal = animals.find(a => a.id === id)
28 return animal.verse
29}
30
31function setVerse(id, verse) {
32 let animal = animals.find(a => a.id === id)
33 let newAnimal = {
34 ...animal,
35 verse: verse
36 }
37 animals = animals.map(a => a.id !== id ? a : newAnimal)
38}
39
40function printAnimals() {
41 console.log(animals)
42}
43
44module.exports = {
45 getVerse,
46 setVerse,
47 printAnimals
48}
49//
50// === end of file animalVerses.js =======
51
52// === file index.js =======
53//
54const myAnimalVerses = require('path/to/animalVerses.js')
55
56console.log(myAnimalVerses.getVerse(3))
57// ===> chirp
58
59myAnimalVerses.setVerse(4, 'bark')
60myAnimalVerses.printAnimals()
61// ===> [ ..., { id: 4, name: 'dolphin', verse: 'bark' }, ...]
62
63console.log(myAnimalVerses.animals)
64// ===> undefined
65
66myAnimalVerses.printAnimals()
67// ===> ... come ci si aspetta
68//
69// === end of file index.js =======
Il file animalVerses.js
definisce i metodi di accesso ai dati in animals
. Il modulo Node espone un oggetto vuoto (module.exports
) al quale vengono assegnati come campi i metodi e i dati del modulo che costituiscono l’interfaccia pubblica 1. Per creare una istanza del modulo si usa la sintassi const myAnimalVerses = require('path/to/animalVerses.js')
che ha l’effetto di assegnare a myAnimalVerses
il contenuto dell’interfaccia pubblica esposta dal modulo animalVerses.js
.
Il metodo require
importa tutto il contenuto dell’interfaccia pubblica del modulo. È possibile però importare solo parte dell’interfaccia con uno dei seguenti procedimenti:
1const printAnimals = require('path/to/animalVerses.js').printAnimals
2// oppure
3const { printAnimals } = require('path/to/animalVerses.js')
Infine, i moduli contenuti all’interno della directory node_modules
possono essere referenziati con la sintassi priva della indicazione del percorso del file, ad es: require('animalVerses')
.
Il modulo in ES
Il modulo in ES è molto simile a quello usato in Node: anche in ES ogni modulo è contenuto in un suo proprio file e la sintassi di definizione di dati e metodi è praticamente identica.
Ma al posto dell’oggetto module.exports
si usa la keyword export
e al posto di require()
si usa la keyword import
. Possiamo quindi riscrivere il nostro modulo in formato ES nel seguente modo:
1// === file animalVerses.js =======
2//
3let animals = [
4 {
5 id: 1,
6 name: 'chicken',
7 verse: 'cluck'
8 },
9 {
10 id: 2,
11 name: 'zebra',
12 verse: 'neigh'
13 },
14 {
15 id: 3,
16 name: 'penguin',
17 verse: 'chirp'
18 },
19 {
20 id: 4,
21 name: 'dolphin',
22 verse: 'whistle'
23 },
24];
25
26function getVerse(id) {
27 let animal = animals.find(a => a.id === id)
28 return animal.verse
29}
30
31function setVerse(id, verse) {
32 let animal = animals.find(a => a.id === id)
33 let newAnimal = {
34 ...animal,
35 verse: verse
36 }
37 animals = animals.map(a => a.id !== id ? a : newAnimal)
38}
39
40function printAnimals() {
41 console.log(animals)
42}
43
44export {
45 getVerse,
46 setVerse,
47 printAnimals
48}
49//
50// === end of file animalVerses.js =======
51
52// === file index.js =======
53//
54import * as myAnimalVerses from 'path/to/animalVerses.js'
55
56console.log(myAnimalVerses.getVerse(3))
57// ===> chirp
58
59myAnimalVerses.setVerse(4, 'bark')
60myAnimalVerses.printAnimals()
61// ===> [ ..., { id: 4, name: 'dolphin', verse: 'bark' }, ...]
62
63console.log(myAnimalVerses.animals)
64// ===> undefined
65
66myAnimalVerses.printAnimals()
67// ===> ... come ci si aspetta
68//
69// === end of file index.js =======
Come si vede, le differenze rispetto al modulo in formato Node sono davvero pochissime.
Anche in ES è possibile importare solo alcuni degli elementi dell’interfaccia (invece di importarli tutti, come è il comportamento di default) con la sintassi
1import { getVerse, printAnimals } from 'path/to/animalVerses.js'
È possibile indicare nel modulo uno dei dati o metodi dell’interfaccia come export default
; in questo caso la sintassi per l’importazione è priva delle parentesi graffe:
1// === file animalVerses.js =======
2function getVerse(id) {/* ... etc. */}
3
4export default getVerse
5
6// === file index.js =======
7import getVerse from 'path/to/animalVerses.js'
Nel banner: Margaret Hamilton. Ha scritto parte del codice che girava sui computer di bordo della missione Apollo 11. In una famosa fotografia la si vede reggere la pila di fogli contenenti il listato (alta quanto lei) perchè non cada. Il codice era scritto in assembly, a mano su fogli di carta, e successivamente trasformato in schede perforate.
-
In realtà la questione è un po’ più complessa: l’istruzione nell’esempio sostituisce l’oggetto vuoto offerto da
module.exports
con il diverso oggetto contenente i riferimenti ai metodi del modulo. Alcuni autori ritengono che questo procedimento possa dar luogo a problemi in alcuni casi limite e propongono metodi alternativi, ad esempio:Object.assign(module.exports, {/* ... exports */})
, oppuremodule.exports.method1 = method1; module.exports.method2 = method2; /* ... etc. */
. Ho usato il procedimento nel testo sia perchè è comunque molto usato nella programmazione contemporanea, sia perchè è un meccanismo facilmente comprensibile e memorizzabile. ↩︎