Arquitectura de Software · Patrón MVC

Laboratorio: del espagueti al MVC

Reciben una aplicación que funciona pero está escrita como código espagueti: las reglas, la pantalla y el almacenamiento mezclados en un solo archivo. La van a separar en Modelo Vista y Controlador, y unas pruebas les dirán si lo hicieron bien. Trabajan en equipo, con JavaScript puro: sin frameworks y sin instalar dependencias.

7 partes≈ 2 h 20 min con la entrega1 repositorio por equipoJavaScript · sin dependencias

Qué van a lograr

Modelo

Sabe: los datos y las reglas del negocio.
No sabe: que existe una pantalla.

Vista

Sabe: cómo mostrar el estado y qué hizo el usuario.
No sabe: reglas, ni guardar datos.

Controlador

Sabe: traducir lo que hace el usuario en órdenes.
No sabe: dibujar, ni calcular.

Reglas del laboratorio

1. El Modelo no conoce a nadie: no importa la Vista ni el Controlador y no usa document, window, alert ni localStorage. 2. La Vista no decide: no tiene reglas de negocio ni cambia el Modelo. 3. El Controlador no dibuja ni calcula: traduce y ordena. 4. La aplicación debe comportarse igual que espagueti.html: se refactoriza, no se reinventa.


Antes de la clase

No hay descargas pesadas: todo cabe en unos pocos archivos. Aun así, comprueben su equipo con calma el día anterior.


Ruta y tiempos

ParteQué hacenTiempoTerminan con…
0 · PreparaciónCrean el repo del equipo, arrancan el servidor y ven las dos versiones10 minLa app espagueti funcionando
1 · Leer el espaguetiClasifican 12 fragmentos de código y listan los olores20 minENTREGA.md §1 y §2
2 · El ModeloImplementan las reglas hasta poner en verde las pruebas del Modelo30 minnpm run test:modelo (grupos A a D)
3 · La VistaDibujan el estado del Modelo y ofrecen los eventos20 minLa tabla y el resumen se dibujan
4 · El ControladorConectan los eventos con las órdenes y manejan los errores20 minLa app MVC se comporta como la espagueti
5 · Tres cambiosUna regla nueva, una vista nueva y un almacén nuevo25 min3 commits, cada uno tocando solo lo debido
6 · Medir y entregarReglas de arquitectura, diagrama del flujo y reflexión15 minnpm test en verde y ENTREGA.md

Ruta corta (≈ 100 min): partes 0 a 4 y una entrega reducida (secciones 1 y 2 de ENTREGA.md y la salida de npm test). Las partes 5 y 6 quedan como trabajo posterior.


Mapa del proyecto

ArchivoQué es¿Lo modifican?
espagueti.htmlLa versión de partida: todo en un solo archivo. Es su referencia para comparar.No: léanla
index.htmlLa pantalla de la app MVC: solo HTML estático, sin lógica.No
src/modelo.jsModelo Clase NotasModelo: datos y reglas.Parte 2 (y 5A)
src/vista.jsVista Clase NotasVista: dibuja y escucha.Parte 3
src/controlador.jsControlador Clase NotasControlador: conecta.Parte 4
src/almacen.jsDónde se guardan los datos (AlmacenMemoria, AlmacenLocal).Parte 5C
src/main.jsLa raíz de composición: el único lugar que crea y conecta las tres piezas.Partes 5B y 5C
pruebas/modelo.pruebas.jsLa especificación del Modelo, escrita como pruebas (grupos A a E).No: léanla
pruebas/arquitectura.pruebas.jsReglas como «el Modelo no toca la pantalla» o «la Vista no decide».No: léanla
ENTREGA.mdLo que entregan: mapa, olores, flujo, evidencias y reflexión.Todas las partes

Parte 0 · Preparación

