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

  1. Per què existeixen els mòduls
  2. El sistema CommonJS: require i module.exports
  3. exports contra module.exports
  4. L'embolcall de mòdul i les seves cinc variables
  5. Tipus de mòdul i algorisme de resolució de require
  6. La memòria cau de mòduls: un mòdul és un singleton
  7. Dependències circulars
  8. Refactorització d'Escena Viva
  9. Bones pràctiques de disseny de mòduls

  1. 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 aqui

Aquesta 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.

  1. El sistema CommonJS: require i module.exports

CommonJS 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:

  1. És ampliable. Afegir una segona exportació no trenca ningú.
  2. El nom viatja amb el valor. const { GestorDeVendes } = require(...) deixa clar què és, mentre que const X = require(...) depèn que qui importa triï bé el nom.
  3. És coherent amb els mòduls ES, que veurem a la lliçó següent.

  1. exports contra module.exports

Aquí 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 objecte
flowchart 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:

// MALAMENT: aixo NO exporta res.
exports = { formatarPreu, formatarData };
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);     // undefined

La 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.

  1. 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:

const cataleg = [];
module.exports = { cataleg };

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, module i exports: són paràmetres d'aquesta funció, no pas variables globals.
  • Per què this al nivell superior d'un mòdul CommonJS és module.exports (un objecte buit), i no global.

Ho pots veure amb els teus propis ulls:

// src/laboratori/embolcall.js
console.log(require('node:module').wrapper);
[
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
]

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ú.

  1. Tipus de mòdul i algorisme de resolució de require

Quan 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 de require('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.json

Aquest 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 pas require('./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/express

Pots veure aquesta llista en temps real:

console.log(module.paths);

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:

  1. És síncron: bloqueja el fil principal mentre llegeix el fitxer.
  2. Es posa a la memòria cau: si el fitxer canvia al disc, require continua tornant la versió antiga.
  3. 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.

  1. 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 seu module.exports es 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:

  1. El missatge d'avaluació apareix un sol cop, encara que hi va haver tres require.
  2. Els tres objectes són idèntics (===).
  3. 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'aplicacio

Remei: congelar allò que ha de ser immutable.

module.exports = Object.freeze({ limitEntradesPerComanda: 10 });

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 = 0

Manipular 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.

  1. 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");
node src/laboratori/circular-a.js
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-se

L'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 function

Com 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.

  1. 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.js
flowchart 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:

  1. 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 amb structuredClone protegeix la font.
  2. La signatura serà la mateixa quan arribin les dades reals. Al Mòdul 3, obtenirCataleg() llegirà dades/esdeveniments.json amb fs i passarà a ser asíncrona. Cap consumidor no haurà de canviar la seva estructura, només afegir un await. 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ó:

node src/cataleg.js --taula
┌─────────┬──────────────┬─────────────────────────────────┬────────────────────┬─────────────┬─────────────┬─────────┬──────────┐
│ (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.

  1. 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 propis

Així, 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:

  1. Depengui només de ../domini i ../utils/format.js (no pas de cataleg-dades.js: les dades se li passen).
  2. 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.
  3. Sigui executable directament amb node src/informes/ocupacio.js gràcies a require.main === module, carregant el catàleg i mostrant els tres informes amb console.table.
  4. 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:

  1. Un mòdul src/laboratori/registre-vendes.js que mantingui un array privat de vendes, exporti anotar(venda), total() i llistar(), i imprimeixi un missatge en ser avaluat.
  2. 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.
  3. 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.
  4. Que imprimeixi quants mòduls hi ha a require.cache abans 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 };
node src/informes/ocupacio.js
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}`);
node src/laboratori/provar-informes.js
# Exhaurides: 0

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.cache en 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');
node src/laboratori/circular-comanda.js
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

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats