Fa cinc lliçons que acumulem un deute. A cada fitxer de laboratori has tornat a copiar l'array cataleg. Les classes Esdeveniment i Sessio que vas escriure al Mòdul 1 continuen sense tenir casa. Has escrit module.exports tres vegades sense que ningú t'expliqués què fa. I a la lliçó El Teu Primer Programa en Node.js vam deixar una nota que deia, literalment, «que aquesta duplicació et resulti incòmoda és precisament el motiu pel qual existeixen els mòduls».
Avui paguem aquest deute. Entendràs el sistema de mòduls CommonJS: com require troba els fitxers, què fa exactament module.exports, per què reassignar exports no funciona, quin embolcall invisible afegeix Node a cada fitxer que executes, i per què un mòdul s'avalua un sol cop en tota la vida del procés, convertint-se de facto en un singleton.
I en acabar, Escena Viva tindrà per fi una estructura de debò: src/cataleg-dades.js exportant el catàleg, src/domini/esdeveniment.js i src/domini/sessio.js amb les classes del Mòdul 1, un src/domini/index.js que les reexporta, i src/cataleg.js consumint-ho tot sense ni una línia duplicada.
Contingut
- Per què existeixen els mòduls
- El sistema CommonJS:
requireimodule.exports exportscontramodule.exports- L'embolcall de mòdul i les seves cinc variables
- Tipus de mòdul i algorisme de resolució de
require - La memòria cau de mòduls: un mòdul és un singleton
- Dependències circulars
- Refactorització d'Escena Viva
- Bones pràctiques de disseny de mòduls
- Per què existeixen els mòduls
JavaScript va néixer sense mòduls. Durant quinze anys, al navegador, tots els scripts d'una pàgina compartien un únic àmbit global, amb les conseqüències previsibles:
<!-- El problema de l'ambit global compartit -->
<script src="cataleg.js"></script> <!-- defineix: var cataleg = [...] -->
<script src="informes.js"></script> <!-- tambe defineix: var cataleg = {} -->
<!-- El segon trepitja el primer. Ningu no avisa. Tot es trenca. -->Tres problemes concrets:
| Problema | Conseqüència |
|---|---|
| Col·lisions de noms | Dos fitxers amb la mateixa variable global es trepitgen silenciosament |
| Dependències implícites | L'ordre dels <script> importa, però no està escrit enlloc |
| Res no és privat | Qualsevol detall intern és accessible i modificable des de qualsevol lloc |
Node.js no s'ho podia permetre: un servidor amb cent fitxers i trenta dependències externes hauria estat inabastable. Així que va adoptar CommonJS, una especificació de mòduls pensada per al costat del servidor, i la va implantar des de la seva primera versió.
La idea central és d'una simplicitat radical:
Cada fitxer és un mòdul. Tot el que declares a dins és privat, tret del que exportis explícitament.
// src/utils/format.js
// PRIVAT: ningu de fora d'aquest fitxer pot veure aquesta constant.
const SIMBOL_MONEDA = 'EUR';
// PRIVAT: funcio auxiliar interna.
function partirCentims(centims) {
return { euros: Math.floor(centims / 100), resta: centims % 100 };
}
// PUBLIC: nomes aixo surt del modul.
function formatarPreu(centims) {
const { euros, resta } = partirCentims(centims);
return `${euros},${String(resta).padStart(2, '0')} ${SIMBOL_MONEDA}`;
}
module.exports = { formatarPreu };// Un altre fitxer
const { formatarPreu } = require('./utils/format.js');
console.log(formatarPreu(2500)); // 25,00 EUR
console.log(SIMBOL_MONEDA); // ReferenceError: no existeix aqui
console.log(partirCentims); // ReferenceError: no existeix aquiAquesta privacitat per defecte és la propietat més valuosa del sistema. Et permet canviar els detalls interns d'un mòdul sense por, perquè saps amb certesa que ningú de fora no en depèn.
- El sistema CommonJS:
require i module.exports
require i module.exportsCommonJS té només dos verbs.
module.exports: el que el mòdul ofereix
Cada mòdul té un objecte module, i la seva propietat exports és exactament el valor que require tornarà. Li pots assignar el que vulguis:
// Un objecte amb diverses coses (el mes habitual)
module.exports = { formatarPreu, formatarData };
// Una sola funcio
module.exports = function calcularOcupacio(sessio) { /* ... */ };
// Una sola classe
module.exports = class Esdeveniment { /* ... */ };
// Un array de dades
module.exports = [ { id: 'evt-001' }, { id: 'evt-002' } ];
// Un valor primitiu (rar, pero legal)
module.exports = 3000;require: el que el mòdul necessita
require(ruta) carrega un mòdul i en torna el module.exports:
// Importar l'objecte complet
const format = require('./utils/format.js');
console.log(format.formatarPreu(2500));
// Desestructurar nomes el que necessites (preferit: es veu d'un cop d'ull
// que fa servir aquest fitxer)
const { formatarPreu } = require('./utils/format.js');
// Reanomenar en desestructurar, per evitar collisions
const { formatarPreu: preu } = require('./utils/format.js');Els dos estils d'exportació
| Estil | Quan fer-lo servir | Exemple d'importació |
|---|---|---|
Objecte amb noms module.exports = { a, b } |
El mòdul ofereix diverses coses relacionades | const { a, b } = require('./m.js') |
Exportació única module.exports = X |
El mòdul és una sola cosa: una classe, una funció | const X = require('./m.js') |
A Escena Viva farem servir l'objecte amb noms gairebé sempre, fins i tot per a mòduls amb un únic element. Raons:
- És ampliable. Afegir una segona exportació no trenca ningú.
- El nom viatja amb el valor.
const { GestorDeVendes } = require(...)deixa clar què és, mentre queconst X = require(...)depèn que qui importa triï bé el nom. - És coherent amb els mòduls ES, que veurem a la lliçó següent.
exports contra module.exports
exports contra module.exportsAquí hi ha el parany clàssic de CommonJS, i val la pena entendre'l a fons perquè la seva explicació revela com funciona el sistema.
Node injecta a cada mòdul dues variables relacionades: module i exports. I al principi del fitxer, es compleix això:
// El que Node fa, conceptualment, abans d'executar el teu codi:
const module = { exports: {} };
let exports = module.exports; // <-- Totes dues apunten al MATEIX objecteflowchart LR
subgraph inici["En començar el mòdul"]
E1["exports"] --> O1["{ }<br/><i>l'objecte d'exportació</i>"]
M1["module.exports"] --> O1
end
Mentre afegeixis propietats, les dues variables funcionen igual, perquè manipulen el mateix objecte:
// src/utils/format.js
exports.formatarPreu = formatarPreu; // Funciona
exports.formatarData = formatarData; // Funciona
// Equivalent:
module.exports.formatarPreu = formatarPreu;Però en el moment en què reassignes exports, la connexió es trenca:
flowchart LR
subgraph trencat["Després de reassignar exports"]
E2["exports"] --> O3["{ formatarPreu,<br/>formatarData }<br/><i>objecte nou, orfe</i>"]
M2["module.exports"] --> O2["{ }<br/><i>l'objecte que require torna</i>"]
end
style O3 stroke-dasharray: 5 5
exports passa a apuntar a un objecte nou, però module.exports continua apuntant a l'original buit. I require torna module.exports, no pas exports.
Comprova-ho:
// src/laboratori/exports-trencat.js
exports = { hola: () => 'hola' };
console.log('Dins del modul, exports:', exports); // { hola: [Function] }
console.log('Dins del modul, module.exports:', module.exports); // {}// src/laboratori/provar-exports.js
const modul = require('./exports-trencat.js');
console.log(modul); // {} <- buit
console.log(modul.hola); // undefinedLa regla, en una taula:
| Escriptura | Funciona? | Per què |
|---|---|---|
exports.nom = valor |
Sí | Afegeix una propietat a l'objecte compartit |
module.exports.nom = valor |
Sí | Idèntic a l'anterior |
module.exports = { ... } |
Sí | Substitueix el que require tornarà |
exports = { ... } |
No | Trenca la connexió; module.exports no canvia |
module.exports = X i després exports.y = ... |
No per a y |
Ja són objectes diferents |
Recomanació per al curs: fes servir sempre
module.exports = { ... }en una sola línia al final del fitxer.Mai no tindràs aquest problema, i a més el fitxer hi guanya una cosa valuosa: un lloc únic on es veu tota la seva API pública. Un lector que obri el mòdul pot anar al final i saber en dos segons què ofereix.
- L'embolcall de mòdul i les seves cinc variables
Com aconsegueix Node que cada fitxer tingui el seu propi àmbit, si JavaScript no té mòduls? Amb un truc d'una elegància notable: abans d'executar el teu fitxer, l'embolcalla en una funció.
El teu codi:
El que Node executa realment:
(function (exports, require, module, __filename, __dirname) {
// ---- El teu codi va aqui, intacte ----
const cataleg = [];
module.exports = { cataleg };
// --------------------------------------
});Això s'anomena l'embolcall de mòdul (module wrapper), i explica de cop diverses coses:
- Per què les teves variables no contaminen l'àmbit global: són dins d'una funció.
- D'on surten
require,moduleiexports: són paràmetres d'aquesta funció, no pas variables globals. - Per què
thisal nivell superior d'un mòdul CommonJS ésmodule.exports(un objecte buit), i noglobal.
Ho pots veure amb els teus propis ulls:
Les cinc variables injectades:
| Variable | Què és | Exemple d'ús |
|---|---|---|
exports |
Drecera a module.exports (amb el parany de l'apartat 3) |
exports.formatar = fn |
require |
Funció per carregar altres mòduls | require('./format.js') |
module |
Objecte que representa aquest mòdul | module.exports = {...} |
__filename |
Ruta absoluta d'aquest fitxer | /home/joan/escena-viva/src/cataleg.js |
__dirname |
Ruta absoluta de la carpeta que el conté | /home/joan/escena-viva/src |
// src/laboratori/variables-modul.js
console.log('__filename:', __filename);
console.log('__dirname :', __dirname);
console.log('module.id :', module.id); // '.' si es el punt d'entrada
console.log('this === module.exports:', this === module.exports); // true
console.log("Es el punt d'entrada:", require.main === module);Dos usos pràctics que veuràs molt:
__dirname per a rutes fiables. El directori de treball (process.cwd()) depèn de des d'on s'executi l'ordre; __dirname, no.
const path = require('node:path');
// MALAMENT: depen de des d'on executis node.
const rutaDades = './dades/esdeveniments.json';
// BE: sempre correcta, executis des d'on executis.
const rutaDades = path.join(__dirname, '..', 'dades', 'esdeveniments.json');Això ho formalitzarem a la lliçó Rutes Multiplataforma amb el Mòdul path.
require.main === module per saber si ets el programa principal. Permet que un fitxer sigui alhora mòdul reutilitzable i script executable:
// src/informes/ocupacio.js
function generarInformeOcupacio(cataleg) {
// ... logica de l'informe ...
}
// Nomes si s'executa directament amb "node src/informes/ocupacio.js"
if (require.main === module) {
const { cataleg } = require('../cataleg-dades.js');
console.log(generarInformeOcupacio(cataleg));
}
module.exports = { generarInformeOcupacio };Amb això, require('./informes/ocupacio.js') no imprimeix res (només exporta la funció), però node src/informes/ocupacio.js sí que executa l'informe. És un patró molt útil i molt comú.
- Tipus de mòdul i algorisme de resolució de
require
requireQuan escrius require('alguna-cosa'), Node ha de decidir quin fitxer carregar. Segueix un algorisme ben definit.
Els tres tipus de mòdul
| Tipus | Com s'escriu | Exemple | On viu |
|---|---|---|---|
| Del nucli | Nom a seques, o amb prefix node: |
require('node:fs') |
Dins del binari de Node |
| De fitxer | Ruta que comença per ./, ../ o / |
require('./domini/esdeveniment.js') |
El teu projecte |
| De paquet | Nom a seques que no és del nucli | require('express') |
node_modules/ |
Fes servir sempre el prefix
node:per als mòduls del nucli:require('node:fs')en lloc derequire('fs'). És la forma moderna i recomanada, elimina qualsevol ambigüitat amb un paquet d'npm que es digués igual, i és lleugerament més ràpida perquè Node se salta la cerca.
L'algorisme
flowchart TD
A["require('X')"] --> B{"X és un mòdul<br/>del nucli?"}
B -->|"Sí"| C["Tornar el mòdul intern<br/>(fs, path, http, events...)"]
B -->|"No"| D{"X comença per<br/>./ , ../ o / ?"}
D -->|"Sí"| E["Resoldre com a fitxer:<br/>1. X tal qual<br/>2. X.js<br/>3. X.json<br/>4. X.node"]
E --> F{"Existeix?"}
F -->|"Sí"| G["Carregar i tornar"]
F -->|"No"| H["Resoldre com a carpeta:<br/>1. X/package.json → camp main<br/>2. X/index.js<br/>3. X/index.json"]
H --> I{"Existeix?"}
I -->|"Sí"| G
I -->|"No"| J["Error: MODULE_NOT_FOUND"]
D -->|"No"| K["Cercar a node_modules,<br/>pujant per l'arbre de carpetes"]
K --> L{"Trobat?"}
L -->|"Sí"| G
L -->|"No"| J
Resolució de fitxers i carpetes
Node prova extensions i després la interpretació com a carpeta:
require('./domini/esdeveniment')
// 1. ./domini/esdeveniment (tal qual)
// 2. ./domini/esdeveniment.js <-- normalment aqui
// 3. ./domini/esdeveniment.json
// 4. ./domini/esdeveniment.node (complement binari en C++)
require('./domini')
// 1-4. L'anterior amb "domini"...
// 5. ./domini/package.json -> llegeix el seu camp "main"
// 6. ./domini/index.js <-- el patro habitual
// 7. ./domini/index.jsonAquest punt 6 és la raó que existeixi la convenció del fitxer index.js: permet que require('./domini') carregui tota una carpeta. El farem servir a la refactorització.
Encara que les extensions siguin opcionals en CommonJS, escriu-les sempre:
require('./sessio.js'), no pasrequire('./sessio'). És més explícit, és una mica més ràpid (Node no ha de provar) i —sobretot— és obligatori en mòduls ES, així que escriure-les et prepara per a la lliçó següent i per migrar sense sorpreses.
La cerca ascendent a node_modules
Per a un paquet com express, Node cerca a node_modules pujant carpeta a carpeta fins a l'arrel del sistema:
Des de /home/joan/escena-viva/src/servidor/rutes.js, require('express') cerca a:
/home/joan/escena-viva/src/servidor/node_modules/express
/home/joan/escena-viva/src/node_modules/express
/home/joan/escena-viva/node_modules/express <-- normalment aqui
/home/joan/node_modules/express
/home/node_modules/express
/node_modules/expressPots veure aquesta llista en temps real:
Aquest mecanisme explica una cosa que confon al principi: per què de vegades un paquet funciona sense ser al teu package.json. Estava instal·lat en una carpeta superior i la cerca ascendent el va trobar. És una dependència accidental que deixarà de funcionar tan bon punt moguis el projecte o algú altre l'instal·li des de zero. Ho veurem al Mòdul 5.
Carregar JSON directament
CommonJS carrega fitxers .json de manera nativa, ja analitzats:
// Torna directament l'array, sense JSON.parse.
const cataleg = require('./dades/esdeveniments.json');
console.log(cataleg.length); // 3És còmode i l'has fet servir als node -p del Mòdul 1. Però té tres inconvenients que cal conèixer:
- És síncron: bloqueja el fil principal mentre llegeix el fitxer.
- Es posa a la memòria cau: si el fitxer canvia al disc,
requirecontinua tornant la versió antiga. - No existeix en mòduls ES sense sintaxi addicional.
Per a configuració d'arrencada és perfectament acceptable. Per a dades que canvien —com el catàleg d'Escena Viva— farem servir fs al Mòdul 3.
- La memòria cau de mòduls: un mòdul és un singleton
Aquesta és la característica de CommonJS amb més conseqüències pràctiques:
Un mòdul s'avalua UN SOL COP. La primera vegada que se li fa
require, s'executa i el seumodule.exportses desa a la memòria cau. Totes les crides posteriors tornen exactament el mateix objecte.
Demostrem-ho amb un comptador:
// src/laboratori/comptador.js
console.log(">> El modul comptador.js s'esta AVALUANT");
let compte = 0;
function incrementar() {
compte++;
return compte;
}
function valor() {
return compte;
}
module.exports = { incrementar, valor };// src/laboratori/usar-comptador.js
console.log('--- Primer require ---');
const primer = require('./comptador.js');
console.log('--- Segon require ---');
const segon = require('./comptador.js');
console.log('--- Tercer require ---');
const tercer = require('./comptador.js');
console.log('');
console.log('Son el mateix objecte?', primer === segon, segon === tercer);
primer.incrementar();
primer.incrementar();
segon.incrementar();
console.log('Valor vist des de "primer":', primer.valor());
console.log('Valor vist des de "tercer":', tercer.valor());--- Primer require --- >> El modul comptador.js s'esta AVALUANT --- Segon require --- --- Tercer require --- Son el mateix objecte? true true Valor vist des de "primer": 3 Valor vist des de "tercer": 3
Tres fets en aquesta sortida:
- El missatge d'avaluació apareix un sol cop, encara que hi va haver tres
require. - Els tres objectes són idèntics (
===). - L'estat es comparteix: incrementar des d'una referència ho veu tothom.
Dit d'una altra manera: tot mòdul CommonJS és un singleton dins del procés.
La utilitat
És exactament el que vols per a recursos compartits i cars de crear:
// src/dades/connexio.js
// El grup de connexions a la base de dades: un de sol a tot el proces.
const grup = crearGrupDeConnexions({ maxim: 20 });
module.exports = { grup };Tant se val des de quants fitxers se li faci require: sempre és el mateix grup de 20 connexions, no pas vint grups. El mateix serveix per al GestorDeVendes d'Escena Viva, per a un registrador (logger) o per a una memòria cau en memòria. Aquest patró és el que farem servir al Mòdul 7.
Els riscos
Risc 1: estat compartit no desitjat.
// PERILL: la configuracio es mutable i global.
// src/config.js
module.exports = { limitEntradesPerComanda: 10 };
// En un altre fitxer, algu fa:
const config = require('./config.js');
config.limitEntradesPerComanda = 999; // Ho canvia per a TOTA l'aplicacioRemei: congelar allò que ha de ser immutable.
Risc 2: les proves es contaminen entre si.
Si un mòdul acumula estat i diverses proves el fan servir, la segona prova hereta l'estat de la primera. És una font clàssica de proves que passen en solitari i fallen en conjunt. Ho tractarem al Mòdul 9; per a mòduls amb estat, la solució habitual és exportar una funció fàbrica en lloc d'una instància:
// En lloc d'exportar una instancia (singleton forcos)...
module.exports = { gestor: new GestorDeVendes(cataleg) };
// ...exporta la classe i deixa que cadascu creï la seva.
module.exports = { GestorDeVendes };Risc 3: la clau de la memòria cau és la ruta resolta. Dues rutes diferents que apuntin al mateix fitxer (per un enllaç simbòlic, o per majúscules i minúscules a Windows) poden produir dues instàncies del mateix mòdul. És rar, però quan passa és desconcertant.
Inspeccionar i buidar la memòria cau
// Rutes de tots els moduls carregats
console.log(Object.keys(require.cache));
// Forcar que un modul es torni a avaluar al seguent require
delete require.cache[require.resolve('./comptador.js')];
const nou = require('./comptador.js'); // Es torna a avaluar: compte = 0Manipular
require.cacheés una eina de laboratori, no de producció. Es fa servir en algunes configuracions de proves i en recarregadors en calent de desenvolupament. En codi d'aplicació és gairebé sempre senyal d'un problema de disseny.
- Dependències circulars
Passa quan A requereix B i B requereix A. Node no falla, però el resultat sorprèn.
// src/laboratori/circular-a.js
console.log('A: comenca a avaluar-se');
const b = require('./circular-b.js');
console.log('A: b val', b);
module.exports = { nom: 'modul A' };
console.log("A: acaba d'avaluar-se");// src/laboratori/circular-b.js
console.log('B: comenca a avaluar-se');
const a = require('./circular-a.js');
console.log('B: a val', a); // <-- Aqui hi ha la sorpresa
module.exports = { nom: 'modul B' };
console.log("B: acaba d'avaluar-se");A: comenca a avaluar-se
B: comenca a avaluar-se
B: a val {} <-- Objecte BUIT, no pas { nom: 'modul A' }
B: acaba d'avaluar-se
A: b val { nom: 'modul B' }
A: acaba d'avaluar-seL'explicació pas a pas:
| Pas | Què passa |
|---|---|
| 1 | Es comença a avaluar A. Node registra A a la memòria cau amb module.exports = {} (buit) |
| 2 | A fa require('./circular-b.js'). Es comença a avaluar B |
| 3 | B fa require('./circular-a.js'). A ja és a la memòria cau, així que Node torna el seu module.exports actual: l'objecte buit |
| 4 | B acaba i exporta el que li toca correctament |
| 5 | A rep el module.exports complet de B i acaba |
La regla: en una dependència circular, el mòdul que es carrega en segon lloc rep una versió incompleta del primer. No és cap fallada de Node: és l'única cosa raonable que pot fer sense entrar en un bucle infinit.
I per això les dependències circulars produeixen errors tan desconcertants:
// A circular-b.js
const { crearEsdeveniment } = require('./circular-a.js');
crearEsdeveniment(); // TypeError: crearEsdeveniment is not a functionCom evitar-les
| Tècnica | Com |
|---|---|
| Extreure el que és comú a un tercer mòdul | Si A i B comparteixen alguna cosa, aquesta cosa va a C, i tots dos depenen de C |
| Invertir la dependència | En lloc que A busqui B, que qui els fa servir tots dos els passi el que necessiten |
Moure el require dins de la funció |
Es resol en temps d'execució, quan tots dos mòduls ja són complets. És un pedaç, no una solució |
| Fer servir esdeveniments | A emet i B escolta, sense que A conegui B. Justament el de la lliçó anterior |
Aquesta última fila no és casual. Moltes dependències circulars són un símptoma d'acoblament excessiu, i el patró observador de la lliçó Esdeveniments i EventEmitter és sovint la solució de disseny correcta, no només un truc per trencar el cicle.
Per detectar-les en un projecte real existeixen eines com madge, que dibuixa el graf de dependències i n'assenyala els cicles.
- Refactorització d'Escena Viva
Ha arribat el moment. Deixarem el projecte com ha d'estar en acabar el Mòdul 2.
8.1 El punt de partida i el destí
ABANS DESPRES
escena-viva/ escena-viva/
├── dades/ ├── dades/
│ └── esdeveniments.json │ └── esdeveniments.json
└── src/ └── src/
├── cataleg.js <- dades ├── cataleg.js <- nomes presentacio
│ incrustades ├── cataleg-dades.js <- nomes dades
└── cataleg-dades.js <- sense ├── domini/
module.exports │ ├── sessio.js
│ ├── esdeveniment.js
│ ├── gestor-vendes.js
│ └── index.js
└── utils/
└── format.jsflowchart TD
C["src/cataleg.js<br/><i>punt d'entrada</i>"]
CD["src/cataleg-dades.js<br/><i>les dades</i>"]
DI["src/domini/index.js<br/><i>façana</i>"]
EV["src/domini/esdeveniment.js"]
SE["src/domini/sessio.js"]
GV["src/domini/gestor-vendes.js"]
FO["src/utils/format.js"]
C --> CD
C --> DI
C --> FO
DI --> EV
DI --> SE
DI --> GV
EV --> SE
GV --> SE
Fixa't en la forma del graf: totes les fletxes van en una direcció. No hi ha cicles, i les dependències van del que és general (cataleg.js) al que és específic (sessio.js). Aquest és l'objectiu de tot disseny de mòduls.
8.2 src/utils/format.js
Comencem per baix: el mòdul que no depèn de res.
// src/utils/format.js
// Utilitats de presentacio d'Escena Viva.
// Sense dependencies: es el modul mes basic del projecte.
// Converteix 2500 (centims) en la cadena '25,00 EUR'.
function formatarPreu(centims) {
const euros = Math.floor(centims / 100);
const resta = String(centims % 100).padStart(2, '0');
return `${euros},${resta} EUR`;
}
// Converteix '2026-10-03T20:00:00' en '03/10/2026 20:00'.
function formatarData(dataISO) {
const data = new Date(dataISO);
const dia = String(data.getDate()).padStart(2, '0');
const mes = String(data.getMonth() + 1).padStart(2, '0');
const any = data.getFullYear();
const hora = String(data.getHours()).padStart(2, '0');
const minut = String(data.getMinutes()).padStart(2, '0');
return `${dia}/${mes}/${any} ${hora}:${minut}`;
}
// Genera un codi d'entrada: EV-2026-000123
function generarCodiEntrada(any, sequencia) {
return `EV-${any}-${String(sequencia).padStart(6, '0')}`;
}
module.exports = { formatarPreu, formatarData, generarCodiEntrada };Aquestes tres funcions estaven duplicades en diversos fitxers del Mòdul 1. Ara existeixen un sol cop.
8.3 src/domini/sessio.js
La classe Sessio que vas escriure com a solució d'exercici a la lliçó JavaScript Modern per a Node.js, ara amb casa pròpia.
// src/domini/sessio.js
// Una sessio es un passi concret d'un esdeveniment, amb data, aforament i preu.
const { formatarPreu, formatarData } = require('../utils/format.js');
class Sessio {
// Camp privat: l'unica manera de modificar-lo es a traves de vendre().
#venudes = 0;
constructor({ id, dataHora, aforament, venudes = 0, preuCentims }) {
this.id = id;
this.dataHora = dataHora;
this.aforament = aforament;
this.preuCentims = preuCentims;
this.#venudes = venudes;
}
get venudes() {
return this.#venudes;
}
get lliures() {
return this.aforament - this.#venudes;
}
get ocupacio() {
return Math.round((this.#venudes / this.aforament) * 100);
}
get exhaurida() {
return this.lliures === 0;
}
get preuEuros() {
return (this.preuCentims / 100).toFixed(2);
}
// Recaptacio en centims, enter.
get recaptacioCentims() {
return this.#venudes * this.preuCentims;
}
vendre(quantitat = 1) {
if (!Number.isInteger(quantitat) || quantitat < 1) {
const error = new Error('La quantitat ha de ser un enter positiu');
error.codi = 'QUANTITAT_INVALIDA';
throw error;
}
if (quantitat > this.lliures) {
const error = new Error(`Aforament insuficient a ${this.id}: en queden ${this.lliures}`);
error.codi = 'AFORAMENT_INSUFICIENT';
throw error;
}
this.#venudes += quantitat;
return this.#venudes;
}
// Linia d'una sola fila per al llistat per consola.
descriure() {
return (
`${formatarData(this.dataHora)} ${formatarPreu(this.preuCentims)} ` +
`${this.lliures}/${this.aforament} lliures (${this.ocupacio}% ocupat)` +
(this.exhaurida ? ' [EXHAURIDA]' : '')
);
}
// JSON.stringify crida automaticament a toJSON si existeix.
// Sense aixo, el camp privat #venudes no apareixeria a la serialitzacio.
toJSON() {
return {
id: this.id,
dataHora: this.dataHora,
aforament: this.aforament,
venudes: this.#venudes,
preuCentims: this.preuCentims
};
}
}
module.exports = { Sessio };8.4 src/domini/esdeveniment.js
// src/domini/esdeveniment.js
// Un esdeveniment es un espectacle programat, amb una o diverses sessions.
const { Sessio } = require('./sessio.js');
class Esdeveniment {
#sessions = [];
constructor({ id, titol, sala, organitzador, categoria, duracioMinuts, estat = 'publicat', sessions = [] }) {
this.id = id;
this.titol = titol;
this.sala = sala;
this.organitzador = organitzador;
this.categoria = categoria;
this.duracioMinuts = duracioMinuts;
this.estat = estat;
// Convertim els objectes plans en instancies de Sessio.
// Si ja ho son, els deixem tal com estan.
this.#sessions = sessions.map((s) => (s instanceof Sessio ? s : new Sessio(s)));
}
get sessions() {
// Copia defensiva: ningu de fora no pot afegir ni treure sessions.
return [...this.#sessions];
}
get nombreSessions() {
return this.#sessions.length;
}
get aforamentTotal() {
return this.#sessions.reduce((total, s) => total + s.aforament, 0);
}
get entradesVenudes() {
return this.#sessions.reduce((total, s) => total + s.venudes, 0);
}
get entradesLliuresTotals() {
return this.aforamentTotal - this.entradesVenudes;
}
get ocupacio() {
if (this.aforamentTotal === 0) return 0;
return Math.round((this.entradesVenudes / this.aforamentTotal) * 100);
}
get recaptacioCentims() {
return this.#sessions.reduce((total, s) => total + s.recaptacioCentims, 0);
}
get exhaurit() {
return this.#sessions.every((s) => s.exhaurida);
}
cercarSessio(idSessio) {
return this.#sessions.find((s) => s.id === idSessio);
}
entradesLliures(idSessio) {
const sessio = this.cercarSessio(idSessio);
return sessio ? sessio.lliures : 0;
}
reservar(idSessio, quantitat = 1) {
const sessio = this.cercarSessio(idSessio);
if (!sessio) {
const error = new Error(`La sessio ${idSessio} no existeix a ${this.id}`);
error.codi = 'SESSIO_NO_TROBADA';
throw error;
}
// Deleguem en Sessio: es ella qui sap validar el seu propi aforament.
return sessio.vendre(quantitat);
}
toString() {
return `${this.titol} (${this.sala}) - ${this.ocupacio}% ocupat`;
}
toJSON() {
return {
id: this.id,
titol: this.titol,
sala: this.sala,
organitzador: this.organitzador,
categoria: this.categoria,
duracioMinuts: this.duracioMinuts,
estat: this.estat,
sessions: this.#sessions.map((s) => s.toJSON())
};
}
static desDeJSON(objecte) {
return new Esdeveniment(objecte);
}
}
module.exports = { Esdeveniment };Fixa't en la millora de disseny respecte a la versió del Mòdul 1: Esdeveniment.reservar ja no valida l'aforament a mà, sinó que delega en sessio.vendre(). Cada classe sap validar el que és seu. Això només és possible ara que Sessio existeix com a mòdul independent i Esdeveniment la pot requerir.
8.5 src/domini/index.js: la façana
// src/domini/index.js
// Facana del domini: un unic punt d'entrada per a tot el model.
// Permet escriure require('./domini') en lloc de tres require diferents.
const { Sessio } = require('./sessio.js');
const { Esdeveniment } = require('./esdeveniment.js');
const { GestorDeVendes, LLINDAR_AFORAMENT_BAIX } = require('./gestor-vendes.js');
module.exports = { Sessio, Esdeveniment, GestorDeVendes, LLINDAR_AFORAMENT_BAIX };Aquest patró —un index.js que reexporta— s'anomena façana o barrel, i aporta dues coses:
// Sense facana: tres linies, i qui importa ha de coneixer l'estructura interna.
const { Sessio } = require('./domini/sessio.js');
const { Esdeveniment } = require('./domini/esdeveniment.js');
const { GestorDeVendes } = require('./domini/gestor-vendes.js');
// Amb facana: una linia, i l'estructura interna pot canviar sense trencar res.
const { Sessio, Esdeveniment, GestorDeVendes } = require('./domini');El segon avantatge és l'important: si demà divideixes esdeveniment.js en dos fitxers, només canvies l'index.js. Ningú més no se n'assabenta.
8.6 src/cataleg-dades.js: per fi, un mòdul
Aquí se salda el deute concret del Mòdul 1.
// src/cataleg-dades.js
// Font de dades del cataleg d'Escena Viva.
// Fins al modul 3 les dades son aqui incrustades; despres es llegiran
// de dades/esdeveniments.json amb fs, i aquest fitxer sera l'unic que canvii.
const cataleg = [
{
id: 'evt-001',
titol: 'Concierto de Otono',
sala: 'Teatro Almendra',
organitzador: 'org-almendra',
categoria: 'concert',
duracioMinuts: 95,
estat: 'publicat',
sessions: [
{ id: 'ses-001-1', dataHora: '2026-10-03T20:00:00', aforament: 420, venudes: 180, preuCentims: 2500 },
{ id: 'ses-001-2', dataHora: '2026-10-04T19:00:00', aforament: 420, venudes: 96, preuCentims: 2200 }
]
},
{
id: 'evt-002',
titol: 'Noche de Monologos',
sala: 'Sala Boveda',
organitzador: 'org-boveda',
categoria: 'humor',
duracioMinuts: 80,
estat: 'publicat',
sessions: [
{ id: 'ses-002-1', dataHora: '2026-10-10T21:30:00', aforament: 120, venudes: 118, preuCentims: 1800 },
{ id: 'ses-002-2', dataHora: '2026-10-11T21:30:00', aforament: 120, venudes: 45, preuCentims: 1800 },
{ id: 'ses-002-3', dataHora: '2026-10-17T21:30:00', aforament: 120, venudes: 12, preuCentims: 1500 }
]
},
{
id: 'evt-003',
titol: 'Festival de Jazz de Primavera',
sala: 'Auditorio Ribera',
organitzador: 'org-ribera',
categoria: 'festival',
duracioMinuts: 240,
estat: 'publicat',
sessions: [
{ id: 'ses-003-1', dataHora: '2027-04-17T19:00:00', aforament: 900, venudes: 640, preuCentims: 3800 },
{ id: 'ses-003-2', dataHora: '2027-04-18T19:00:00', aforament: 900, venudes: 720, preuCentims: 4200 }
]
}
];
// Torna una copia profunda perque ningu no modifiqui la font per accident.
// structuredClone es natiu a Node des de la versio 17.
function obtenirCataleg() {
return structuredClone(cataleg);
}
function obtenirEsdevenimentPerId(id) {
const esdeveniment = cataleg.find((e) => e.id === id);
return esdeveniment ? structuredClone(esdeveniment) : undefined;
}
module.exports = { obtenirCataleg, obtenirEsdevenimentPerId };Dues decisions importants:
- Exportem funcions, no pas l'array directament. Si exportéssim
module.exports = { cataleg }, qualsevol podria modificar les dades d'origen i, per la memòria cau de mòduls, aquesta modificació afectaria tot el procés. Tornar una còpia ambstructuredCloneprotegeix la font. - La signatura serà la mateixa quan arribin les dades reals. Al Mòdul 3,
obtenirCataleg()llegiràdades/esdeveniments.jsonambfsi passarà a ser asíncrona. Cap consumidor no haurà de canviar la seva estructura, només afegir unawait. Aquesta és la raó de ser d'aquesta capa.
8.7 src/cataleg.js: el punt d'entrada
// src/cataleg.js
// Punt d'entrada del cataleg per consola d'Escena Viva.
// Us:
// node src/cataleg.js
// node src/cataleg.js --sala="Teatro Almendra"
// node src/cataleg.js --taula
// node src/cataleg.js --max=2000
const { obtenirCataleg } = require('./cataleg-dades.js');
const { Esdeveniment } = require('./domini');
const { formatarPreu } = require('./utils/format.js');
// --- Lectura d'arguments ---
function llegirOpcions(parametres) {
const opcions = { sala: null, taula: false, preuMaximCentims: Infinity };
for (const parametre of parametres) {
if (parametre === '--taula') {
opcions.taula = true;
} else if (parametre.startsWith('--sala=')) {
opcions.sala = parametre.slice('--sala='.length);
} else if (parametre.startsWith('--max=')) {
opcions.preuMaximCentims = Number(parametre.slice('--max='.length));
}
}
return opcions;
}
// --- Presentacio ---
function mostrarDetall(esdeveniments) {
console.log('');
console.log('==============================================');
console.log(" ESCENA VIVA - CATALEG D'ESDEVENIMENTS");
console.log('==============================================');
for (const esdeveniment of esdeveniments) {
console.log('');
console.log(`${esdeveniment.titol} [${esdeveniment.id}]`);
console.log(` Sala : ${esdeveniment.sala}`);
console.log(` Categoria : ${esdeveniment.categoria}`);
console.log(` Durada : ${esdeveniment.duracioMinuts} min`);
console.log(` Sessions : ${esdeveniment.nombreSessions}`);
console.log(` Lliures : ${esdeveniment.entradesLliuresTotals} entrades`);
console.log(` Ocupacio : ${esdeveniment.ocupacio}%`);
for (const sessio of esdeveniment.sessions) {
// La sessio sap descriure's a si mateixa: cataleg.js no calcula res.
console.log(` - ${sessio.descriure()}`);
}
}
console.log('');
}
function mostrarTaula(esdeveniments) {
const files = esdeveniments.flatMap((esdeveniment) =>
esdeveniment.sessions.map((sessio) => ({
esdeveniment: esdeveniment.id,
titol: esdeveniment.titol,
sala: esdeveniment.sala,
sessio: sessio.id,
preu: formatarPreu(sessio.preuCentims),
lliures: sessio.lliures,
ocupacio: `${sessio.ocupacio}%`
}))
);
console.table(files);
}
// --- Programa principal ---
function principal() {
const opcions = llegirOpcions(process.argv.slice(2));
// Les dades planes es converteixen en objectes de domini.
let esdeveniments = obtenirCataleg().map((dades) => Esdeveniment.desDeJSON(dades));
if (opcions.sala) {
esdeveniments = esdeveniments.filter((esdeveniment) => esdeveniment.sala === opcions.sala);
}
if (opcions.preuMaximCentims < Infinity) {
esdeveniments = esdeveniments.filter((esdeveniment) =>
esdeveniment.sessions.some((s) => s.preuCentims <= opcions.preuMaximCentims)
);
}
if (esdeveniments.length === 0) {
// Diagnostic per stderr, segons la convencio del projecte.
console.error('No hi ha esdeveniments que compleixin els criteris indicats.');
process.exitCode = 1;
return;
}
if (opcions.taula) {
mostrarTaula(esdeveniments);
} else {
mostrarDetall(esdeveniments);
}
const aforamentTotal = esdeveniments.reduce((t, e) => t + e.aforamentTotal, 0);
const venudes = esdeveniments.reduce((t, e) => t + e.entradesVenudes, 0);
const recaptacio = esdeveniments.reduce((t, e) => t + e.recaptacioCentims, 0);
console.error(
`${esdeveniments.length} esdeveniments | aforament ${aforamentTotal} | venudes ${venudes} | ` +
`lliures ${aforamentTotal - venudes} | recaptacio ${formatarPreu(recaptacio)}`
);
}
// Nomes s'executa si aquest fitxer es el programa principal.
if (require.main === module) {
principal();
}
module.exports = { llegirOpcions, principal };Comprovació:
┌─────────┬──────────────┬─────────────────────────────────┬────────────────────┬─────────────┬─────────────┬─────────┬──────────┐ │ (index) │ esdeveniment │ titol │ sala │ sessio │ preu │ lliures │ ocupacio │ ├─────────┼──────────────┼─────────────────────────────────┼────────────────────┼─────────────┼─────────────┼─────────┼──────────┤ │ 0 │ 'evt-001' │ 'Concierto de Otono' │ 'Teatro Almendra' │ 'ses-001-1' │ '25,00 EUR' │ 240 │ '43%' │ │ 1 │ 'evt-001' │ 'Concierto de Otono' │ 'Teatro Almendra' │ 'ses-001-2' │ '22,00 EUR' │ 324 │ '23%' │ │ 2 │ 'evt-002' │ 'Noche de Monologos' │ 'Sala Boveda' │ 'ses-002-1' │ '18,00 EUR' │ 2 │ '98%' │ │ 3 │ 'evt-002' │ 'Noche de Monologos' │ 'Sala Boveda' │ 'ses-002-2' │ '18,00 EUR' │ 75 │ '38%' │ │ 4 │ 'evt-002' │ 'Noche de Monologos' │ 'Sala Boveda' │ 'ses-002-3' │ '15,00 EUR' │ 108 │ '10%' │ │ 5 │ 'evt-003' │ 'Festival de Jazz de Primavera' │ 'Auditorio Ribera' │ 'ses-003-1' │ '38,00 EUR' │ 260 │ '71%' │ │ 6 │ 'evt-003' │ 'Festival de Jazz de Primavera' │ 'Auditorio Ribera' │ 'ses-003-2' │ '42,00 EUR' │ 180 │ '80%' │ └─────────┴──────────────┴─────────────────────────────────┴────────────────────┴─────────────┴─────────────┴─────────┴──────────┘ 3 esdeveniments | aforament 3000 | venudes 1811 | lliures 1189 | recaptacio 62298,00 EUR
Els totals coincideixen amb la llavor del Mòdul 1: 3000 d'aforament, 1811 venudes, 1189 lliures. La refactorització no ha canviat ni una dada.
I l'important: mira el que ja no hi ha a cataleg.js. No hi ha array de dades, no hi ha formatarPreu duplicat, no hi ha càlculs d'ocupació. Només hi ha lectura d'arguments i presentació. Cada mòdul fa una cosa.
- Bones pràctiques de disseny de mòduls
Les regles que seguirem en tot el curs.
9.1 Una responsabilitat per mòdul
Si en descriure un mòdul has de fer servir «i», probablement en són dos:
| Malament | Bé |
|---|---|
utils.js amb formatatge, validació, dates i càlculs |
format.js, validacio.js, dates.js |
esdeveniment.js que a més llegeix el fitxer de dades |
esdeveniment.js (model) + cataleg-dades.js (accés) |
Un utils.js que creix sense límit és el destí de tot allò que ningú no sap on posar. Quan passi, divideix-lo.
9.2 Exporta poc
L'API pública d'un mòdul és un compromís. Tot el que exportes és alguna cosa que algú pot fer servir i que, per tant, no podràs canviar sense trencar codi aliè.
// MALAMENT: exposa detalls interns que ningu de fora necessita.
module.exports = {
formatarPreu,
partirCentims, // Auxiliar intern
SIMBOL_MONEDA, // Constant interna
cacheDeFormats // Estat intern
};
// BE: nomes el que forma part del contracte.
module.exports = { formatarPreu };Comença exportant el mínim. Ampliar una API és fàcil; reduir-la, no.
9.3 Sense efectes secundaris a la càrrega
Un require hauria de ser barat i segur: definir coses, no fer-les.
// MALAMENT: en requerir aquest modul s'obre una connexio, es llegeix un fitxer
// i s'imprimeix per consola. Sense que ningu ho hagi demanat.
const connexio = connectarABaseDeDades();
const dades = fs.readFileSync('dades/esdeveniments.json');
console.log('Modul de cataleg carregat');
module.exports = { connexio, dades };// BE: el modul defineix capacitats. Qui les vulgui, les invoca.
function connectar(opcions) { /* ... */ }
function carregarDades(ruta) { /* ... */ }
module.exports = { connectar, carregarDades };Un mòdul amb efectes secundaris és impossible de provar en aïllament, fa que l'arrencada sigui lenta i impredictible, i —per la memòria cau— els seus efectes passen un sol cop en un moment que no controles.
L'excepció legítima és el patró require.main === module de l'apartat 4: efectes només quan el fitxer és el programa.
9.4 Situa els require al principi
// Al principi del fitxer, agrupats i ordenats:
const path = require('node:path'); // 1. Nucli de Node
const express = require('express'); // 2. Paquets externs
const { Esdeveniment } = require('./domini'); // 3. Moduls propisAixí, qualsevol que obri el fitxer en veu les dependències en tres segons. Un require amagat enmig d'una funció és una dependència que ningú no trobarà.
9.5 Evita els require condicionals
// MALAMENT: la dependencia nomes es descobreix en temps d'execucio.
if (process.env.MODE === 'produccio') {
registrador = require('./registrador-produccio.js');
}Carrega'ls tots dos i tria, o fes servir una fàbrica. L'única excepció raonable és la càrrega mandrosa d'un mòdul molt pesat que poques vegades es fa servir, i tot i així convé documentar-ho.
9.6 Prefereix fàbriques a instàncies quan hi hagi estat
// Menys flexible: forca un singleton i complica les proves.
module.exports = { gestor: new GestorDeVendes(cataleg) };
// Mes flexible: cadascu crea el seu quan i com vulgui.
module.exports = { GestorDeVendes };Exporta la instància només quan el singleton és deliberat i desitjat: un grup de connexions, un registrador global, una memòria cau compartida.
Errors Comuns i Consells
Error 1: exports = { ... } en lloc de module.exports = { ... }.
El mòdul exporta un objecte buit i qui l'importa rep undefined en tot. Fes servir sempre module.exports.
Error 2: oblidar l'extensió en rutes relatives.
Funciona en CommonJS, però és ambigu i no funcionarà en mòduls ES. Escriu ./sessio.js.
Error 3: fer servir ./ per a un paquet d'npm o a l'inrevés.
require('express') cerca a node_modules; require('./express') cerca un fitxer. Són coses diferents.
Error 4: creure que cada require crea una instància nova.
És un singleton a la memòria cau. Si necessites instàncies independents, exporta la classe o una fàbrica.
Error 5: mutar un objecte exportat per un altre mòdul.
Com que tots comparteixen el mateix objecte, el canvi afecta tota l'aplicació. Object.freeze per a la configuració, i còpies defensives per a les dades.
Error 6: dependències circulars. Un dels dos mòduls rebrà un objecte incomplet i l'error serà incomprensible. Extreu el que és comú a un tercer mòdul o fes servir esdeveniments.
Error 7: require dins d'un bucle o d'una funció calenta.
La memòria cau ho fa barat després de la primera vegada, però la cerca a la memòria cau no és gratis. Al principi del fitxer.
Error 8: un utils.js que ho té tot.
Acaba sent un mòdul del qual depèn tot el projecte i que ningú no gosa tocar.
Consell 1: un module.exports únic al final del fitxer. Aquest és l'índex de l'API pública del teu mòdul.
Consell 2: fes servir node: per als mòduls del nucli. require('node:fs'), require('node:path'), require('node:events').
Consell 3: fes servir __dirname per construir rutes. No depenguis mai de process.cwd().
Consell 4: dibuixa el graf de dependències del teu projecte. Si té cicles o si un mòdul té quinze fletxes entrants, ja saps on és el problema de disseny.
Exercicis
Exercici 1: diagnosticar un mòdul trencat
Aquest mòdul té cinc problemes segons el que s'ha après. Troba'ls, explica la conseqüència de cadascun i reescriu-lo.
// src/utils/ajudes.js
const fs = require('fs');
console.log('Carregant ajudes...');
const cataleg = JSON.parse(fs.readFileSync('./dades/esdeveniments.json', 'utf8'));
var CONFIG = { limit: 10 };
function formatarPreu(c) {
return (c / 100).toFixed(2) + ' EUR';
}
function _arrodonir(n) {
return Math.round(n);
}
exports = { formatarPreu, _arrodonir, CONFIG, cataleg };Exercici 2: el mòdul d'informes
Crea src/informes/ocupacio.js, un mòdul que:
- Depengui només de
../dominii../utils/format.js(no pas decataleg-dades.js: les dades se li passen). - Exporti tres funcions:
resumirPerSala(esdeveniments)→ array de{ sala, esdeveniments, sessions, aforament, venudes, lliures, ocupacio, recaptacioEuros }.sessionsEnRisc(esdeveniments, llindarPercentatge)→ sessions per sota del llindar d'ocupació.sessionsExhaurides(esdeveniments)→ sessions sense entrades lliures.
- Sigui executable directament amb
node src/informes/ocupacio.jsgràcies arequire.main === module, carregant el catàleg i mostrant els tres informes ambconsole.table. - En ser importat amb
require, no imprimeixi absolutament res.
Verifica els dos comportaments: l'execució directa i un fitxer que l'importi i només cridi sessionsExhaurides.
Exercici 3: memòria cau de mòduls i dependència circular
Escriu dos programes de laboratori que demostrin experimentalment el que has après:
Part A — src/laboratori/demostrar-cache.js:
- Un mòdul
src/laboratori/registre-vendes.jsque mantingui un array privat de vendes, exportianotar(venda),total()illistar(), i imprimeixi un missatge en ser avaluat. - Un programa que el requereixi des de tres punts diferents (el mateix programa i dos mòduls auxiliars), anoti vendes des de cadascun i demostri que el total és compartit.
- Que després buidi la memòria cau amb
delete require.cache[require.resolve(...)], el torni a requerir i demostri que l'estat s'ha perdut. - Que imprimeixi quants mòduls hi ha a
require.cacheabans i després.
Part B — src/laboratori/circular-*.js:
Crea una dependència circular real entre un mòdul comanda.js i un mòdul entrada.js (una comanda té entrades; una entrada coneix la seva comanda), demostra la fallada amb una traça de missatges, i després arregla-la amb la tècnica que consideris correcta, justificant-ne l'elecció.
Solucions
Solució 1
Els cinc problemes:
| # | Problema | Conseqüència |
|---|---|---|
| 1 | exports = { ... } en lloc de module.exports |
El mòdul no exporta res. Qui l'importi rep {} |
| 2 | Efecte secundari a la càrrega: llegeix un fitxer i fa console.log |
Requerir el mòdul bloqueja el fil i embruta la sortida sense que ningú ho demani |
| 3 | Ruta relativa a process.cwd() ('./dades/esdeveniments.json') |
Falla si s'executa des d'una altra carpeta. Ha de fer servir __dirname |
| 4 | Exporta detalls interns (_arrodonir, CONFIG, cataleg) |
Amplia el contracte públic amb coses que haurien de ser privades, i CONFIG és mutable globalment |
| 5 | var i require('fs') sense prefix node: |
var no té àmbit de bloc; el prefix evita ambigüitat amb paquets |
I un sisè de propina: el mòdul barreja responsabilitats (formatatge, configuració i accés a dades). Haurien de ser tres mòduls.
Versió corregida, dividida com correspon:
// src/utils/format.js
// Nomes formatatge. Sense dependencies, sense efectes secundaris.
function formatarPreu(centims) {
const euros = Math.floor(centims / 100);
const resta = String(centims % 100).padStart(2, '0');
return `${euros},${resta} EUR`;
}
module.exports = { formatarPreu };// src/config.js
// Configuracio de l'aplicacio. Congelada perque ningu no la modifiqui.
module.exports = Object.freeze({
limitEntradesPerComanda: 10,
llindarAforamentBaix: 0.10
});// src/cataleg-dades.js
// Acces a les dades. La lectura passa quan es DEMANA, no en carregar.
const fs = require('node:fs');
const path = require('node:path');
// Ruta relativa AL FITXER, no al directori de treball.
const RUTA_DADES = path.join(__dirname, '..', 'dades', 'esdeveniments.json');
function obtenirCataleg() {
const contingut = fs.readFileSync(RUTA_DADES, 'utf8');
return JSON.parse(contingut);
}
module.exports = { obtenirCataleg };(La versió asíncrona d'aquesta lectura arriba al Mòdul 3; aquí el que importa és que la lectura és dins d'una funció.)
Solució 2
// src/informes/ocupacio.js
// Informes d'ocupacio d'Escena Viva.
// Rep els esdeveniments com a argument: no en coneix l'origen.
const { formatarPreu } = require('../utils/format.js');
// Resum agregat per sala.
function resumirPerSala(esdeveniments) {
const perSala = new Map();
for (const esdeveniment of esdeveniments) {
const acumulat = perSala.get(esdeveniment.sala) ?? {
sala: esdeveniment.sala, esdeveniments: 0, sessions: 0,
aforament: 0, venudes: 0, recaptacioCentims: 0
};
acumulat.esdeveniments += 1;
acumulat.sessions += esdeveniment.nombreSessions;
acumulat.aforament += esdeveniment.aforamentTotal;
acumulat.venudes += esdeveniment.entradesVenudes;
acumulat.recaptacioCentims += esdeveniment.recaptacioCentims;
perSala.set(esdeveniment.sala, acumulat);
}
return [...perSala.values()].map((a) => ({
sala: a.sala,
esdeveniments: a.esdeveniments,
sessions: a.sessions,
aforament: a.aforament,
venudes: a.venudes,
lliures: a.aforament - a.venudes,
ocupacio: `${Math.round((a.venudes / a.aforament) * 100)}%`,
recaptacioEuros: formatarPreu(a.recaptacioCentims)
}));
}
// Sessions per sota del llindar d'ocupacio.
function sessionsEnRisc(esdeveniments, llindarPercentatge = 20) {
return esdeveniments.flatMap((esdeveniment) =>
esdeveniment.sessions
.filter((sessio) => sessio.ocupacio < llindarPercentatge)
.map((sessio) => ({
esdeveniment: esdeveniment.id,
titol: esdeveniment.titol,
sessio: sessio.id,
lliures: sessio.lliures,
ocupacio: `${sessio.ocupacio}%`
}))
);
}
// Sessions sense entrades disponibles.
function sessionsExhaurides(esdeveniments) {
return esdeveniments.flatMap((esdeveniment) =>
esdeveniment.sessions
.filter((sessio) => sessio.exhaurida)
.map((sessio) => ({
esdeveniment: esdeveniment.id,
titol: esdeveniment.titol,
sessio: sessio.id,
aforament: sessio.aforament,
recaptacioEuros: formatarPreu(sessio.recaptacioCentims)
}))
);
}
// --- Execucio directa: nomes si aquest fitxer ES el programa principal ---
if (require.main === module) {
const { obtenirCataleg } = require('../cataleg-dades.js');
const { Esdeveniment } = require('../domini');
const esdeveniments = obtenirCataleg().map((dades) => Esdeveniment.desDeJSON(dades));
const llindar = Number(process.argv[2]) || 20;
console.log('OCUPACIO PER SALA');
console.table(resumirPerSala(esdeveniments));
console.log('');
console.log(`SESSIONS EN RISC (menys del ${llindar}%)`);
const risc = sessionsEnRisc(esdeveniments, llindar);
if (risc.length === 0) {
console.log(' Cap.');
} else {
console.table(risc);
process.exitCode = 1;
}
console.log('');
console.log('SESSIONS EXHAURIDES');
const exhaurides = sessionsExhaurides(esdeveniments);
if (exhaurides.length === 0) {
console.log(' Cap.');
} else {
console.table(exhaurides);
}
}
module.exports = { resumirPerSala, sessionsEnRisc, sessionsExhaurides };OCUPACIO PER SALA ┌─────────┬────────────────────┬───────────────┬──────────┬───────────┬─────────┬─────────┬──────────┬─────────────────┐ │ (index) │ sala │ esdeveniments │ sessions │ aforament │ venudes │ lliures │ ocupacio │ recaptacioEuros │ ├─────────┼────────────────────┼───────────────┼──────────┼───────────┼─────────┼─────────┼──────────┼─────────────────┤ │ 0 │ 'Teatro Almendra' │ 1 │ 2 │ 840 │ 276 │ 564 │ '33%' │ '6612,00 EUR' │ │ 1 │ 'Sala Boveda' │ 1 │ 3 │ 360 │ 175 │ 185 │ '49%' │ '3114,00 EUR' │ │ 2 │ 'Auditorio Ribera' │ 1 │ 2 │ 1800 │ 1360 │ 440 │ '76%' │ '52572,00 EUR' │ └─────────┴────────────────────┴───────────────┴──────────┴───────────┴─────────┴─────────┴──────────┴─────────────────┘ SESSIONS EN RISC (menys del 20%) ┌─────────┬──────────────┬──────────────────────┬─────────────┬─────────┬──────────┐ │ (index) │ esdeveniment │ titol │ sessio │ lliures │ ocupacio │ ├─────────┼──────────────┼──────────────────────┼─────────────┼─────────┼──────────┤ │ 0 │ 'evt-002' │ 'Noche de Monologos' │ 'ses-002-3' │ 108 │ '10%' │ └─────────┴──────────────┴──────────────────────┴─────────────┴─────────┴──────────┘ SESSIONS EXHAURIDES Cap.
I la comprovació que importar-lo no imprimeix res:
// src/laboratori/provar-informes.js
const { sessionsExhaurides } = require('../informes/ocupacio.js');
const { obtenirCataleg } = require('../cataleg-dades.js');
const { Esdeveniment } = require('../domini');
const esdeveniments = obtenirCataleg().map((d) => Esdeveniment.desDeJSON(d));
console.log(`Exhaurides: ${sessionsExhaurides(esdeveniments).length}`);Només aquesta línia. Ni les taules ni els encapçalaments apareixen, perquè require.main !== module. Aquesta és la diferència entre un mòdul ben dissenyat i un amb efectes secundaris.
Solució 3, part A
// src/laboratori/registre-vendes.js
console.error(">> registre-vendes.js AVALUANT (aixo nomes ha de veure's un cop)");
const vendes = [];
function anotar(venda) {
vendes.push({ ...venda, anotadaEl: new Date().toISOString() });
return vendes.length;
}
function total() {
return vendes.reduce((t, v) => t + v.importCentims, 0);
}
function llistar() {
return [...vendes];
}
module.exports = { anotar, total, llistar };// src/laboratori/taquilla-a.js
const registre = require('./registre-vendes.js');
function vendreDesDeTaquillaA() {
registre.anotar({ sessioId: 'ses-001-1', quantitat: 2, importCentims: 5000 });
}
module.exports = { vendreDesDeTaquillaA };// src/laboratori/taquilla-b.js
const registre = require('./registre-vendes.js');
function vendreDesDeTaquillaB() {
registre.anotar({ sessioId: 'ses-002-2', quantitat: 3, importCentims: 5400 });
}
module.exports = { vendreDesDeTaquillaB };// src/laboratori/demostrar-cache.js
console.error(`Moduls a la cache en comencar: ${Object.keys(require.cache).length}`);
const registre = require('./registre-vendes.js');
const { vendreDesDeTaquillaA } = require('./taquilla-a.js');
const { vendreDesDeTaquillaB } = require('./taquilla-b.js');
console.error(`Moduls a la cache despres dels require: ${Object.keys(require.cache).length}`);
console.error('');
// Vendes des de tres punts diferents del programa.
registre.anotar({ sessioId: 'ses-003-1', quantitat: 1, importCentims: 3800 });
vendreDesDeTaquillaA();
vendreDesDeTaquillaB();
console.log('--- Estat compartit ---');
console.log(`Vendes anotades: ${registre.llistar().length}`);
console.log(`Total: ${(registre.total() / 100).toFixed(2)} EUR`);
// Comprovacio d'identitat.
const altraReferencia = require('./registre-vendes.js');
console.log(`Mateix objecte? ${registre === altraReferencia}`);
console.log(`Total vist des de l'altra referencia: ${(altraReferencia.total() / 100).toFixed(2)} EUR`);
// --- Buidatge de la cache ---
console.log('');
console.log('--- Despres de buidar la cache ---');
const ruta = require.resolve('./registre-vendes.js');
delete require.cache[ruta];
const registreNou = require('./registre-vendes.js'); // Es torna a avaluar
console.log(`Mateix objecte que abans? ${registre === registreNou}`);
console.log(`Vendes a la instancia nova: ${registreNou.llistar().length}`);
console.log(`Vendes a la instancia antiga: ${registre.llistar().length}`);
console.error('');
console.error(`Moduls a la cache en acabar: ${Object.keys(require.cache).length}`);Moduls a la cache en comencar: 1 >> registre-vendes.js AVALUANT (aixo nomes ha de veure's un cop) Moduls a la cache despres dels require: 4 --- Estat compartit --- Vendes anotades: 3 Total: 142.00 EUR Mateix objecte? true Total vist des de l'altra referencia: 142.00 EUR --- Despres de buidar la cache --- >> registre-vendes.js AVALUANT (aixo nomes ha de veure's un cop) Mateix objecte que abans? false Vendes a la instancia nova: 0 Vendes a la instancia antiga: 3 Moduls a la cache en acabar: 4
Tres conclusions que es llegeixen directament a la sortida:
- El missatge d'avaluació apareix un sol cop malgrat els quatre
require, i les tres vendes anotades des de punts diferents sumen al mateix array. És el singleton. - Després de buidar la memòria cau, el mòdul es torna a avaluar i la instància nova comença de zero, mentre que l'antiga conserva el seu estat. Coexisteixen dues còpies del mateix mòdul, cadascuna amb les seves dades.
- Aquesta última frase és exactament la raó per la qual manipular
require.cacheen producció és perillós: pots acabar amb dos «registres de vendes» i vendes repartides entre tots dos sense que ningú se n'adoni.
Solució 3, part B
La dependència circular:
// src/laboratori/circular-comanda.js
console.error('comanda.js: comenca');
const { Entrada } = require('./circular-entrada.js');
console.error('comanda.js: Entrada val', typeof Entrada);
class Comanda {
constructor(id, sessioId, quantitat) {
this.id = id;
this.entrades = [];
for (let i = 0; i < quantitat; i++) {
this.entrades.push(new Entrada(`EV-2026-00000${i + 1}`, sessioId, this));
}
}
}
module.exports = { Comanda };
console.error('comanda.js: acaba');// src/laboratori/circular-entrada.js
console.error('entrada.js: comenca');
const { Comanda } = require('./circular-comanda.js');
console.error('entrada.js: Comanda val', typeof Comanda); // <-- undefined
class Entrada {
constructor(codi, sessioId, comanda) {
this.codi = codi;
this.sessioId = sessioId;
this.comanda = comanda;
this.estat = 'valida';
}
// Fa servir Comanda per validar: fallara si Comanda es undefined.
pertanyAComandaValida() {
return this.comanda instanceof Comanda;
}
}
module.exports = { Entrada };
console.error('entrada.js: acaba');comanda.js: comenca entrada.js: comenca entrada.js: Comanda val undefined <-- el simptoma entrada.js: acaba comanda.js: Entrada val function comanda.js: acaba
I en fer-lo servir: TypeError: Right-hand side of 'instanceof' is not callable.
L'arranjament triat: invertir la dependència.
// src/domini/entrada.js
// Entrada NO necessita coneixer la classe Comanda: en te prou amb el seu identificador.
class Entrada {
constructor({ codi, sessioId, comandaId, estat = 'valida' }) {
this.codi = codi;
this.sessioId = sessioId;
this.comandaId = comandaId; // Nomes l'id, no pas l'objecte
this.estat = estat;
}
usar() {
if (this.estat !== 'valida') {
const error = new Error(`L'entrada ${this.codi} esta ${this.estat}`);
error.codi = 'ENTRADA_NO_VALIDA';
throw error;
}
this.estat = 'usada';
return this;
}
anullar() {
this.estat = 'anullada';
return this;
}
}
module.exports = { Entrada };// src/domini/comanda.js
// Comanda coneix Entrada. Entrada no coneix Comanda. Sense cicle.
const { Entrada } = require('./entrada.js');
const { generarCodiEntrada } = require('../utils/format.js');
class Comanda {
#entrades = [];
constructor({ id, usuariId, sessioId, quantitat, preuCentims, estat = 'pendent' }) {
this.id = id;
this.usuariId = usuariId;
this.sessioId = sessioId;
this.quantitat = quantitat;
this.totalCentims = quantitat * preuCentims;
this.estat = estat;
}
get entrades() {
return [...this.#entrades];
}
emetre(any, sequenciaInicial) {
if (this.estat !== 'pagat') {
const error = new Error(`No es poden emetre entrades d'una comanda ${this.estat}`);
error.codi = 'ESTAT_INVALID';
throw error;
}
for (let i = 0; i < this.quantitat; i++) {
this.#entrades.push(new Entrada({
codi: generarCodiEntrada(any, sequenciaInicial + i),
sessioId: this.sessioId,
comandaId: this.id
}));
}
this.estat = 'emes';
return this.entrades;
}
}
module.exports = { Comanda };Justificació de l'elecció. De les quatre tècniques de l'apartat 7, invertir la dependència és la correcta aquí perquè el cicle era un símptoma d'un modelatge incorrecte, no pas un problema tècnic: una entrada no necessita l'objecte Comanda complet, només el seu identificador (comandaId), que és exactament el que ja defineix el model de domini del Mòdul 1. Moure el require dins del mètode hauria funcionat, però hauria deixat l'acoblament intacte i amagat; extreure a un tercer mòdul hi hauria afegit un fitxer sense necessitat. El cicle va desaparèixer perquè el disseny va millorar, que és sempre la millor solució possible a una dependència circular.
Conclusió
Has tancat el deute que arrossegàvem des de la tercera lliçó del curs. Ara saps que cada fitxer de Node és un mòdul amb àmbit propi, i que aquesta privacitat per defecte —la que permet canviar els detalls interns sense por— s'aconsegueix amb un truc molt concret: l'embolcall de mòdul, una funció que Node afegeix al voltant del teu codi i que li injecta cinc paràmetres: exports, require, module, __filename i __dirname.
Entens amb precisió el parany clàssic: exports i module.exports apunten al mateix objecte al principi, així que afegir propietats a qualsevol dels dos funciona, però reassignar exports trenca la connexió i deixa el mòdul exportant un objecte buit, perquè require torna module.exports. D'aquí la regla que seguirem sempre: un únic module.exports = { ... } al final del fitxer, que a més funciona com a índex de l'API pública.
Coneixes l'algorisme de resolució: primer mòduls del nucli (millor amb el prefix node:), després rutes relatives o absolutes amb la seva cascada d'extensions i la seva interpretació com a carpeta amb index.js, i finalment la cerca ascendent a node_modules. I saps que un mòdul s'avalua un sol cop: és un singleton desat a la memòria cau per ruta resolta, cosa que és perfecta per a un grup de connexions o un registrador, i perillosa per a configuració mutable o per a proves que es contaminen entre si. Saps també què torna Node davant d'una dependència circular —un objecte incomplet al segon mòdul— i que la solució gairebé sempre és de disseny, no pas de sintaxi.
I Escena Viva ha deixat de ser un munt de fitxers amb dades copiades. Ara té src/utils/format.js amb les funcions de presentació que estaven duplicades, src/domini/sessio.js i src/domini/esdeveniment.js amb les classes del Mòdul 1 —i amb Esdeveniment delegant la validació d'aforament en Sessio, que és qui la sap fer—, src/domini/gestor-vendes.js de la lliçó anterior, src/domini/index.js com a façana, src/cataleg-dades.js amb module.exports i una signatura preparada per tornar-se asíncrona al Mòdul 3, i un src/cataleg.js que ja només llegeix arguments i presenta. Els totals continuen quadrant: 3000 d'aforament, 1811 venudes, 1189 lliures.
Queda una última peça del mòdul, i és una que canvia el paisatge. Tot el que has après avui —require, module.exports, la memòria cau, l'embolcall— és CommonJS, el sistema propi de Node. Però JavaScript va acabar tenint un sistema de mòduls estàndard, definit al llenguatge i compartit amb el navegador: import i export. No és una sintaxi alternativa per al mateix: és un model estàtic, que es resol abans d'executar res, amb regles diferents sobre extensions, sense __dirname, sense require… i amb await de nivell superior. A la lliçó següent, Mòduls ES i Interoperabilitat, veuràs les dues direccions de la convivència entre tots dos sistemes, els seus límits reals, i escriuràs la versió ESM del domini d'Escena Viva costat a costat amb la que acabes d'enllestir.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