10 min
  1. Una persona del equipo abre la plantilla del laboratorio y presiona Create repository from template. Nombre: equipo-N-mvc (cambien N). Visibilidad: Public. Después, en Settings → Collaborators, agrega a sus compañeros.
  2. Cada integrante clona el repositorio de su equipo (no la plantilla) y entra a la carpeta:
    git clone https://github.com/SU-USUARIO/equipo-N-mvc.git
    cd equipo-N-mvc
  3. Arranquen el servidor (no hay nada que instalar):
    npm start
    La terminal imprime tres direcciones. Déjenla abierta mientras trabajan.
  4. Abran http://localhost:3000/espagueti.html y usen la app dos minutos: agreguen una evaluación con nota y otra sin nota, provoquen un error, califiquen, eliminen y recarguen la página.
  5. Abran http://localhost:3000/: es la app MVC. Se ve la pantalla pero no hace nada: es lo esperado, la van a construir ustedes.
  6. En otra terminal ejecuten las pruebas:
    npm test
    Verán rojo: también es lo esperado. Cada grupo de pruebas es un pedazo del Modelo que falta.
Trabajar en equipo con git

Hagan un commit por parte (git add -A y git commit -m "Parte 2: Modelo") y git pull antes de cada git push. En la Parte 5 esos commits son su evidencia.


Parte 1 · Leer el espagueti

20 min

Antes de separar algo hay que entender por qué está mal pegado. Esta parte no escribe código: lee, clasifica y anota.

  1. Abran espagueti.html en el editor y lean el <script> completo (≈ 100 líneas). Para cada línea pregúntense: ¿esto son datos y reglas M, dibujar la pantalla V o reaccionar al usuario C?
  2. Completen la tabla de 12 fragmentos de ENTREGA.md, sección 1. Si una línea mezcla varios roles, escriban Mezcla y digan cuáles.
  3. Hagan estos tres experimentos y anoten qué observan:
    • El cambio de regla. Cambien en el código la nota mínima de 3.0 a 3.5. ¿Cuántos lugares tuvieron que tocar? (Busquen 3.0 con Ctrl + F.) ¿Y si olvidan uno?
    • El nombre raro. En la app espagueti escriban <b>hola</b> como nombre de una evaluación. ¿Qué pasa y por qué?
    • La prueba imposible. ¿Pueden comprobar la regla «los pesos suman a lo sumo 100» sin abrir el navegador? ¿Qué se lo impide?
  4. Listen al menos 5 olores de diseño en ENTREGA.md, sección 2: qué consecuencia tiene cada uno cuando el proyecto crece y en qué pieza de MVC viviría su solución.
Pistas para los olores

Variables globales · una misma regla escrita en dos funciones · un valor «mágico» repetido · HTML armado con texto · funciones que hacen tres cosas a la vez · el almacenamiento disperso · imposible probar sin navegador.

No la modifiquen

espagueti.html es su referencia: al final de la Parte 4 van a comparar su app MVC contra ella, paso a paso.


Parte 2 · El Modelo

30 minModelo

El Modelo es la pieza más importante y la más fácil de probar, porque no sabe nada de pantallas. Abran src/modelo.js: ya trae las constantes, el constructor y un getter. El resto son TODO que ustedes reemplazan. Las pruebas de pruebas/modelo.pruebas.js son la especificación: léanlas.

La API que deben construir

MétodoQué haceLanza ErrorDeReglas si…
agregar({ nombre, peso, nota })Crea { id, nombre, peso, nota } y avisa del cambio. nota es opcional (null).falla alguna de R1 a R5
calificar(id, nota)Pone la nota, o la quita con null.falla R4 o R6
eliminar(id)Quita la evaluación.falla R6
get evaluacionesCopias de la lista (ya escrito).—
resumen()Calcula { acumulado, pesoCalificado, pesoPendiente, notaNecesaria, estado }.—
suscribir(fn)Observer: registra fn, que se llama después de cada cambio. Devuelve una función para cancelar.—

Las reglas y sus mensajes exactos

Las pruebas comparan el mensaje tal cual. Validen en este orden: nombre, peso, nota y, al final, la suma de pesos.

