Tens el pla, el model de dades, les regles R1–R15 i un repositori verd amb la pàgina buida. Ara cal omplir-la, i la pregunta que decideix si el projecte avança o s'encalla no és «què escric?», sinó «per on començo?». La resposta intuïtiva —obrir index.html i maquetar la pantalla, perquè és l'única cosa que es veu— és exactament l'equivocada, i és la raó per la qual tants projectes personals tenen una interfície preciosa a sobre d'una lògica que no es pot provar ni canviar. En aquesta lliçó aprendràs l'ordre que funciona: de dins cap a fora —domini, dades, vista, aplicació— i vertical abans que horitzontal, una funcionalitat completa de punta a punta abans que totes les capes a mitges. Veuràs com construir el domini amb TDD sobre les regles noves, com resoldre l'arbre de subtasques amb la recursivitat de 03-07, com muntar una capa de dades en memòria que no et bloquegi i que deixi la frontera llesta per a la lliçó següent, com escriure components de vista sense framework amb el cicle estat → render → esdeveniment, com governar l'estat amb una única font de veritat i actualitzacions immutables, com fer accessible l'aplicació des del primer commit en lloc de al final, i com gestionar i registrar els errors de tot el sistema. I acabaràs amb dues coses que no són codi però decideixen el ritme del projecte: una llista de comprovació de qualitat per tancar cada increment, i un mètode concret per quan t'encalles — perquè t'encallaràs.
Contingut
- De dins cap a fora: per què aquest ordre
- Vertical abans que horitzontal
- L'ordre de treball recomanat
- El domini: entitats i invariants
- TDD sobre les regles R11 a R15
- L'arbre de subtasques amb recursivitat
- La capa de dades: el repositori en memòria primer
- El contracte del repositori com a frontera
- L'estat de l'aplicació: una única font de veritat
- Actualitzacions immutables i esdeveniments
- La vista sense framework: estat → render → esdeveniment
- Delegació d'esdeveniments i el controlador
- El formulari accessible
- Les tres pantalles noves
- Accessibilitat des del principi
- Registre i gestió d'errors de tota l'aplicació
- Increments verificables i commits petits
- Quan refactoritzar i com no trencar res
- La llista de comprovació abans de tancar un increment
- Què fer quan t'encalles
- Errors Habituals i Consells
- Exercicis
- Conclusió
- De dins cap a fora: per què aquest ordre
Hi ha dues maneres de construir una aplicació, i l'elecció determina gairebé tota la resta.
De fora cap a dins és la intuïtiva: es comença per l'HTML i el CSS, perquè és el que es veu i el que dona sensació d'avenç. Quan la pantalla és bonica, s'hi afegeix JavaScript perquè els botons facin alguna cosa. Quan els botons fan alguna cosa, es desa a localStorage. I la lògica de negoci acaba repartida entre els gestors d'esdeveniments, perquè és on es necessitava.
De dins cap a fora és la que faràs servir: es comença pel domini —les entitats i les regles—, després les dades, després la vista, i per últim l'aplicació que ho uneix. Durant els primers dies no hi ha res per ensenyar al navegador, i això incomoda.
La comparació honesta:
| Aspecte | De fora cap a dins | De dins cap a fora |
|---|---|---|
| Sensació d'avenç inicial | Alta: es veu alguna cosa de seguida | Baixa: només hi ha proves en verd |
| On acaba la lògica de negoci | Repartida entre gestors d'esdeveniments | Concentrada a domini/ |
| Es pot provar sense navegador | No: tot necessita DOM | Sí: el domini es prova a Node en mil·lisegons |
| Cost de canviar la interfície | Alt: la lògica se'n va amb ella | Baix: només canvia vista/ |
| On apareixen les errades | Tard, a la interfície, difícils d'aïllar | Aviat, en proves unitàries, amb causa evident |
| Quan es descobreix que el model estava malament | A la setmana 5 | El dia 2 |
L'última fila és la decisiva. Els errors de model són els més cars que existeixen, perquè contaminen tot el que s'hi recolza. Si descobreixes el dia 2 que horesEstimades no pot ser un camp editable en una tasca amb filles (R12), canvies tres funcions. Si ho descobreixes a la setmana 5, canvies tres funcions, dues vistes, un formulari, l'informe, la persistència i les dades que ja van desar els teus usuaris de prova.
I hi ha un argument addicional del mateix curs: a 10-06 va quedar demostrat que domini/regles.js va ser idèntic a les quatre versions de la mateixa pantalla. El domini és la part del projecte amb més valor i més vida útil. Construir-lo primer és construir primer el que més dura.
flowchart LR
A["1 · domini/<br/>entitats + R1-R15<br/><i>es prova a Node</i>"] --> B["2 · dades/<br/>repositori en memoria<br/><i>frontera definida</i>"]
B --> C["3 · vista/<br/>components + esdeveniments<br/><i>rep estat i funcions</i>"]
C --> D["4 · aplicacio/<br/>casos d'us + estat<br/><i>ho uneix tot</i>"]
D -.->|"i nomes llavors"| E["Navegador<br/>funcionant"]
style A fill:#dcfce7,stroke:#16a34a
style B fill:#dbeafe,stroke:#2563eb
style C fill:#fef3c7,stroke:#d97706
style D fill:#f3e8ff,stroke:#9333ea
L'objecció raonable, i la seva resposta. «Si no veig res durant tres dies, perdo la motivació». És un problema real i té dues solucions concretes:
- Les proves en verd són la teva pantalla. Veure
47 passedamb les regles del domini cobertes és exactament el mateix senyal d'avenç que veure una llista pintada. Canvia el que compta com a progrés. - L'apartat 2 escurça aquests dies dràsticament. No construeixes tot el domini abans de tocar la vista: construeixes el tall del domini que necessita la primera funcionalitat, i surts al navegador amb ell.
- Vertical abans que horitzontal
Aquest és el segon principi, i corregeix el perill del primer.
- Horitzontal vol dir acabar una capa sencera abans de passar a la següent: tot el domini, després totes les dades, després tota la vista.
- Vertical vol dir acabar una funcionalitat completa travessant les quatre capes abans de començar la següent.
flowchart TB
subgraph H["❌ Horitzontal: 3 setmanes sense res per ensenyar"]
direction LR
H1["Tot el domini"] --> H2["Totes les dades"] --> H3["Tota la vista"] --> H4["Funciona?<br/>Es descobreix aqui"]
end
subgraph V["✅ Vertical: alguna cosa funciona el dia 3"]
direction LR
V1["Veure el tauler<br/>domini→dades→vista→app"] --> V2["Crear tasca<br/>domini→dades→vista→app"] --> V3["Subtasques<br/>domini→dades→vista→app"]
end
style H fill:#fee2e2,stroke:#b91c1c
style V fill:#dcfce7,stroke:#16a34a
Per què vertical guanya, amb tres raons que no són d'estil:
1 · Valida l'arquitectura aviat. El primer tall vertical és el que revela si les teves fronteres funcionen. Si en pintar la primera llista descobreixes que necessites importar dades/ des de vista/, tens un problema de disseny — i el tens el dia 3, quan canviar-lo costa una hora.
2 · Produeix alguna cosa lliurable en tot moment. Al final de cada tall, l'aplicació funciona. Amb menys funcionalitats, però funciona. Si el projecte s'interrompés demà, tindries un producte petit en lloc de tres capes incompletes que no arrenquen.
3 · Descobreix els requisits ocults. Cada tall complet treu a la llum coses que el pla no contemplava: què passa si la llista és buida, on va el focus després de crear una tasca, què es mostra mentre carrega. Descobrir-les d'una en una és manejable; descobrir-ne trenta alhora a la setmana 6 és aclaparador.
El primer tall vertical d'Òrbita —el que cal fer literalment primer— és aquest:
Veure el tauler amb les tasques d'exemple. Sense crear, sense editar, sense filtrar. Només: les dades de llavor arriben del repositori en memòria, es construeixen entitats del domini, i es pinten en una llista accessible.
Sembla poc. Travessa les quatre capes, obliga a definir el contracte del repositori, el format de l'estat, l'estructura de la vista i el punt d'entrada. Quan aquest tall funciona, l'esquelet del projecte sencer està decidit i provat.
- L'ordre de treball recomanat
Combinant els dos principis, aquest és l'ordre concret de les fites H2 i H3, amb les hores estimades del pla de 11-01:
| # | Increment | Capes que toca | Est. | Com saps que està fet |
|---|---|---|---|---|
| 1 | Entitats base amb les seves invariants (Tasca, Usuari) |
domini | 6 h | Proves de R1–R5, R8, R9, R11 en verd |
| 2 | Transicions d'estat (R6) i venciment (R10) | domini | 3 h | Matriu de transicions provada, vàlides i no vàlides |
| 3 | Repositori en memòria + llavor fictícia | dades | 3 h | Es pot llistar, afegir i cercar des d'una prova |
| 4 | Tall vertical 1: veure el tauler | les 4 | 8 h | Es veu la llista al navegador, accessible |
| 5 | Tall vertical 2: crear una tasca | les 4 | 8 h | Formulari amb validació real del domini |
| 6 | Tall vertical 3: canviar d'estat | les 4 | 5 h | R6 aplicada des de la interfície, amb error visible |
| 7 | Arbre de subtasques (R12) | domini | 7 h | Cicles detectats, hores per fulles, profunditat limitada |
| 8 | Tall vertical 4: subtasques a la interfície (R13) | les 4 | 7 h | Es crea, es veu imbricada, no es pot tancar la mare |
| 9 | Assignació de responsable (R11, R15) | domini + vista | 6 h | Usuari inactiu rebutjat; convidat en només lectura |
| 10 | Editar i esborrar amb historial (R14) | les 4 | 6 h | Cada canvi genera entrada immutable |
Fixa't en el patró: tres increments de domini pur al principi (els tres primers, 12 hores) i a partir d'aquí talls verticals. Aquest arrencament de domini no és horitzontalisme: és la porció mínima sense la qual el primer tall no té què pintar.
I fixa't en l'increment 7, que és de domini pur enmig dels talls. És correcte: quan una funcionalitat té lògica de negoci complexa —l'arbre— convé resoldre-la i provar-la al domini abans d'exposar-la. Depurar un cicle en un arbre a través de la interfície és un malson; depurar-lo en una prova unitària és trivial.
- El domini: entitats i invariants
Un invariant és una afirmació sobre un objecte que és certa sempre, des que es crea fins que es destrueix. «Una tasca sempre té títol no buit» és un invariant. «Una tasca de vegades té títol» no ho és.
La idea que fa que el domini valgui la pena és aquesta:
Si un objecte no pot existir en estat invàlid, la meitat dels teus errors no poden ocórrer.
Això s'aconsegueix amb dues tècniques que ja coneixes de 05-02 i 05-03: validar al constructor i encapsular l'estat mutable.
4.1 L'estructura d'una entitat
Aquest és l'esquelet de src/domini/tasca.js. No és el codi complet: és el contracte que has d'omplir, amb els punts de decisió marcats.
// src/domini/tasca.js
import { ErrorDeValidacio, ErrorDeRegla } from './errors.js';
import { TRANSICIONS, PRIORITATS, MAX_HORES } from './regles.js';
export class Tasca {
#estat; // R5, R6: nomes canvia per canviarEstat()
#hores; // R12: derivat si hi ha subtasques
constructor({ id, titol, prioritat, horesEstimades, /* … */ }) {
// 1 · Validar-ho TOT abans d'assignar res.
// Un objecte a mig construir es un objecte invalid.
// 2 · Normalitzar: trim del titol, etiquetes a minuscules i sense
// duplicats (R9), responsable '' -> null (R8).
// 3 · Congelar el que no ha de canviar: Object.freeze sobre
// etiquetes, id de nomes lectura.
// 4 · R5: l'estat inicial es SEMPRE 'pendent'; no s'accepta
// per parametre excepte a desDeJSON().
}
get estat() { return this.#estat; }
canviarEstat(nou, { subtasques = [] } = {}) {
// R6: la transicio es a TRANSICIONS[actual]?
// R13: si nou === 'feta' i alguna subtasca no esta feta -> ErrorDeRegla
// Retorna el canvi realitzat perque l'aplicacio registri historial (R14)
}
estaVencuda(avui) {
// R10. COMPTE: 'avui' es PASSA com a parametre, no es llegeix de Date.now().
// Es el que fa que la prova no depengui del dia en que s'executi.
}
toJSON() { /* … */ }
static desDeJSON(pla) { /* … reconstrueix validant … */ }
}Cinc decisions incrustades en aquest esquelet que convé entendre bé:
1 · Validar-ho tot abans d'assignar res. Si valides i assignes alternant, un error a mitges deixa l'objecte mig construït. En JavaScript el constructor que llança no retorna objecte, així que tant és a la pràctica… excepte si el constructor ja ha modificat alguna cosa externa (un comptador d'ids, per exemple). Valida primer, sempre.
2 · Normalitzar a la frontera. El títol arriba amb espais, les etiquetes amb majúscules, el responsable com a ''. El domini els normalitza una vegada, en entrar, i a partir d'aquí tot el codi pot confiar en el format. És la mateixa idea que R8 i R9 defensaven des del Mòdul 1.
3 · Congelar l'immutable. Object.freeze(this.etiquetes) evita que algú faci tasca.etiquetes.push('X') saltant-se R9. És barat i tanca una classe sencera d'errors.
4 · avui com a paràmetre. Aquesta és la decisió que més agrairàs a 11-04. Si estaVencuda() crida Date.now(), la prova «una tasca del 5 de setembre està vençuda» funciona avui i falla el dia que canviïs la dada d'exemple. Passant avui, la prova és determinista per sempre. La mateixa tècnica serveix per al generador d'ids i per a qualsevol font de no determinisme: s'injecta, no s'invoca.
5 · canviarEstat retorna el canvi. No retorna void ni this: retorna un objecte { camp, abans, despres } que la capa d'aplicació farà servir per construir l'entrada d'historial (R14). Així el domini no necessita saber que existeix un historial, i l'historial no necessita saber com es calcula un canvi.
4.2 regles.js: les regles en un sol lloc
// src/domini/regles.js — l'unica font de veritat de les regles
export const ESTATS = Object.freeze(['pendent', 'en-curs', 'feta']);
export const PRIORITATS = Object.freeze(['alta', 'mitjana', 'baixa']);
export const ROLS = Object.freeze(['coordinacio', 'equip', 'convidat']);
// R6: matriu de transicions valides
export const TRANSICIONS = Object.freeze({
'pendent': ['en-curs'],
'en-curs': ['feta', 'pendent'],
'feta': ['en-curs']
});
export const MAX_HORES = 40; // R3
export const MAX_HORES_SETMANA = 40; // R7, R15
export const PROFUNDITAT_MAXIMA = 3; // R12
// R15: qui pot escriure
export const POT_ESCRIURE = Object.freeze({
coordinacio: true, equip: true, convidat: false
});Tenir les regles en constants amb nom, en un únic fitxer, produeix tres beneficis immediats:
- Es llegeixen com a documentació. Algú que obri
regles.jsentén el negoci en dos minuts. - Canviar-les és canviar una línia. Si el màxim passa de 40 a 45 hores, hi ha un sol lloc.
- Les proves les poden importar. I això evita l'error clàssic d'escriure
40a mà a la prova: si la regla canvia, la prova canvia sola amb ella… la qual cosa és bona per als límits i dolenta per als valors d'exemple. Sigues conscient de quin estàs fent servir en cada cas.
- TDD sobre les regles R11 a R15
TDD (Test-Driven Development, desenvolupament guiat per proves) és el cicle que 08-03 va introduir: vermell → verd → refactoritzar. Escrius una prova que falla, escrius el codi mínim que la fa passar, i després netges.
No tot el projecte es fa amb TDD, i dir-ho és més honest que fingir el contrari. Però hi ha una part on compensa moltíssim:
| On | TDD? | Per què |
|---|---|---|
| Regles de negoci del domini | Sí, sempre | Els criteris ja estan escrits com a Donat/Quan/Llavors: la prova és una traducció, no una invenció |
| Càlculs (càrrega, progrés, arbre) | Sí | Entrada i sortida clares, casos límit evidents |
| Repositoris | De vegades | La prova de contracte (apartat 8) sí; els detalls no |
| Vista i maquetació | No | No saps com serà fins que la veus; es prova després |
| Exploració d'una API nova | No | Primer entens, després proves el que has entès |
5.1 El cicle aplicat a R13
Aquest és un cicle complet, tal com l'has d'executar. Vermell primer:
// test/domini/r13-tancament.test.js
import { Tasca } from '../../src/domini/tasca.js';
import { ErrorDeRegla } from '../../src/domini/errors.js';
describe('R13 · no es tanca una tasca amb subtasques obertes', () => {
test('rebutja passar a feta si una subtasca continua pendent', () => {
const mare = crearTascaEnCurs();
const subtasques = [tascaFeta(), tascaPendent({ titol: 'Mesurar el buit' })];
expect(() => mare.canviarEstat('feta', { subtasques }))
.toThrow(ErrorDeRegla);
});
test('el missatge de l error anomena la subtasca que ho impedeix', () => {
const mare = crearTascaEnCurs();
const subtasques = [tascaPendent({ titol: 'Mesurar el buit' })];
expect(() => mare.canviarEstat('feta', { subtasques }))
.toThrow(/Mesurar el buit/);
});
test('permet tancar quan totes les subtasques estan fetes', () => {
const mare = crearTascaEnCurs();
const canvi = mare.canviarEstat('feta', { subtasques: [tascaFeta(), tascaFeta()] });
expect(mare.estat).toBe('feta');
expect(canvi).toEqual({ camp: 'estat', abans: 'en-curs', despres: 'feta' });
});
test('permet tancar una tasca sense subtasques', () => {
const fulla = crearTascaEnCurs();
fulla.canviarEstat('feta');
expect(fulla.estat).toBe('feta');
});
});Quatre proves, i cap no és redundant:
- La primera comprova que falla.
- La segona comprova que el missatge serveix a l'usuari. Un
ErrorDeReglaamb el text «Operació no permesa» és inútil a la interfície. Provar el missatge obliga a escriure'l bé. - La tercera comprova el camí feliç i, de passada, l'objecte de canvi que necessita R14.
- La quarta comprova el cas degenerat: sense subtasques, la regla no ha de destorbar. És el cas que més sovint es trenca en implementar la regla amb massa entusiasme.
Les funcions auxiliars (crearTascaEnCurs, tascaFeta, tascaPendent) són el secret d'una suite llegible. Construir una Tasca vàlida requereix sis camps; repetir-los en quaranta proves fa que no se'n llegeixi cap. Escriu un fitxer test/ajudes/fabriques.js amb constructors que rebin només el que la prova vol destacar i omplin la resta amb valors per defecte vàlids. És mitja hora de feina que s'amortitza a la tercera prova.
5.2 Proves parametritzades per a les transicions
R6 té 3 estats × 3 destins = 9 combinacions, de les quals 4 són vàlides. Escriure nou proves gairebé idèntiques és soroll; test.each de Jest ho resol:
describe.each([
['pendent', 'en-curs', true],
['pendent', 'feta', false],
['pendent', 'pendent', false],
['en-curs', 'feta', true],
['en-curs', 'pendent', true],
['en-curs', 'en-curs', false],
['feta', 'en-curs', true],
['feta', 'pendent', false],
['feta', 'feta', false]
])('R6 · de %s a %s', (des, fins, permesa) => {
test(permesa ? 'es permet' : 'es rebutja', () => {
const tasca = tascaEnEstat(des);
if (permesa) {
tasca.canviarEstat(fins);
expect(tasca.estat).toBe(fins);
} else {
expect(() => tasca.canviarEstat(fins)).toThrow(ErrorDeRegla);
}
});
});La taula és l'especificació. Si algú pregunta quines transicions són vàlides, aquestes nou línies ho responen millor que qualsevol document, i a més estan verificades.
Consell important sobre la cobertura de regles: per a cada regla, escriu sempre almenys el cas que sí que passa, el cas que no passa, i el cas límit exacte. Per a R3 (horesEstimades de 0 a 40): 0 rebutjat, 0.5 acceptat, 40 acceptat, 40.1 rebutjat. Les errades viuen als marges, no al mig.
- L'arbre de subtasques amb recursivitat
Aquesta és la part del domini amb més substància tècnica i la que reprèn directament la lliçó 03-07. Recorda la decisió de 11-01: l'arbre es desa pla (cada tasca amb tascaMareId) i es construeix en memòria quan cal.
6.1 Construir l'arbre des de la llista plana
// src/domini/arbre.js
export function construirArbre(tasques) {
// 1 · Un index per id per no cercar en bucle: O(n) en comptes de O(n²)
const perId = new Map(tasques.map((t) => [t.id, { tasca: t, filles: [] }]));
const arrels = [];
// 2 · Enganxar cada node a la seva mare, o a les arrels
for (const node of perId.values()) {
const mareId = node.tasca.tascaMareId;
if (mareId === null) { arrels.push(node); continue; }
const mare = perId.get(mareId);
if (!mare) throw new ErrorDeDades(`Tasca ${node.tasca.id}: mare ${mareId} inexistent`);
mare.filles.push(node);
}
return arrels;
}Dos detalls que separen una implementació correcta d'una que funciona per casualitat:
- El
Mapper id converteix la construcció en una sola passada. Ambtasques.find(...)dins del bucle, el cost seria quadràtic: irrellevant amb 6 tasques, perceptible amb 600 (que és exactament l'escenari de 09-01). - La mare inexistent llança
ErrorDeDades, no s'ignora. Si una dada desada apunta a una mare esborrada, vols assabentar-te'n, no que la tasca desaparegui en silenci de la interfície. Aquest cas passarà de debò a la lliçó 11-03, quan migris dades.
6.2 Les quatre funcions recursives que necessites
| Funció | Què retorna | Cas base | Regla |
|---|---|---|---|
profunditat(node) |
Nivells sota el node | Sense filles → 1 | R12 (màx. 3) |
fulles(node) |
Totes les tasques sense filles del subarbre | Sense filles → [tasca] |
R12 (hores per fulles) |
horesTotals(node) |
Suma d'hores de les fulles | Sense filles → tasca.horesEstimades |
R12 |
esDescendent(node, id) |
true si id és al subarbre |
Sense filles → false |
R12 (sense cicles) |
I la detecció de cicles, que és la regla que més gent implementa malament:
// R12: vincular una subtasca sense crear cicles
export function potVincular(nodeMare, idFilla, arbre) {
if (nodeMare.tasca.id === idFilla) return false; // no es la seva propia mare
if (esDescendent(cercarNode(arbre, idFilla), nodeMare.tasca.id)) return false; // cicle
if (profunditatDesDeArrel(nodeMare, arbre) + profunditat(cercarNode(arbre, idFilla)) > PROFUNDITAT_MAXIMA) return false;
return true;
}Les tres condicions són diferents i cal provar-les per separat:
- Autoreferència: A no pot ser mare d'A. És el cas trivial i l'únic que tothom comprova.
- Cicle indirecte: si A és mare de B i B de C, C no pot ser mare d'A. És el que s'oblida, i produeix un desbordament de pila així que algú pinta l'arbre.
- Profunditat: vincular un subarbre de 2 nivells sota un node que ja és al nivell 2 donaria 4. Aquest també s'oblida, perquè la comprovació ingènua només mira el node, no el subarbre sencer.
Compte amb la recursivitat i la pila. Amb profunditat màxima 3 no hi ha cap risc. Però si el teu domini permet arbres arbitràriament profunds (una categoria d'inventari, per exemple), un cicle no detectat produeix recursió infinita i RangeError: Maximum call stack size exceeded. La lliçó 03-07 explicava l'alternativa iterativa amb pila explícita; tingues-la present si el teu domini no acota la profunditat.
- La capa de dades: el repositori en memòria primer
Aquí hi ha una temptació forta que convé desactivar ja: començar per localStorage perquè «així ja persisteix». No ho facis. Comença amb un repositori en memòria, per quatre raons molt concretes:
- No et bloqueja. Serialitzar, migrar formats i gestionar quotes són problemes reals que mereixen una lliçó sencera — la següent. Resoldre'ls ara t'aparta del que estàs construint.
- Les proves són instantànies i netes. Un repositori en memòria es crea nou a cada prova. Amb
localStorage, cada prova arrossega l'estat de l'anterior tret que netegis, i aquellbeforeEachque s'oblida és una font clàssica de proves intermitents. - Obliga a definir la frontera. Si escrius primer la implementació fàcil, el contracte queda clar; si escrius primer la difícil, el contracte acaba contaminat amb detalls de
localStorage(claus,JSON.stringify, quotes) que no n'haurien de sortir. - És la implementació que faran servir les teves proves per sempre. No és codi rebutjable:
RepositoriMemoriacontinuarà viu a la suite de 11-04 com a doble de prova.
// src/dades/repositori-memoria.js
export class RepositoriMemoria {
#tasques = new Map();
#usuaris = new Map();
#historial = [];
#seguentId = 1;
async llistarTasques() { return [...this.#tasques.values()]; }
async desarTasca(tasca) {
// R1: si no te id, se li assigna aqui; mai no el porta l'usuari
// Retorna la tasca desada (amb el seu id definitiu)
}
async esborrarTasca(id) { /* … */ }
async llistarUsuaris() { /* … */ }
async afegirCanvi(canvi) { /* R14: append-only, mai no modifica */ }
async llistarHistorial(tascaId) { /* … */ }
}Per què async si tot és a la memòria? Perquè el contracte ha de ser el mateix que el del repositori d'API de la lliçó 11-03, on l'asincronia és inevitable. Si el repositori en memòria fos síncron, tota la capa d'aplicació estaria escrita en estil síncron i canviar-la després significaria reescriure-la sencera. Dissenya la frontera per a la implementació més exigent, no per a la més còmoda. És una de les decisions més rendibles de tot el projecte i costa zero.
La llavor. src/dades/llavor.js conté les dades fictícies d'arrencada: tres o quatre usuaris inventats i vuit o deu tasques que incloguin almenys un cas límit de cada regla nova — una tasca vençuda (R10), una amb subtasques mixtes (R13), un usuari inactiu amb tasques assignades (R11), una persona per sobre de 40 h en una setmana (R7/R15). Així, cada vegada que obris l'aplicació, els casos interessants són al davant i no els has de fabricar a mà.
- El contracte del repositori com a frontera
src/dades/repositori.js no conté implementació: conté el contracte, documentat.
// src/dades/repositori.js
/**
* Contracte que TOTES les implementacions de repositori compleixen.
* Implementacions: memoria (11-02), local (11-03), api (11-03).
*
* Regles del contracte:
* - Tots els metodes son asincrons i retornen promeses.
* - Retornen ENTITATS del domini, mai objectes plans ni HTML.
* - En cas de fallada llancen ErrorDeDades (mai no retornen null silencios).
* - llistar*() retorna sempre un array, buit si no hi ha res. Mai null.
* - desar*() retorna l'entitat desada, amb el seu id definitiu.
* - El repositori NO valida regles de negoci: aixo es del domini.
*
* @typedef {Object} Repositori
* @property {() => Promise<Tasca[]>} llistarTasques
* @property {(t: Tasca) => Promise<Tasca>} desarTasca
* @property {(id: number) => Promise<void>} esborrarTasca
* @property {() => Promise<Usuari[]>} llistarUsuaris
* @property {(c: Canvi) => Promise<void>} afegirCanvi
* @property {(id: number) => Promise<Canvi[]>} llistarHistorial
*/Les sis regles del contracte no són burocràcia; cadascuna evita un problema concret:
| Regla | Problema que evita |
|---|---|
| Tot asíncron | Reescriure l'aplicació en canviar d'implementació |
| Retorna entitats, no plans | Que la validació se salti en carregar dades desades |
Llança ErrorDeDades, no retorna null |
Els null silenciosos que esclaten tres capes més amunt |
llistar* mai no retorna null |
Els if (llista) repartits per tota la vista |
desar* retorna l'entitat |
Haver de recarregar-ho tot per conèixer l'id nou |
| No valida regles de negoci | Regles duplicades en dos llocs que es desincronitzen |
La prova de contracte és la tècnica que fa que això funcioni de debò: una única bateria de proves que s'executa contra les tres implementacions.
// test/dades/contracte-repositori.js
export function provesDeContracte(nom, crearRepositori) {
describe(`Contracte de repositori · ${nom}`, () => {
let repo;
beforeEach(async () => { repo = await crearRepositori(); });
test('llistarTasques retorna array buit si no hi ha res', async () => {
expect(await repo.llistarTasques()).toEqual([]);
});
test('desarTasca assigna id i la tasca apareix en llistar', async () => { /* … */ });
test('retorna instancies de Tasca, no objectes plans', async () => { /* … */ });
test('esborrar una tasca inexistent llanca ErrorDeDades', async () => { /* … */ });
test('l historial es append-only', async () => { /* … */ });
});
}A la lliçó 11-03 cridaràs aquesta mateixa funció amb RepositoriLocal i RepositoriApi, i si les tres passen, són intercanviables. És la garantia que fa possible que canviar d'emmagatzematge sigui canviar una línia a main.js. Escriu-la ara, amb la implementació en memòria, encara que sembli excessiva per a un sol cas: en dues lliçons t'estalviarà el dia sencer.
- L'estat de l'aplicació: una única font de veritat
Aquí hi ha l'error de disseny més comú en aplicacions sense framework, i convé veure'l amb nom i cognoms: l'estat repartit. El filtre actiu viu en una variable de la barra de filtres, les tasques visibles en un array de la vista de tauler, l'usuari actual en un atribut data- de la capçalera, i el mode del formulari en una propietat del mateix formulari.
Funciona. Fins al dia que dos d'aquests llocs deixen de coincidir, i llavors tens una interfície que mostra «12 tasques» sobre una llista de 9, i no hi ha manera de saber quin dels dos menteix.
La regla que ho evita:
Un sol objecte conté tot el que la interfície necessita per pintar-se. Res més no governa el que es veu.
// src/aplicacio/estat.js
export function estatInicial() {
return Object.freeze({
// Dades del domini
tasques: [],
usuaris: [],
// Sessio
usuariActual: null,
// Interficie
pantalla: 'tauler', // 'tauler' | 'informe' | 'historial'
filtres: { responsableId: null, estats: [], etiquetes: [], modeEtiquetes: 'o' },
tascaObertaId: null,
// Cicle de vida de la carrega
carregant: false,
error: null,
// Marca de temps de l'ultim canvi, util per depurar
versio: 0
});
}Tres coses que sí que van a l'estat i sovint s'obliden:
carregantierror. Tota operació asíncrona té tres desenllaços i la interfície ha de poder pintar-los tots tres. Si no són a l'estat, acabaran com a variables soltes i com a interfícies que es queden penjades.pantalla. La navegació és estat, no una conseqüència lateral de l'encaminador. L'encaminador tradueix la URL a estat i a l'inrevés; no és la font de veritat.filtrescomplet, amb el seu mode. És el que permet complir el criteri 3 d'H-07: serialitzar el filtre a la URL i restaurar-lo.
I una que no hi va: les dades derivades. tasquesVisibles, horesObertes o tasquesPerPersona es calculen a partir de l'estat, no es desen dins seu. Desar-les crea dues fonts de veritat, que és exactament el problema que estem evitant. Si el càlcul és car, es memoïtza (09-02), però es continua calculant.
// src/aplicacio/selectors.js — funcions pures, deriven de l'estat
export const tasquesVisibles = (estat) => aplicarFiltres(estat.tasques, estat.filtres);
export const horesObertes = (estat) => tasquesVisibles(estat)
.filter((t) => t.estat !== 'feta')
.reduce((suma, t) => suma + t.horesEstimades, 0);Aquestes funcions s'anomenen selectors i tenen tres avantatges: són pures (es proven sense muntar res), es componen entre elles, i són el lloc natural on memoïtzar si el rendiment ho demana. És el mateix concepte que 10-03 presentava a Redux, aquí sense cap llibreria: quaranta línies pròpies.
- Actualitzacions immutables i esdeveniments
L'estat se substitueix, no es modifica. Cada acció produeix un estat nou a partir de l'anterior, amb la desestructuració i el spread de 04-07:
// src/aplicacio/magatzem.js
export function crearMagatzem(estatInicial) {
let estat = estatInicial;
const subscriptors = new Set();
return {
obtenir: () => estat,
actualitzar(canvi) {
const anterior = estat;
estat = Object.freeze({ ...estat, ...canvi, versio: estat.versio + 1 });
for (const fn of subscriptors) fn(estat, anterior);
},
subscriure(fn) {
subscriptors.add(fn);
return () => subscriptors.delete(fn); // funcio per donar-se de baixa
}
};
}Cinc decisions en vint línies:
1 · Object.freeze a cada actualització. Converteix en error qualsevol intent de mutar l'estat directament (en mode estricte, que ja fas servir). És una barana barata que detecta l'error on passa.
2 · versio s'incrementa sempre. Serveix per depurar («quantes actualitzacions porto?») i per detectar renderitzats innecessaris. És la mateixa idea del comptador de versió que invalidava la memòria cau del Tauler de Nómada Tasques.
3 · Els subscriptors reben l'estat anterior. Sense ell, una vista no pot saber què ha canviat i ha de repintar-ho tot. Amb ell, pot comparar i actualitzar només el necessari, que és la base de l'actualitzar() de 09-04.
4 · subscriure retorna la funció de baixa. Aquest detall val una fuita de memòria. Si subscriure's no retorna com donar-se de baixa, les vistes destruïdes continuen al Set per sempre retenint el seu DOM: exactament la fuita que vas estudiar a 09-03. Retornant la baixa, destruir() és una línia.
5 · Actualització superficial. { ...estat, ...canvi } fusiona un nivell. Per canviar un filtre cal escriure filtres: { ...estat.filtres, etiquetes: [...] }, la qual cosa és una mica verbosa però explícita. Resistir la temptació d'una fusió profunda «màgica» és correcte: la fusió profunda amaga què s'està canviant i produeix sorpreses amb arrays.
Com encaixa amb els esdeveniments de 06-04. Hi ha dos mecanismes i fan coses diferents; convé no barrejar-los:
| Mecanisme | Per a què | Direcció |
|---|---|---|
| Subscripció al magatzem | Que les vistes s'assabentin que l'estat ha canviat | Aplicació → vista |
CustomEvent amb delegació |
Que la interacció de l'usuari arribi a l'aplicació | Vista → aplicació |
La vista emet una intenció ({ accio: 'canviar-estat', id: 4, a: 'feta' }), l'aplicació executa el cas d'ús, el cas d'ús crida el domini i el repositori, i el magatzem notifica l'estat nou. És un cicle unidireccional, i aquesta unidireccionalitat és la raó per la qual s'hi pot raonar.
sequenceDiagram
participant U as Usuari
participant V as vista/
participant A as aplicacio/
participant D as domini/
participant R as dades/
U->>V: clic a "Marcar feta"
V->>A: CustomEvent {accio, id, a}
A->>D: tasca.canviarEstat('feta', {subtasques})
D-->>A: {camp, abans, despres} o ErrorDeRegla
A->>R: desarTasca(tasca) + afegirCanvi(canvi)
A->>A: magatzem.actualitzar({tasques, error:null})
A-->>V: subscriptor(estatNou, estatAnterior)
V->>U: repinta nomes el que ha canviat
Aquest diagrama és tota l'arquitectura de la teva aplicació. Si el pots dibuixar de memòria, el pots explicar en una entrevista (lliçó 11-06).
- La vista sense framework: estat → render → esdeveniment
Un component de vista sense framework té una forma molt concreta, i convé que tots els del teu projecte la comparteixin. Aquesta és la plantilla:
// src/vista/tauler-vista.js
export function crearTaulerVista(contenidor, { enEmetre }) {
let ultimEstat = null;
const baixes = [];
function render(estat) {
// Primera vegada: construir l'estructura estable (capcalera, <ul>, peu)
}
function actualitzar(estat, anterior) {
// Segents vegades: comparar i tocar nomes el que ha canviat (09-04)
}
function destruir() {
baixes.forEach((baixa) => baixa()); // treure escoltes
contenidor.replaceChildren(); // buidar el DOM
ultimEstat = null; // deixar anar referencies
}
return { render, actualitzar, destruir };
}Quatre exigències d'aquesta forma:
1 · Rep el seu contenidor, no el busca. Un component que fa document.querySelector('#tauler') a dins només pot existir una vegada i només funciona si aquell id existeix. Rebent-lo, es pot muntar dues vegades, en un fragment, o a jsdom durant una prova (08-05).
2 · Rep enEmetre, no importa l'aplicació. És la frontera de l'apartat 10 de 11-01 feta codi: la vista no sap què passa quan emet, només que algú escolta. Això és el que permet provar-la amb un jest.fn() en lloc de amb l'aplicació sencera.
3 · Separa render d'actualitzar. render construeix; actualitzar reconcilia. Repintar-ho tot a cada canvi és el que portava render() a 310 ms amb 600 tasques a 09-01; reconciliar per data-id és el que ho va baixar a 31 ms a 09-04. Si el teu projecte tindrà llistes llargues, la separació és obligatòria; si no, continua sent bona disciplina.
4 · Té destruir() i es fa servir. Cada escolta afegida, cada setInterval, cada subscripció i cada IntersectionObserver es desfà allà. Sense això, navegar entre pantalles perd memòria — i a 11-04 et tocarà depurar exactament aquesta fuita.
Plantilles amb <template>. Per a l'estructura repetida (la fila de tasca), fes servir <template> a l'HTML i cloneNode(true), com a 06-06. És més ràpid que construir amb createElement node a node i, sobretot, manté l'estructura visible a l'HTML on es pot revisar la semàntica.
<template id="plantilla-fila-tasca">
<li class="tasca" data-id="">
<span class="tasca__estat" aria-hidden="true"></span>
<h3 class="tasca__titol"></h3>
<p class="tasca__meta"></p>
<button type="button" data-accio="canviar-estat">Marcar en curs</button>
<button type="button" data-accio="obrir-detall">Veure detall</button>
</li>
</template>No facis servir mai innerHTML amb dades de l'usuari. Un títol de tasca que contingui <img src=x onerror=alert(1)> s'executa. Fes servir textContent per a tot el que vingui de dades, i innerHTML només amb literals teus. És la lliçó de 06-02, i a 11-03 hi tornarem perquè així que les dades vénen d'una API el risc es multiplica.
- Delegació d'esdeveniments i el controlador
Amb 600 tasques i tres botons cadascuna, afegir escoltes individuals significa 1.800 escoltes. La delegació de 06-04 ho resol amb una:
// src/vista/controlador.js
export function connectarControlador(arrel, enEmetre) {
function onClic(esdeveniment) {
const disparador = esdeveniment.target.closest('[data-accio]');
if (!disparador || !arrel.contains(disparador)) return;
const fila = disparador.closest('[data-id]');
enEmetre({
accio: disparador.dataset.accio,
id: fila ? Number(fila.dataset.id) : null,
valor: disparador.dataset.valor ?? null
});
}
arrel.addEventListener('click', onClic);
return () => arrel.removeEventListener('click', onClic); // baixa
}Quatre detalls del codi que importen:
closest('[data-accio]')troba el botó encara que el clic caigui sobre una icona a dins seu. Sense això, la meitat dels clics no fan res i l'errada és intermitent i desconcertant.arrel.contains(disparador)evita actuar sobre elements que ja no són a l'arbre (pot passar si el DOM va canviar entre el clic i el gestor).- L'identificador es llegeix de
data-id, l'única font de veritat sobre a quina fila correspon l'esdeveniment. Res d'índexs d'array: canvien en filtrar. - Retorna la funció de baixa. Un altre cop. És un patró, no una casualitat: tot el que es connecta ha de poder desconnectar-se.
La delegació i el teclat. Un click en un <button> es dispara també amb Enter i amb Espai: el navegador ho fa per tu. Aquesta és la raó pràctica —i no estètica— per la qual un <div> clicable està malament: obliga a afegir tabindex, role="button" i un gestor de teclat propi per replicar el que l'element correcte ja fa. L'accessibilitat gairebé sempre és menys feina, no més.
- El formulari accessible
El formulari és on es concentren els errors d'accessibilitat i on el domini demostra que serveix. La regla que ho governa tot:
La validació del formulari i la del domini són la mateixa. El formulari l'anticipa; el domini la decideix.
A la pràctica, això significa que el formulari crida el domini i tradueix els seus errors, en lloc de duplicar les regles:
// src/vista/formulari-tasca.js (fragment de l'enviament)
function onEnviar(esdeveniment) {
esdeveniment.preventDefault();
netejarErrors();
const dades = Object.fromEntries(new FormData(formulari));
try {
// El DOMINI valida. El formulari no repeteix les regles.
const tasca = construirTascaDesDeFormulari(dades);
enEmetre({ accio: 'crear-tasca', tasca });
} catch (error) {
if (error instanceof ErrorDeValidacio) {
mostrarErrorAlCamp(error.camp, error.message);
} else {
mostrarErrorGeneral(error);
}
}
}Aquest error.camp és la raó per la qual ErrorDeValidacio porta aquesta propietat des del Mòdul 5: permet que la vista sàpiga quin camp marcar sense analitzar el missatge.
La llista de comprovació d'un formulari accessible, que has de complir a tots:
| # | Requisit | Com es fa | Per què |
|---|---|---|---|
| 1 | Tota entrada té etiqueta | <label for="titol"> |
Sense ella, el lector de pantalla diu «camp de text» |
| 2 | El camp amb error es marca | aria-invalid="true" |
S'anuncia l'estat, no només la vora vermella |
| 3 | El missatge s'associa al camp | aria-describedby="error-titol" |
Es llegeix en enfocar el camp |
| 4 | El focus va al primer error | camp.focus() després de validar |
Sense això, l'usuari de teclat no sap on és el problema |
| 5 | El resum d'errors s'anuncia | role="alert" o aria-live="assertive" |
Un error silenciós no existeix |
| 6 | El botó diu què fa | «Crear tasca», no «Enviar» | Es llegeix fora de context a la llista d'elements |
| 7 | Els camps obligatoris s'indiquen | required + text, no només asterisc |
L'asterisc sol és una convenció visual |
| 8 | Es pot enviar amb Enter | Fer servir <form> i submit, no un click |
Comportament esperat per tothom |
| 9 | No es perd res en fallar | No buidar el formulari en l'error | Refer un formulari llarg és la pitjor experiència possible |
| 10 | L'èxit es comunica | Missatge a aria-live="polite" + focus |
Si no, no se sap si va funcionar |
Els punts 4, 5 i 10 són els que gairebé mai no s'implementen en projectes de portafoli i els que més es noten en una revisió d'accessibilitat (11-04).
- Les tres pantalles noves
Les extensions E4, E5 i E6 de 11-01 produeixen tres pantalles que no són llistes de targetes, i aquí hi ha el seu valor formatiu: són les primeres que no pots resoldre copiant TaulerVista.
14.1 Calendari (E4)
| Aspecte | Decisió i per què |
|---|---|
| Estructura | <table> real amb <th scope="col"> per als dies de la setmana. Un calendari és una taula de dades; simular-lo amb <div> és refer a mà tota la semàntica |
| Cel·les buides | Els dies d'altres mesos van amb aria-hidden="true" o directament sense contingut interactiu |
| Moltes tasques en un dia | Mostrar-ne dues i «+3 més» que obre el dia. No apilis sense límit: trenca la graella i el CLS |
Sense dataLimit |
Secció a part «Sense data», mai repartides arbitràriament |
| Navegació | Botons anterior/següent i teclat: fletxes mouen de dia, PageUp/PageDown de mes |
| Dates | Intl.DateTimeFormat per als noms de mes i dia (07-06), mai arrays de textos propis |
| Avui | Marcat amb text (aria-current="date"), no només amb color |
El càlcul del primer dia de la graella i del nombre de setmanes és el clàssic exercici de dates: escriu-lo a util/dates.js com a funció pura i prova'l amb febrer d'un any de traspàs, amb un mes que comença en diumenge i amb el canvi d'any. Aquests tres casos cobreixen gairebé totes les errades possibles.
14.2 Informe de càrrega (E5)
| Aspecte | Decisió i per què |
|---|---|
| Càlcul | Un selector pur: carregaPerPersonaISetmana(estat). Sense DOM. Es prova amb taules d'entrada/sortida |
| Setmana ISO | Funció pròpia a util/dates.js, amb el cas de l'1 de gener provat explícitament (R15) |
| Doble comptabilització | Només fulles de l'arbre (R12). És l'errada més probable de tota la pantalla |
| Sobrecàrrega | Fons i text («46 h · supera el límit»), mai només color |
| Taula | <table> amb <caption>, <th scope="col"> per a setmanes i <th scope="row"> per a persones |
| Exportació | Fora del MVP; quan arribi, Blob + URL.createObjectURL (07-06). Compte a escapar comes i cometes al CSV |
| Rendiment | Si el càlcul passa de 16 ms amb dades realistes, memoïtza'l (09-02) |
14.3 Historial (E6)
| Aspecte | Decisió i per què |
|---|---|
| Estructura | <ol> en ordre cronològic invers. És una llista ordenada de debò |
| Cada entrada | «La Marta va canviar l'estat de pendent a feta», amb <time datetime="…"> |
| Volum | Creix sense límit: pagina o carrega per lots des del principi (09-05) |
| Immutabilitat | Sense botons d'editar ni esborrar. Cap. És la R14 feta interfície |
| Usuari esborrat | Mostra el nom desat a l'entrada, no l'actual: l'historial és una foto del passat |
| Filtre | Per tasca i per persona; reutilitza el mecanisme de filtres de l'estat |
Aquest detall de l'usuari esborrat és una decisió de modelatge que potser no havies anticipat: si l'entrada d'historial desa només usuariId i aquest usuari es desactiva o desapareix, l'historial perd llegibilitat. Desar també el nom en el moment del canvi és duplicació deliberada, i és el correcte en dades històriques. Anota-ho com a ADR (11-06).
- Accessibilitat des del principi
L'argument definitiu per no deixar-la per al final és de cost:
| Quan | Cost típic | Què implica |
|---|---|---|
| Des del primer commit | ~5 % del temps | Triar l'element correcte i afegir un atribut |
| Al final del projecte | ~30 % del temps | Reescriure la meitat de la vista, refer el focus, reestructurar l'HTML |
I bona part d'aquest 5 % es redueix a fer servir l'element HTML que ja existeix:
| Necessites | Element correcte | El que et dona de franc |
|---|---|---|
| Alguna cosa clicable | <button type="button"> |
Focus, Enter, Espai, rol, estat deshabilitat |
| Navegar a una altra vista | <a href="…"> |
Focus, Enter, menú contextual, obrir en pestanya nova |
| Una llista | <ul> / <ol> + <li> |
«Llista de 12 elements» anunciat |
| Dades tabulars | <table> amb <th scope> |
Navegació per cel·les amb encapçalaments llegits |
| Agrupar camps | <fieldset> + <legend> |
El grup s'anuncia en entrar-hi |
| Contingut plegable | <details> / <summary> |
Estat desplegat/plegat sense JavaScript |
| Diàleg modal | <dialog> amb showModal() |
Focus atrapat, Escape, fons inert |
Aquest últim mereix èmfasi: <dialog> amb showModal() resol tot sol l'atrapament de focus, el tancament amb Escape i la inertització del fons. Reimplementar això a mà són cent línies fràgils.
Llista de comprovació d'accessibilitat per increment (va a la Definició de Fet de 11-01):
| # | Comprovació | Com es fa en 30 segons |
|---|---|---|
| 1 | Tot assolible amb Tab | Desa el ratolí i recorre la pantalla sencera |
| 2 | Focus sempre visible | Mira on ets a cada parada; :focus-visible amb contrast ≥ 3:1 |
| 3 | Ordre de tabulació lògic | Segueix l'ordre visual? Si no, l'ordre del DOM està malament |
| 4 | Sense paranys de focus | Pots sortir de tot allò on has entrat? |
| 5 | Focus gestionat en obrir i tancar | En obrir un diàleg, hi entra; en tancar-lo, torna al disparador |
| 6 | Els canvis s'anuncien | Regió aria-live="polite" per a «12 tasques visibles de 47» |
| 7 | Els errors s'anuncien | role="alert" al resum d'errors |
| 8 | Imatges i icones | alt descriptiu, o aria-hidden="true" si són decoratius |
| 9 | Contrast | DevTools sobre el text; ≥ 4,5:1 normal, ≥ 3:1 gran |
| 10 | Res només per color | Vençuda (R10) i sobrecàrrega (R7) amb text o icona |
| 11 | Zoom al 200 % | Ctrl + roda; es perd contingut o funció? |
| 12 | Idioma declarat | <html lang="ca"> |
Els dotze punts es comproven en uns cinc minuts per pantalla. Fes-ho en tancar cada increment, no al final del projecte.
La regió d'anuncis és una de sola, viva des de l'arrencada, i la fa servir tota l'aplicació:
// src/vista/anunciar.js
let regio = null;
export function anunciar(text) {
regio ??= document.getElementById('anuncis');
regio.textContent = ''; // forca el reanunci
requestAnimationFrame(() => { regio.textContent = text; });
}El truc de buidar i tornar a posar al fotograma següent és necessari perquè molts lectors de pantalla no anuncien un text idèntic a l'anterior. Sense ell, «Tasca creada» s'anuncia la primera vegada i cap més.
- Registre i gestió d'errors de tota l'aplicació
El teu projecte té tres tipus d'error, i cadascun es tracta diferent:
| Tipus | Exemple | Qui el llança | Què veu l'usuari | Es registra |
|---|---|---|---|---|
| De validació | Títol buit (R2) | Domini | Missatge al costat del camp | No |
| De regla | Tancar amb subtasques obertes (R13) | Domini | Avís explicatiu amb la causa | No |
| De dades / tècnic | Quota plena, xarxa caiguda, JSON corrupte | Dades | «Alguna cosa ha fallat» + acció de reintent | Sí |
La distinció no és acadèmica: els dos primers són comportament esperat i no han d'embrutar el registre d'errors. Si registres cada validació fallida, el registre s'omple de soroll i les errades reals s'hi perden.
// src/aplicacio/casos-us.js (patro general d'un cas d'us)
export async function crearTasca(magatzem, repo, dades) {
magatzem.actualitzar({ carregant: true, error: null });
try {
const tasca = new Tasca(dades); // pot llancar ErrorDeValidacio
const desada = await repo.desarTasca(tasca); // pot llancar ErrorDeDades
await repo.afegirCanvi(canviDeCreacio(desada, magatzem.obtenir().usuariActual));
magatzem.actualitzar({ tasques: [...magatzem.obtenir().tasques, desada], carregant: false });
anunciar(`Tasca «${desada.titol}» creada`);
return desada;
} catch (error) {
magatzem.actualitzar({ carregant: false, error: aErrorInterficie(error) });
if (error instanceof ErrorDeDades) registrar(error, { cas: 'crearTasca', dades });
throw error;
}
}Cinc punts del patró, que has de repetir a tots els casos d'ús:
carregant: trueierror: nullen començar. Netejar l'error anterior evita que quedi penjat un missatge de fa dues operacions.try/catchal voltant de tot, ambcarregant: falsealcatch. Uncarregantque no s'apaga és un comptador girant per sempre.aErrorInterficie(error)tradueix un error tècnic a alguna cosa que una persona entén. És responsabilitat de l'aplicació, no del domini ni de les dades.- Només es registren els tècnics.
- Es rellança l'error. Qui l'ha cridat (el formulari) necessita saber que ha fallat per no tancar-se ni netejar els camps.
El registrador, minimalista i suficient:
// src/util/registre.js
const MAXIM = 50;
const entrades = [];
export function registrar(error, context = {}) {
const entrada = {
data: new Date().toISOString(),
nom: error.name,
missatge: error.message,
context, // MAI dades personals aqui
pila: error.stack?.split('\n').slice(0, 5).join('\n')
};
entrades.push(entrada);
if (entrades.length > MAXIM) entrades.shift(); // no creixer sense limit
if (import.meta.env.DEV) console.error('[Òrbita]', entrada);
}
export const historialErrors = () => [...entrades];I les dues xarxes de seguretat globals, a main.js:
window.addEventListener('error', (e) => registrar(e.error ?? new Error(e.message), { tipus: 'global' }));
window.addEventListener('unhandledrejection', (e) => registrar(e.reason, { tipus: 'promesa' }));La segona és la que més errades silencioses caça: una promesa rebutjada sense catch no apareix enlloc excepte a la consola, i en producció ningú no mira la consola dels seus usuaris.
Avís de privacitat al registre. El camp
contextés temptador per posar-hi «les dades que van causar l'errada». No hi posis mai dades personals, tokens ni contingut escrit per l'usuari. Registra identificadors i noms de camp ({ cas: 'crearTasca', camps: ['titol', 'horesEstimades'] }), no valors. Quan a 11-05 connectis un servei de seguiment d'errors en producció, aquesta disciplina serà una obligació legal, no una recomanació.
- Increments verificables i commits petits
Un increment verificable és un tros de feina que compleix tres condicions:
- L'aplicació continua funcionant en acabar-lo.
- Es pot demostrar amb alguna cosa: una prova nova en verd o un comportament visible.
- Cap en mig dia o menys.
Comparació de dues maneres d'abordar la història H-04 (subtasques), amb la mateixa quantitat de feina:
| ❌ Un sol commit | ✅ Sis increments |
|---|---|
feat: subtasques (18 fitxers, 640 línies) |
feat(domini): afegir tascaMareId amb validació |
feat(domini): construir arbre des de llista plana |
|
feat(domini): detectar cicles en vincular (R12) |
|
feat(domini): calcular hores per fulles (R12) |
|
feat(domini): bloquejar tancament amb filles obertes (R13) |
|
feat(vista): mostrar subtasques imbricades al tauler |
|
Si falla, git bisect assenyala 640 línies |
Si falla, assenyala 40 |
| Impossible de revisar | Cadascun es revisa en cinc minuts |
| Impossible de revertir en part | Es reverteix només el que sobra |
L'argument de git bisect mereix atenció: és l'ordre que troba automàticament quin commit va introduir una errada, provant la meitat de la història cada vegada. Amb commits de 40 línies et dona la causa exacta; amb commits de 640 et dona un rang on encara has de buscar. Aquesta eina només funciona bé si els teus commits són petits i cadascun deixa el projecte funcionant.
El ritme que funciona, i que pots cronometrar:
1 · Tria l'increment més petit que aporti alguna cosa (2 min)
2 · Escriu la prova que falla (10 min)
3 · Escriu-ho fins que passi (30 min)
4 · Neteja amb les proves en verd (10 min)
5 · npm run verificar (1 min)
6 · Commit amb missatge que expliqui el perquè (2 min)
→ torna a 1Uns 55 minuts per volta. Quatre voltes és una sessió de treball productiva, i acaba amb quatre commits que expliquen una història llegible.
- Quan refactoritzar i com no trencar res
Refactoritzar és canviar l'estructura interna del codi sense canviar-ne el comportament. Aquesta definició conté la regla més important:
No refactoritzis mai i afegeixis funcionalitat al mateix commit.
Si ho barreges, i alguna cosa es trenca, no saps si va ser el canvi d'estructura o el de comportament. Separats, el commit de refactorització té una propietat valuosíssima: totes les proves passaven abans i passen després, sense tocar cap prova. Si has de modificar proves, no estaves refactoritzant: estaves canviant comportament.
Quan refactoritzar, amb senyals objectius:
| Senyal | Què indica | Què fer |
|---|---|---|
| Copies i enganxes per tercera vegada | Falta una abstracció | Extreu la funció (a la tercera, no a la segona) |
| Una funció no cap a la pantalla | Fa massa coses | Extreu els passos amb noms que expliquin |
El nom porta «i» (validarIDesar) |
Dues responsabilitats | Parteix-la |
| Necessites un comentari per explicar un bloc | El codi no s'explica | Extreu aquest bloc a funció amb el nom del comentari |
| Costa escriure la prova | Hi ha massa dependències | Injecta el que necessita en comptes de crear-ho a dins |
| Canviar una cosa obliga a tocar cinc fitxers | Acoblament alt | Revisa si estàs creuant una frontera de capa |
| Et fa por tocar-ho | Falta cobertura | Primer proves, després refactorització |
Aquesta última fila és la regla d'or: no es refactoritza codi sense proves. Si cal tocar alguna cosa sense cobertura, l'ordre és: escriure proves del que fa ara (encara que el que faci sigui estrany), veure-les en verd, i només llavors canviar l'estructura. Les proves són la xarxa; sense xarxa, no és refactorització, és reescriptura a cegues.
I la regla que evita l'altre extrem: no refactoritzis codi que funciona, que no tocaràs i que ningú no ha reportat. La refactorització té un cost i un risc, i només es justifica quan l'estructura actual t'està destorbant per a alguna cosa concreta que faràs ara. «És lleig» no és una raó suficient quan tens un pla amb fites per complir.
- La llista de comprovació abans de tancar un increment
Cinc minuts per increment. És la versió operativa de la Definició de Fet de 11-01.
| # | Comprovació | Com |
|---|---|---|
| 1 | Fa una cosa? | Llegeix-ho en veu alta; si fas servir «i», parteix-lo |
| 2 | Hi ha una prova que falla sense aquest canvi? | git stash el codi, executa les proves |
| 3 | Els casos límit estan coberts? | Buit, null, text llarguíssim, número fora de rang |
| 4 | Respecta les fronteres de capa? | npm run lint |
| 5 | Sense console.log, sense codi comentat? |
git diff complet |
| 6 | Els noms diuen la veritat? | calcularCarrega calcula o també desa? |
| 7 | Accessible amb teclat? | Recorre la pantalla sense ratolí |
| 8 | S'anuncia el que canvia? | Hi ha anunciar() on cal? |
| 9 | Els errors tenen el seu camí? | Provoca'n un a propòsit i mira què es veu |
| 10 | L'estat continua sent única font de veritat? | Has creat alguna variable d'estat a la vista? |
| 11 | npm run verificar en verd? |
Executa'l |
| 12 | El missatge de commit explica el perquè? | Llegeix-lo com si fos d'una altra persona |
El punt 2 té un procediment concret que val la pena aprendre: desa el teu canvi de codi amb git stash deixant les proves noves, executa-les i comprova que fallen; recupera amb git stash pop i comprova que passen. Això demostra que la prova prova alguna cosa. Una prova que passa amb i sense el canvi no està provant res, i és més comú del que sembla.
- Què fer quan t'encalles
T'encallaràs. No és una possibilitat: forma part de la feina, i saber-ne sortir és una habilitat tan tècnica com saber escriure un reduce.
20.1 El mètode de les tres preguntes
Quan alguna cosa no funciona i portes més de quinze minuts, para de teclejar i respon per escrit:
1 · Què esperava exactament que passés?
No «que funcioni». Alguna cosa verificable: «esperava que carregaPerPersona(estat) retornés { 'u-1': 25 }». Si no ho pots formular així, el problema no és el codi: és que no saps què vols. I aquest és l'encallament de debò.
2 · Què passa en realitat?
Amb la dada al davant: «retorna { 'u-1': 25, 'u-2': undefined }». No «dona error», sinó el missatge exacte, la línia exacta, el valor exacte.
3 · Quina és la diferència més petita entre el que esperava i el que passa?
«Hi ha una clau de més, amb valor undefined». Aquesta frase acostuma a contenir la causa: si apareix una clau que no hi hauria de ser, alguna cosa està iterant sobre usuaris en lloc de sobre tasques assignades.
Aquest mètode resol, per experiència, més de la meitat dels encallaments sense executar res. Funciona perquè l'encallament gairebé mai no és «no sé programar això»: és «tinc tres suposicions barrejades i una és falsa». Escriure-les les separa.
20.2 La caixa de temps
Posa un temporitzador de 45 minuts. Si en sonar no has avançat, canvia d'estratègia obligatòriament:
| Intent | Estratègia | Temps |
|---|---|---|
| 1 | Les tres preguntes + depurador (08-01) | 45 min |
| 2 | Reproduir en aïllament (20.3) | 45 min |
| 3 | Cercar a la documentació oficial —MDN, no un fòrum qualsevol | 30 min |
| 4 | Explicar-ho a algú (o a un objecte inanimat, en veu alta) | 15 min |
| 5 | Deixar-ho i fer una altra cosa del projecte | Fins demà |
El pas 5 no és rendir-se: és l'estratègia amb millor relació entre cost i resultat de les cinc. Una quantitat sorprenent d'encallaments es resolen a la dutxa de l'endemà, perquè el problema era una suposició fixa que només es deixa anar en deixar de mirar-la.
El pas 4 té nom propi —rubber duck debugging, depuració amb ànec de goma— i funciona per una raó concreta: explicar-ho en veu alta t'obliga a fer explícites les suposicions que estaves donant per bones. Moltíssimes vegades la frase es talla a mitges amb un «…ah, espera».
20.3 Reproduir el problema en aïllament
És la tècnica més potent de totes i la que menys es fa servir. Consisteix a construir l'exemple més petit possible que reprodueixi l'errada, fora de l'aplicació.
Procediment:
- Nou fitxer de prova,
test/aillat.test.js, amb només l'imprescindible. - Dades mínimes: dues tasques en lloc de quaranta.
- Sense vista, sense repositori, sense estat: només la funció sospitosa.
- Treu coses fins que deixi de fallar. L'última cosa que has tret hi està implicada.
- Quan falli amb deu línies, la causa acostuma a ser òbvia.
Un exemple real d'aquest projecte:
// test/aillat.test.js — l'informe donava 71 h on havia de donar 48
test('hores d un arbre de 3 tasques', () => {
const mare = tasca({ id: 1, hores: 0, mare: null });
const filla1 = tasca({ id: 2, hores: 10, mare: 1 });
const filla2 = tasca({ id: 3, hores: 5, mare: 1 });
const arbre = construirArbre([mare, filla1, filla2]);
expect(horesTotals(arbre[0])).toBe(15); // ← falla: retorna 30
});Amb tres tasques, «retorna 30 en comptes de 15» diu immediatament que alguna cosa es compta dues vegades. Amb quaranta tasques i la interfície muntada, aquesta mateixa errada és «els números de l'informe estan estranys» i et pot costar una tarda.
I un benefici addicional que acostuma a passar desapercebut: aquesta prova aïllada es queda a la suite. En arreglar-ho, ja tens la prova de regressió escrita, de franc. És exactament el mètode que aplicaràs a les tres errades de la lliçó 11-04.
20.4 Quan l'encallament significa una altra cosa
De vegades l'encallament no és tècnic. Aquests senyals indiquen que el problema és un nivell més amunt:
| Senyal | Què acostuma a significar | Què fer |
|---|---|---|
| Portes dies sense tancar un increment | L'increment és massa gran | Parteix-lo fins que càpiga en mig dia |
| Cada canvi trenca tres coses | Falta cobertura o hi ha acoblament | Para i escriu proves abans de continuar |
| No saps per on començar la història | No està ben definida | Torna als criteris d'acceptació de 11-01 |
| Canvies de tasca sense acabar-ne cap | Falta un ordre explícit | Tanca l'actual abans de tocar-ne una altra |
| Portes dues setmanes sense veure progrés | Estàs treballant horitzontalment | Torna a l'apartat 2: fes un tall vertical |
Errors Habituals i Consells
Començar per l'HTML perquè és el que es veu. És el parany més natural del món i produeix aplicacions amb la lògica de negoci repartida entre gestors de clic. Si la teva regla R13 viu dins d'un onclick, no es pot provar sense navegador, no es pot reutilitzar des de l'informe i desapareixerà el dia que canviïs la interfície. Domini primer, sempre.
Construir tot el domini abans de tocar el navegador. L'error oposat, i també real. Tres increments de domini i fora: fes el primer tall vertical. L'arquitectura només es valida quan la travessa alguna cosa de punta a punta.
Estat repartit per la interfície. El filtre a la barra, la llista a la vista, el mode al formulari. Funciona fins al dia que deixen de coincidir, i llavors no hi ha manera de saber quin menteix. Un objecte d'estat, i les vistes en llegeixen.
Mutar l'estat directament. estat.tasques.push(nova) no dispara cap subscripció, així que la interfície no se n'assabenta i apareix «no s'actualitza» sense causa aparent. Object.freeze al magatzem converteix aquest error silenciós en una excepció immediata: fes-lo servir.
Duplicar les regles al formulari. Si valides «màxim 40 hores» al formulari i al domini, tens dues veritats que es desincronitzaran. El formulari crida el domini i tradueix l'error; el número 40 apareix una sola vegada, a regles.js.
Oblidar destruir(). Cada vista que es munta i no es desmunta bé deixa escoltes i subscripcions vives. Amb dues pantalles no es nota; a la lliçó 11-04 apareixerà com una fuita de memòria en navegar i l'hauràs de buscar. Escriu destruir() alhora que render(), no després.
Llegir Date.now() dins del domini. Fa que les proves depenguin del dia en què s'executen i produeix errades que apareixen soles un dilluns. Passa avui com a paràmetre. La mateixa regla val per a Math.random() i per als ids.
Commits gegants. «feat: subtasques» amb 640 línies és impossible de revisar, de revertir i de bisecar. Si el missatge necessita una «i», són dos commits.
Refactoritzar i afegir funcionalitat alhora. Quan alguna cosa es trenqui, no sabràs quin dels dos va ser. I si has de tocar proves mentre refactoritzes, no estàs refactoritzant.
Consell · Escriu primer la funció auxiliar de proves. Mitja hora construint test/ajudes/fabriques.js fa que les teves cent proves següents es llegeixin en una línia cadascuna. És la inversió amb millor retorn de tot el mòdul.
Consell · Tingues sempre un increment «en verd» al qual tornar. Abans de començar alguna cosa arriscada, assegura't que l'últim commit funciona. Així, si et perds, git restore . et torna a terreny ferm en lloc de a un pantà de canvis a mitges.
Consell · Apunta les decisions a mesura que les prens. Un fitxer docs/decisions.md amb tres línies per decisió. A 11-06 el convertiràs en ADR formals, i agrairàs no haver de reconstruir de memòria per què l'arbre és pla.
Consell · Acaba la sessió deixant una prova escrita que falla. És el millor punt de represa que existeix: l'endemà saps exactament què estaves fent i què toca fer, sense rellegir res.
Exercicis
Aquests exercicis són les fites H2 i H3 del teu projecte: el domini amb les seves proves en verd i la primera funcionalitat completa de punta a punta.
Exercici 1 — El domini complet amb TDD.
Implementa src/domini/ sencer amb les seves proves, seguint el cicle vermell → verd → refactoritzar:
errors.jsambErrorDeValidacio(amb.camp),ErrorDeReglaiErrorDeDades, tots heretant d'Errori ambnamecorrecte.regles.jsamb totes les constants: estats, prioritats, rols, matriu de transicions i límits.tasca.jsiusuari.jsamb validació completa al constructor, camps privats per al que és mutable itoJSON/desDeJSON.arbre.jsambconstruirArbre,fulles,horesTotals,profunditat,esDescendentipotVincular.tauler.jsamb les operacions de conjunt i els càlculs (resum,horesObertes,carregaPerPersonaISetmana).- Proves: cada regla R1–R15 amb cas que passa, cas que falla i cas límit. Les transicions amb
test.each(9 combinacions). Els cicles de l'arbre amb els tres tipus: autoreferència, cicle indirecte i excés de profunditat. test/ajudes/fabriques.jsamb constructors d'entitats vàlides per defecte.- Cobertura del domini ≥ 90 % de branques.
Requisit estricte: el domini no pot importar res de dades/, vista/ ni aplicacio/, ni fer servir document, localStorage, fetch, Date.now() ni Math.random(). npm run lint ho ha de verificar (frontera configurada a 11-01).
Exercici 2 — El primer tall vertical.
Implementa «veure el tauler» de punta a punta:
RepositoriMemoriaamb el contracte complet i la llavor fictícia (mínim 8 tasques amb un cas límit de cada regla nova).test/dades/contracte-repositori.jsamb almenys 8 proves de contracte, executades contra la implementació en memòria.crearMagatzemambobtenir,actualitzar,subscriure(que retorna la baixa) i congelació de l'estat.- Selectors purs:
tasquesVisibles,horesObertes,resumPerEstat. TaulerVistaambrender,actualitzaridestruir, fent servir<template>i semàntica de llista.- Controlador amb delegació per
[data-accio]i[data-id]. main.jsque ho munta tot, carrega la llavor i pinta.- Regió
aria-livefuncionant i anunci del nombre de tasques visibles. - Prova d'integració amb Testing Library: consultar per rol (
getAllByRole('listitem')) i comprovar el contingut.
Exercici 3 — Els dos talls següents i l'arbre.
- Crear una tasca: formulari amb
FormData, validació delegada al domini,aria-invalidiaria-describedby, focus al primer error, i anunci de l'èxit. Compleix els 10 punts de la llista de l'apartat 13. - Canviar d'estat: R6 aplicada des de la interfície, amb l'error de regla mostrat de manera comprensible i anunciat.
- Subtasques: crear una subtasca, veure-la imbricada, i comprovar que R13 impedeix tancar la mare amb filles obertes, amb el missatge que anomena la subtasca.
- Cadascuna a la seva branca, amb almenys quatre commits convencionals, i tancada amb la llista de comprovació de l'apartat 19.
- Documenta a
docs/decisions.mdalmenys tres decisions preses durant la implementació.
Solucions
De nou, no hi ha codi de solució: hi ha criteris d'acceptació i rúbriques. Aquestes llistes són les que has de recórrer abans de donar-te per satisfet.
Criteris d'acceptació de l'exercici 1 — Domini
| # | Criteri | Com es comprova |
|---|---|---|
| 1 | Un objecte invàlid no es pot construir | new Tasca({ titol: ' ' }) llança ErrorDeValidacio amb .camp === 'titol' |
| 2 | L'estat inicial és sempre 'pendent' |
Passar estat: 'feta' al constructor no el canvia (R5) |
| 3 | Les 9 transicions estan cobertes | test.each amb les 9 files, 4 vàlides i 5 rebutjades |
| 4 | Les etiquetes estan normalitzades i congelades | ['Taller','taller'] → ['taller']; push llança en mode estricte |
| 5 | estaVencuda és determinista |
La mateixa prova passa avui i d'aquí a un any |
| 6 | Els tres tipus de cicle es detecten | Autoreferència, indirecte i profunditat, cadascun amb la seva prova |
| 7 | Les hores no es dupliquen | Arbre d'1 mare + 2 filles (10 h i 5 h) → 15 h, no 30 |
| 8 | R11 rebutja usuaris inactius | Assignar a un usuari amb actiu: false llança |
| 9 | R11 rebutja responsable = revisor | La mateixa persona en tots dos camps llança |
| 10 | R13 anomena la subtasca que bloqueja | El missatge conté el títol |
| 11 | La setmana ISO és correcta | L'1 de gener d'un any que comença en divendres cau a la setmana 53 de l'anterior |
| 12 | El domini es prova sense jsdom | testEnvironment: 'node' per a test/domini/ i tot passa |
El criteri 12 és la prova definitiva que la teva arquitectura funciona. Configura Jest amb projectes separats: node per a domini, jsdom per a la resta. Si el domini necessita jsdom, has creuat la frontera sense adonar-te'n.
Rúbrica de l'exercici 1 (24 punts)
| Dimensió | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| Invariants | Objectes invàlids possibles | Validació bàsica | Validació completa | A més normalització i congelació |
| Cobertura de regles | < 8 regles provades | 10 regles | Les 15 | Les 15 amb cas límit exacte |
| Determinisme | Fa servir Date.now() a dins |
Parcialment injectat | Tot injectat | A més amb rellotge fals a les proves |
| Recursivitat | Només casos feliços | Cicle directe | Els 3 tipus de cicle | A més amb arbre profund provat |
| Llegibilitat de les proves | Repetitives | Amb fàbriques | Amb test.each on toca |
Es llegeixen com a especificació |
| Errors | Genèrics | Tipats | Tipats amb .camp |
Amb missatges útils per a l'usuari |
| Fronteres | Creuades | Respectades de facto | Verificades per ESLint | A més amb Jest en entorn node |
| Cobertura | < 70 % | 70–85 % | 85–90 % | ≥ 90 % de branques, sense buits en regles |
Llindar: 17/24, amb obligatòriament 3 a «Fronteres» i ≥ 2 a «Cobertura de regles».
Criteris d'acceptació de l'exercici 2 — Tall vertical
| # | Criteri | Com es comprova |
|---|---|---|
| 1 | Es veuen les tasques de la llavor en obrir | Manual, npm run dev |
| 2 | La llista és una llista de debò | getAllByRole('listitem') en retorna tantes com tasques |
| 3 | Cada fila té el seu data-id |
Cap índex d'array al DOM |
| 4 | Les vençudes es distingeixen amb text | No només color (R10 + pressupost d'accessibilitat) |
| 5 | L'estat està congelat | estat.tasques.push(x) llança |
| 6 | La subscripció es pot cancel·lar | subscriure() retorna funció; després de cridar-la no es notifica |
| 7 | El contracte passa contra memòria | Les 8 proves de contracte en verd |
| 8 | La vista no importa dades/ |
ESLint ho verifica |
| 9 | destruir() deixa el contenidor buit i sense escoltes |
Prova que compta escoltes abans i després |
| 10 | El nombre de tasques s'anuncia | La regió aria-live conté el text després del render |
Criteris d'acceptació de l'exercici 3 — Talls 2, 3 i 4
| # | Criteri | Com es comprova |
|---|---|---|
| 1 | Títol buit marca el camp, no un alert |
aria-invalid="true" + missatge associat |
| 2 | El focus va al primer camp amb error | document.activeElement és aquell camp |
| 3 | El formulari no es buida en fallar | Els valors continuen allà |
| 4 | Crear anuncia l'èxit i torna el focus | aria-live amb text + focus a la fila nova o al disparador |
| 5 | Una transició no vàlida no canvia l'estat | El domini llança i la interfície continua mostrant l'estat anterior |
| 6 | L'error de regla és comprensible | Cap traça tècnica visible per a l'usuari |
| 7 | La subtasca apareix imbricada semànticament | <ul> dins del <li> de la mare |
| 8 | Tancar la mare amb filles obertes s'impedeix | Missatge que anomena la subtasca |
| 9 | Tot funciona sense ratolí | Recorregut complet amb Tab, Enter i Escape |
| 10 | Cada branca té ≥ 4 commits convencionals | git log --oneline |
Autoavaluació de la fita H3. Abans de passar a la lliçó següent:
| Pregunta | Sí / No |
|---|---|
| Puc executar les proves del domini sense jsdom i passen totes? | |
Hi ha alguna regla de negoci fora de domini/? |
|
Podria canviar RepositoriMemoria per un altre sense tocar vista/? |
|
| Puc fer servir l'aplicació sencera sense ratolí? | |
Cada vista que munto té el seu destruir() i el crido? |
|
| Els meus últims deu commits tenen missatges que expliquen el perquè? | |
npm run verificar està en verd ara mateix? |
Un «sí» a la segona pregunta (que és l'única formulada a l'inrevés) o un «no» a qualsevol de les altres són deute que la lliçó 11-03 multiplicarà. Arregla-ho ara.
Conclusió
Has construït l'esquelet real del teu producte, i ho has fet en l'ordre que aguanta.
Saps per què es construeix de dins cap a fora: perquè els errors de model són els més cars que existeixen i apareixen el dia 2 en una prova unitària en lloc de a la setmana 5 repartits per sis fitxers; perquè el domini es prova a Node en mil·lisegons; i perquè és la part amb més vida útil, com va demostrar que domini/regles.js fos idèntic a les quatre versions de 10-06. I saps per què això no significa horitzontalisme: vertical abans que horitzontal, un tall complet que travessi les quatre capes abans que totes les capes a mitges, perquè és el que valida l'arquitectura aviat, deixa alguna cosa lliurable en tot moment i treu a la llum els requisits ocults d'un en un.
Tens el domini construït sobre invariants: un objecte que no pot existir en estat invàlid elimina la meitat dels errors possibles. Validar-ho tot abans d'assignar res, normalitzar a la frontera, congelar l'immutable, injectar avui en lloc d'invocar Date.now() —cosa que fa les proves deterministes per sempre— i retornar l'objecte de canvi des de canviarEstat perquè l'historial de R14 no obligui el domini a saber que existeix un historial. Amb les regles en un únic regles.js que es llegeix com a documentació.
Saps aplicar TDD on compensa —les regles de negoci i els càlculs, no la maquetació ni l'exploració— i saps que els criteris Donat / Quan / Llavors de 11-01 es tradueixen a proves gairebé paraula per paraula. Tens les quatre proves de R13 —que falla, que el missatge serveix, que el camí feliç funciona, i que la regla no destorba sense subtasques—, les nou transicions parametritzades amb test.each com a especificació verificada, i la disciplina de provar sempre el cas que passa, el que no i el límit exacte, perquè les errades viuen als marges.
Tens l'arbre resolt amb la recursivitat de 03-07: construcció en una passada amb un Map per id, error explícit davant d'una mare inexistent, i les tres condicions de cicle que cal comprovar per separat —autoreferència, cicle indirecte i excés de profunditat—, de les quals només la primera és evident i les altres dues són les que produeixen desbordaments de pila.
Tens la capa de dades començada per on no bloqueja: un repositori en memòria que no és codi rebutjable —serà el teu doble de prova per sempre—, amb un contracte asíncron dissenyat per a la implementació més exigent i no per a la més còmoda, i una bateria de proves de contracte que a la lliçó següent executaràs contra localStorage i contra l'API per demostrar que són intercanviables.
Tens l'estat governat: una única font de veritat congelada, amb carregant i error a dins perquè tota operació asíncrona té tres desenllaços; dades derivades calculades per selectors purs i mai desades; actualitzacions immutables amb spread; subscripcions que retornen la seva baixa, que és la línia que separa una aplicació neta d'una amb fuites; i el cicle unidireccional vista → aplicació → domini → dades → magatzem → vista que pots dibuixar de memòria.
Tens vistes sense framework amb una forma comuna: reben el seu contenidor i el seu enEmetre, separen render d'actualitzar —els 310 ms davant dels 31 ms de 09-04— i tenen destruir() escrit alhora que la resta. Amb delegació per [data-accio] i closest, plantilles <template>, textContent per a tota dada d'usuari, i formularis que criden el domini en lloc de duplicar-ne les regles, amb els deu punts d'accessibilitat que gairebé ningú no implementa: focus al primer error, error anunciat, i el formulari que no es buida quan alguna cosa falla.
Saps fer les tres pantalles que no són llistes —un calendari que és una <table> de debò, un informe el càlcul del qual és un selector pur i el risc més gran del qual és comptar dues vegades les hores de l'arbre, i un historial sense ni un botó d'editar perquè la R14 és una promesa—, i saps que l'accessibilitat costa un 5 % des del principi i un 30 % al final, i que la major part d'aquest 5 % consisteix simplement a fer servir l'element HTML que ja existeix.
Tens una gestió d'errors amb tres categories que es tracten diferent, amb les validacions i les regles fora del registre perquè el soroll no tapi les errades reals, un patró de cas d'ús amb carregant, try/catch, traducció a llenguatge humà i rellançament, les dues xarxes de seguretat globals —inclosa la de promeses rebutjades, que és la que caça les errades silencioses— i la disciplina de no posar mai dades personals al context del registre.
I tens el ritme: increments de mig dia que deixen l'aplicació funcionant, commits petits que fan útil git bisect, la regla de no refactoritzar i afegir funcionalitat al mateix commit, els set senyals objectius de quan refactoritzar —i el de quan no—, la llista de dotze comprovacions per tancar un increment, i un mètode concret per als encallaments: les tres preguntes per escrit, la caixa de temps de 45 minuts amb canvi obligatori d'estratègia, i reproduir en aïllament, que converteix «els números de l'informe estan estranys» en «amb tres tasques retorna 30 en comptes de 15» i, de passada, et regala la prova de regressió.
La fita H3 està tancada: el domini en verd i la primera funcionalitat completa de punta a punta al navegador. Però hi ha una cosa que continua sent mentida: en recarregar la pàgina, tot desapareix. El repositori en memòria va fer la seva feina —no bloquejar-te— i ara toca substituir-lo sense tocar ni una línia de vista, que és exactament per a això que vas definir el contracte. Això, més versionar el format desat i migrar-lo sense perdre dades, parlar amb una API de debò amb els seus estats i els seus errors, i treballar sense connexió amb una cua de canvis pendents, és Persistència i Sincronització de Dades.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