#ReglaMensaje
R1El nombre es obligatorio (se ignoran los espacios de los extremos).El nombre es obligatorio.
R2El nombre tiene 40 caracteres como máximo.El nombre no puede superar 40 caracteres.
R3El peso es un número entre 1 y 100.El peso debe estar entre 1 y 100.
R4La nota, si existe, está entre 0 y 5.La nota debe estar entre 0 y 5.
R5Los pesos no suman más de 100.Los pesos no pueden sumar más de 100 % (te quedan X %).
R6El id debe existir (al calificar o eliminar).No existe la evaluación.

Cómo se calcula el resumen

ValorFórmula
acumuladoSuma de peso × nota ÷ 100 de las evaluaciones que tienen nota.
pesoCalificadoSuma de los pesos de las evaluaciones con nota.
pesoPendiente100 − pesoCalificado (incluye lo que aún no han registrado).
notaNecesaria(NOTA_MINIMA − acumulado) ÷ (pesoPendiente ÷ 100), nunca negativa, redondeada hacia arriba a 2 decimales (usen redondearArriba). null si pesoPendiente es 0.
estado'aprobado' si acumulado ≥ NOTA_MINIMA. Si no: 'imposible' si notaNecesaria es null o mayor que NOTA_MAXIMA; 'en-riesgo' si es mayor que UMBRAL_RIESGO; y 'en-camino' en los demás casos.

Paso a paso

  1. 2A · Agregar y validar (≈ 10 min). Implementen agregar. Pistas: String(nombre ?? '').trim(); Number.isFinite(peso); el id sale de this.#siguienteId++ y solo se consume si todo salió bien; guarden el nombre ya limpio. Ejecuten npm run test:modelo: el grupo A debe quedar completo.
  2. 2B · Calificar y eliminar (≈ 5 min). Les conviene un método privado #buscar(id) (lanza R6) y otro #validarNota(nota) que también reutilice agregar. Grupo B en verde.
  3. 2C · Resumen (≈ 8 min). Sigan la tabla de fórmulas. Grupo C en verde.
  4. 2D · Observer y persistencia (≈ 7 min). suscribir guarda la función en el Set de suscriptores y devuelve cómo quitarla. #cambio() —que deben llamar al final de cada orden que sí cambió algo— guarda en el almacén y avisa a todos los suscriptores. Grupo D en verde.
Una cosa a la vez

npm run test:modelo muestra un grupo a la vez, con esperado contra recibido. Con npm test -- --todo ven todos los fallos. El grupo E es de la Parte 5: déjenlo para entonces.

Decimales

En un computador 0.1 + 0.2 no es exactamente 0.3. Por eso el archivo trae EPS (una tolerancia): úsenla al comparar el acumulado con la nota mínima. Hay una prueba que se lo recuerda.

Listo cuando: los grupos A, B, C y D están en verde y el Modelo no menciona document, window, alert ni localStorage. Hagan commit: Parte 2: Modelo.


Parte 3 · La Vista

20 minVista

La Vista presenta y escucha; no decide. El HTML estático (formulario, tabla, resumen) ya está en index.html: la Vista lo rellena y avisa qué hizo el usuario. Abran src/vista.js.

  1. 3A · Constructor (≈ 3 min). Guarden en this.#el los elementos que van a usar (los id están en el comentario). Suscríbanse al Modelo —modelo.suscribir(...)— para redibujarse sola y llamen a this.render() una vez.
  2. 3B · Eventos que la Vista ofrece (≈ 6 min). alAgregar, alEliminar y alCalificar reciben un manejador (una función) y lo llaman cuando el usuario actúa. La Vista no decide qué hacer: solo avisa. alEliminar ya viene escrito en vista.js como ejemplo resuelto; hagan los otros dos igual:
    alEliminar(manejador) {
      // Un solo escuchador en la tabla (delegación de eventos): sirve para todas las filas.
      this.#el.tabla.addEventListener('click', (evento) => {
        const boton = evento.target.closest('button[data-eliminar]');
        if (boton) manejador(Number(boton.dataset.eliminar)); // el id viaja en data-eliminar
      });
    }
    Para alAgregar escuchen submit del formulario (y evento.preventDefault()); para alCalificar, el evento change de la tabla con el id en data-nota.
  3. 3C · Órdenes que obedece (≈ 3 min). leerFormulario() devuelve { nombre, peso, nota } con el texto escrito; limpiarFormulario(), mostrarError(mensaje) y limpiarError() son de una línea.
  4. 3D · Dibujar (≈ 8 min). render() lee this.#modelo.evaluaciones y this.#modelo.resumen() y actualiza la pantalla:
Qué se muestraCómo
Una fila por evaluaciónNombre · "30 %" · un <input type="number"> con la nota (vacío si es null) y data-nota="id" · aporte peso × nota ÷ 100 con 2 decimales, o — · botón Eliminar con data-eliminar="id"
#vacio («Aún no hay evaluaciones.»)Solo se ve si la lista está vacía (atributo hidden)
#acumulado2 decimales (toFixed(2))
#pesoCalificado y #pesoPendiente"30 %": el número redondeado a 2 decimales (Math.round(n * 100) / 100) seguido de %
#necesaria2 decimales, o — si es null
#estadoTexto y clase CSS según el estado (tabla siguiente)
estadoTextoClase de #estado
aprobado¡Aprobado! Ya aseguraste la nota mínima.estado ok
en-caminoEn camino: necesitas X.XX en lo que falta.estado en-camino
en-riesgoEn riesgo: necesitas X.XX en lo que falta.estado riesgo
imposibleImposible aprobar con las notas actuales.estado imposible
Nada de innerHTML con datos del usuario

El nombre lo escribe el usuario. Si arman la tabla con innerHTML y texto, ese nombre se interpreta como HTML (¿recuerdan el experimento de la Parte 1?). Usen document.createElement y textContent.

Cómo verla funcionar sin Controlador

La app espagueti y la MVC guardan sus datos en el mismo lugar (localStorage, clave notas.v1). Si agregaron evaluaciones en espagueti.html, ya deben aparecer en http://localhost:3000/ en cuanto la Vista dibuje. Todavía no pueden agregar nada ahí: falta el Controlador.

Listo cuando: las filas y el resumen se dibujan, y npm run arquitectura muestra el grupo Vista pasiva completo. Commit: Parte 3: Vista.


Parte 4 · El Controlador

20 minControlador

El Controlador es el más pequeño y el que más tienta a convertirse en un «cajón de sastre». Su trabajo: leer → ordenar → reaccionar. Abran src/controlador.js.

  1. 4A · Registrarse en la Vista (≈ 8 min). En el constructor llamen a vista.alAgregar, vista.alEliminar y vista.alCalificar. Recuerden que la Vista entrega texto, pero el Modelo espera números: un texto vacío en la nota significa null (sin nota).
  2. 4B · Errores (≈ 8 min). Armen un método privado (por ejemplo #intentar(orden)) que ejecute la orden al Modelo y, si lanza un ErrorDeReglas, le pida a la Vista que muestre el mensaje y que se redibuje (así el campo vuelve al valor que el Modelo sí aceptó). Si la orden sale bien, limpien el error; y al agregar, también el formulario. Los errores que no son de reglas no se esconden: se vuelven a lanzar.
  3. 4C · Comparar (≈ 4 min). Repitan en http://localhost:3000/ (borren antes los datos: F12 → Application → Local Storage → Clear) esta lista y comprueben que obtienen exactamente lo mismo que en espagueti.html:
#AcciónDebe verse
1Agregar Parcial 1, peso 30, nota 3.5Aporte 1.05 · Acumulado 1.05 · 30 % / 70 % · Necesaria 2.79 · «En camino: necesitas 2.79 en lo que falta.»
2Agregar Parcial 2, peso 30, sin notaAporte — · mismos números (el peso pendiente sigue en 70 %)
3Agregar Final, peso 50Error: «Los pesos no pueden sumar más de 100 % (te quedan 40 %).» · el formulario conserva lo escrito
4Poner nota 4 a Parcial 2Acumulado 2.25 · 60 % / 40 % · Necesaria 1.88
5Agregar Taller, peso 20, nota 5Acumulado 3.25 · 80 % / 20 % · Necesaria 0.00 · «¡Aprobado! Ya aseguraste la nota mínima.» (verde)
6Escribir 7 como nota de una filaError: «La nota debe estar entre 0 y 5.» · el campo vuelve a su valor anterior
7Eliminar Parcial 1Acumulado 2.20 · 50 % / 50 % · Necesaria 1.60 · «En camino: necesitas 1.60 en lo que falta.»
8Recargar la páginaTodo sigue igual

Listo cuando: la lista da igual en las dos versiones y npm test muestra en verde los grupos de arquitectura Modelo puro, Vista pasiva y Controlador delgado. Commit: Parte 4: Controlador.


Parte 5 · Tres cambios

25 min

Aquí se cobra la separación. Cada cambio debe tocar solo las piezas que le corresponden. Hagan un commit por cambio y peguen git show --stat HEAD en ENTREGA.md, sección 5: es la prueba de que no tocaron de más.

5A · Una regla nueva: nombres únicos Modelo 8 min

Dos evaluaciones no pueden llamarse igual (sin importar mayúsculas ni espacios sobrantes). Mensaje exacto: Ya existe una evaluación con ese nombre. Las pruebas del grupo E ya están escritas: háganlas pasar.

Qué deben observar

Con solo tocar modelo.js, el error aparece en pantalla al intentar repetir un nombre: la Vista y el Controlador no cambiaron. Comprueben con git show --stat HEAD.

5B · Una segunda vista: el termómetro Vista 12 min

Otra forma de mostrar el mismo Modelo: una barra que se llena según el acumulado, con una línea en la nota mínima. Creen src/termometro-vista.js con esta forma (los estilos .termometro, .relleno, .linea y .leyenda ya existen y index.html trae el contenedor #termometro):

import { NOTA_MINIMA, NOTA_MAXIMA } from './modelo.js';

export class TermometroVista {
  constructor(raiz, modelo) {
    // 1. Crea la barra: un div.termometro con un div.relleno y un div.linea, y debajo un p.leyenda.
    //    La línea siempre va en NOTA_MINIMA ÷ NOTA_MAXIMA × 100 % (style.left).
    // 2. Suscríbete al Modelo y dibuja una primera vez.
  }

  render() {
    // relleno.style.width = acumulado ÷ NOTA_MAXIMA × 100 %   (sin pasar de 100 %)
    // leyenda: "3.25 de 5.0 · la línea marca el mínimo para aprobar (3.0)"
  }
}

Móntenla en src/main.js con una línea (más su import): new TermometroVista(document.getElementById('termometro'), modelo);

Qué deben observar

Agregan, califican y eliminan: las dos vistas cambian solas. El Modelo no sabe que ahora hay dos; solo avisa. npm run arquitectura revisa también el archivo nuevo.

5C · Otro almacén: sesión en vez de localStorage Almacén 5 min

Agreguen a src/almacen.js la clase AlmacenSesion, igual a AlmacenLocal pero con sessionStorage (sobrevive a recargar, pero se pierde al cerrar la pestaña). Cámbienlo en main.js: new NotasModelo(new AlmacenSesion('notas.v1')). Comprueben: recargar conserva los datos; cerrar la pestaña y volver a abrir, no.

Qué deben observar

Cambió solo almacen.js y main.js. El Modelo ni se enteró: solo exige que el almacén tenga cargar() y guardar().

Retos opcionales bonus


Parte 6 · Medir y entregar

15 min
  1. Todo en verde. Ejecuten npm test: deben pasar las 54 pruebas (39 del Modelo y 15 de arquitectura). Peguen el resultado en ENTREGA.md, sección 4.
  2. Dibujen el flujo de «agregar una evaluación», desde que el usuario presiona Agregar hasta que ve la fila nueva, marcando en cada paso el archivo y el método que actúa (ENTREGA.md, sección 3). Pista: son al menos siete pasos y pasan por las tres piezas, el almacén y las dos vistas.
  3. Reflexionen sobre las cinco preguntas de ENTREGA.md, sección 6. Con sus palabras, no las de la presentación.
  4. Suban todo:
    git add -A
    git commit -m "Entrega del laboratorio MVC"
    git pull
    git push

Entrega y rúbrica

Entreguen el enlace del repositorio del equipo (público). Todo lo escrito va en ENTREGA.md.

CriterioQué se miraPuntos
ModeloImplementado correctamente y probado: grupos A a D en verde, sin depender de la pantalla.5
Vista y ControladorCada uno en su papel (pruebas de arquitectura en verde) y la app se comporta igual que la espagueti.4
Tres cambiosCada cambio tocó solo lo debido, con la evidencia de git show --stat.4
Mapa y oloresClasificación acertada de los 12 fragmentos y olores bien justificados.3
Diagrama del flujoRecorrido completo y correcto, con archivo y método en cada paso.2
ReflexiónRespuestas propias, concretas y relacionadas con lo que hicieron.2

Problemas frecuentes

«node» o «npm» no se reconocen

Falta instalar Node.js (versión LTS) o hay que cerrar y volver a abrir la terminal después de instalarlo. Comprueben con node -v.

Abrí index.html con doble clic y no pasa nada (o veo errores de «module» o «CORS»)

Los módulos de JavaScript (import/export) no funcionan si abren el archivo directamente. Usen npm start y la dirección http://localhost:3000/, o la extensión Live Server de VS Code.

El puerto 3000 está ocupado

Elijan otro:

# macOS / Linux
PORT=3001 npm start
# Windows PowerShell
$env:PORT=3001; npm start
# Windows cmd (las comillas evitan que se cuele un espacio al final)
set "PORT=3001" && npm start
Plan B: no puedo instalar Node en este computador

Instalen la extensión Live Server en VS Code, abran el proyecto, hagan clic derecho en index.html → Open with Live Server. Las pruebas están en pruebas/pruebas.html (se abre igual y muestra lo mismo que npm test; recarguen la página después de cada cambio).

La página muestra datos que no recuerdo haber escrito

Los datos viven en localStorage y se comparten entre espagueti.html e index.html. Para empezar de cero: F12 → Application → Local Storage → botón derecho → Clear, o una ventana de incógnito.

Cambié el código y la página sigue igual

Recarguen con Ctrl + Shift + R y abran la consola (F12 → Console): un error de sintaxis en un módulo detiene toda la app y se ve ahí.

Dice que falta el grupo X, pero ya lo hice

Lean el mensaje: muestra esperado contra recibido. Los motivos típicos: un mensaje de error con una letra distinta, el orden de las validaciones o un #cambio() que se olvidó en una de las órdenes.

Las pruebas de arquitectura fallan con «métodos sin implementar»

Todavía queda algún throw new Error('TODO ...') en esa pieza. Búsquenlo con Ctrl + F y reemplácenlo por código real.

Git: «rejected» al hacer push

Alguien del equipo subió antes. Hagan git pull, resuelvan los conflictos si los hay, y vuelvan a hacer git push. Para no pisarse, reparten piezas distintas.


Vocabulario

Modelo
La pieza que guarda los datos y las reglas del negocio. No sabe nada de pantallas.
Vista
La pieza que muestra el estado del Modelo y avisa lo que hace el usuario. No decide reglas.
Controlador
La pieza que traduce lo que hace el usuario en órdenes para el Modelo y decide qué mostrar si falla.
Observer
Patrón de diseño: un objeto avisa de sus cambios a quien se haya suscrito, sin conocerlo. Así el Modelo actualiza las Vistas sin depender de ellas.
Almacén
Quien guarda y carga los datos (memoria, localStorage, un servidor…). El Modelo solo conoce su «contrato»: cargar() y guardar().
Raíz de composición
El único lugar (main.js) que crea las piezas y las conecta. Las demás las reciben.
Olor de diseño
Una señal en el código de que algo está mal estructurado, aunque funcione.
Refactorizar
Mejorar la estructura del código sin cambiar lo que hace.
Prueba de arquitectura
Una regla de diseño que una máquina revisa sola (en inglés, fitness function): por ejemplo, «el Modelo no usa el DOM».
Acoplamiento
Cuánto depende una pieza de otras. Menos acoplamiento = cambios más baratos.