Arquitectura de Software · Patrón 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.
Sabe: los datos y las reglas del negocio.
No sabe: que existe una pantalla.
Sabe: cómo mostrar el estado y qué hizo el usuario.
No sabe: reglas, ni guardar datos.
Sabe: traducir lo que hace el usuario en órdenes.
No sabe: dibujar, ni calcular.
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.
No hay descargas pesadas: todo cabe en unos pocos archivos. Aun así, comprueben su equipo con calma el día anterior.
node -v. Si no lo tienen, instalen la versión LTS desde nodejs.org. ¿No pueden instalar nada? Hay un Plan B al final de esta guía.git --version.| Parte | Qué hacen | Tiempo | Terminan con… |
|---|---|---|---|
| 0 · Preparación | Crean el repo del equipo, arrancan el servidor y ven las dos versiones | 10 min | La app espagueti funcionando |
| 1 · Leer el espagueti | Clasifican 12 fragmentos de código y listan los olores | 20 min | ENTREGA.md §1 y §2 |
| 2 · El Modelo | Implementan las reglas hasta poner en verde las pruebas del Modelo | 30 min | npm run test:modelo (grupos A a D) |
| 3 · La Vista | Dibujan el estado del Modelo y ofrecen los eventos | 20 min | La tabla y el resumen se dibujan |
| 4 · El Controlador | Conectan los eventos con las órdenes y manejan los errores | 20 min | La app MVC se comporta como la espagueti |
| 5 · Tres cambios | Una regla nueva, una vista nueva y un almacén nuevo | 25 min | 3 commits, cada uno tocando solo lo debido |
| 6 · Medir y entregar | Reglas de arquitectura, diagrama del flujo y reflexión | 15 min | npm 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.
| Archivo | Qué es | ¿Lo modifican? |
|---|---|---|
espagueti.html | La versión de partida: todo en un solo archivo. Es su referencia para comparar. | No: léanla |
index.html | La pantalla de la app MVC: solo HTML estático, sin lógica. | No |
src/modelo.js | Modelo Clase NotasModelo: datos y reglas. | Parte 2 (y 5A) |
src/vista.js | Vista Clase NotasVista: dibuja y escucha. | Parte 3 |
src/controlador.js | Controlador Clase NotasControlador: conecta. | Parte 4 |
src/almacen.js | Dónde se guardan los datos (AlmacenMemoria, AlmacenLocal). | Parte 5C |
src/main.js | La raíz de composición: el único lugar que crea y conecta las tres piezas. | Partes 5B y 5C |
pruebas/modelo.pruebas.js | La especificación del Modelo, escrita como pruebas (grupos A a E). | No: léanla |
pruebas/arquitectura.pruebas.js | Reglas como «el Modelo no toca la pantalla» o «la Vista no decide». | No: léanla |
ENTREGA.md | Lo que entregan: mapa, olores, flujo, evidencias y reflexión. | Todas las partes |
equipo-N-mvc (cambien N). Visibilidad: Public. Después, en Settings → Collaborators, agrega a sus compañeros.git clone https://github.com/SU-USUARIO/equipo-N-mvc.git
cd equipo-N-mvcnpm start
La terminal imprime tres direcciones. Déjenla abierta mientras trabajan.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.http://localhost:3000/: es la app MVC. Se ve la pantalla pero no hace nada: es lo esperado, la van a construir ustedes.npm test
Verán rojo: también es lo esperado. Cada grupo de pruebas es un pedazo del Modelo que falta.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.
Antes de separar algo hay que entender por qué está mal pegado. Esta parte no escribe código: lee, clasifica y anota.
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?ENTREGA.md, sección 1. Si una línea mezcla varios roles, escriban Mezcla y digan cuáles.3.0 con Ctrl + F.) ¿Y si olvidan uno?<b>hola</b> como nombre de una evaluación. ¿Qué pasa y por qué?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.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.
espagueti.html es su referencia: al final de la Parte 4 van a comparar su app MVC contra ella, paso a paso.
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.
| Método | Qué hace | Lanza 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 evaluaciones | Copias 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 pruebas comparan el mensaje tal cual. Validen en este orden: nombre, peso, nota y, al final, la suma de pesos.
| # | Regla | Mensaje |
|---|---|---|
| R1 | El nombre es obligatorio (se ignoran los espacios de los extremos). | El nombre es obligatorio. |
| R2 | El nombre tiene 40 caracteres como máximo. | El nombre no puede superar 40 caracteres. |
| R3 | El peso es un número entre 1 y 100. | El peso debe estar entre 1 y 100. |
| R4 | La nota, si existe, está entre 0 y 5. | La nota debe estar entre 0 y 5. |
| R5 | Los pesos no suman más de 100. | Los pesos no pueden sumar más de 100 % (te quedan X %). |
| R6 | El id debe existir (al calificar o eliminar). | No existe la evaluación. |
| Valor | Fórmula |
|---|---|
acumulado | Suma de peso × nota ÷ 100 de las evaluaciones que tienen nota. |
pesoCalificado | Suma de los pesos de las evaluaciones con nota. |
pesoPendiente | 100 − 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. |
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.#buscar(id) (lanza R6) y otro #validarNota(nota) que también reutilice agregar. Grupo B en verde.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.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.
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.
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.
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.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.leerFormulario() devuelve { nombre, peso, nota } con el texto escrito; limpiarFormulario(), mostrarError(mensaje) y limpiarError() son de una línea.render() lee this.#modelo.evaluaciones y this.#modelo.resumen() y actualiza la pantalla:| Qué se muestra | Cómo |
|---|---|
| Una fila por evaluación | Nombre · "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) |
#acumulado | 2 decimales (toFixed(2)) |
#pesoCalificado y #pesoPendiente | "30 %": el número redondeado a 2 decimales (Math.round(n * 100) / 100) seguido de % |
#necesaria | 2 decimales, o — si es null |
#estado | Texto y clase CSS según el estado (tabla siguiente) |
estado | Texto | Clase de #estado |
|---|---|---|
aprobado | ¡Aprobado! Ya aseguraste la nota mínima. | estado ok |
en-camino | En camino: necesitas X.XX en lo que falta. | estado en-camino |
en-riesgo | En riesgo: necesitas X.XX en lo que falta. | estado riesgo |
imposible | Imposible aprobar con las notas actuales. | estado imposible |
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.
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.
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.
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).#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.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ón | Debe verse |
|---|---|---|
| 1 | Agregar Parcial 1, peso 30, nota 3.5 | Aporte 1.05 · Acumulado 1.05 · 30 % / 70 % · Necesaria 2.79 · «En camino: necesitas 2.79 en lo que falta.» |
| 2 | Agregar Parcial 2, peso 30, sin nota | Aporte — · mismos números (el peso pendiente sigue en 70 %) |
| 3 | Agregar Final, peso 50 | Error: «Los pesos no pueden sumar más de 100 % (te quedan 40 %).» · el formulario conserva lo escrito |
| 4 | Poner nota 4 a Parcial 2 | Acumulado 2.25 · 60 % / 40 % · Necesaria 1.88 |
| 5 | Agregar Taller, peso 20, nota 5 | Acumulado 3.25 · 80 % / 20 % · Necesaria 0.00 · «¡Aprobado! Ya aseguraste la nota mínima.» (verde) |
| 6 | Escribir 7 como nota de una fila | Error: «La nota debe estar entre 0 y 5.» · el campo vuelve a su valor anterior |
| 7 | Eliminar Parcial 1 | Acumulado 2.20 · 50 % / 50 % · Necesaria 1.60 · «En camino: necesitas 1.60 en lo que falta.» |
| 8 | Recargar la página | Todo 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.
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.
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.
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.
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);
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.
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.
Cambió solo almacen.js y main.js. El Modelo ni se enteró: solo exige que el almacén tenga cargar() y guardar().
NOTA_MINIMA.) ¿Qué cambió en el termómetro sin que lo programaran?document.title = 'x' en modelo.js y ejecuten npm run arquitectura. ¿Qué prueba lo detecta y con qué mensaje?npm test: deben pasar las 54 pruebas (39 del Modelo y 15 de arquitectura). Peguen el resultado en ENTREGA.md, sección 4.ENTREGA.md, sección 3). Pista: son al menos siete pasos y pasan por las tres piezas, el almacén y las dos vistas.ENTREGA.md, sección 6. Con sus palabras, no las de la presentación.git add -A
git commit -m "Entrega del laboratorio MVC"
git pull
git pushEntreguen el enlace del repositorio del equipo (público). Todo lo escrito va en ENTREGA.md.
npm test en verde: 54 de 54 pruebas.git show --stat en ENTREGA.md.| Criterio | Qué se mira | Puntos |
|---|---|---|
| Modelo | Implementado correctamente y probado: grupos A a D en verde, sin depender de la pantalla. | 5 |
| Vista y Controlador | Cada uno en su papel (pruebas de arquitectura en verde) y la app se comporta igual que la espagueti. | 4 |
| Tres cambios | Cada cambio tocó solo lo debido, con la evidencia de git show --stat. | 4 |
| Mapa y olores | Clasificación acertada de los 12 fragmentos y olores bien justificados. | 3 |
| Diagrama del flujo | Recorrido completo y correcto, con archivo y método en cada paso. | 2 |
| Reflexión | Respuestas propias, concretas y relacionadas con lo que hicieron. | 2 |
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.
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.
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 startInstalen 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).
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.
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í.
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.
Todavía queda algún throw new Error('TODO ...') en esa pieza. Búsquenlo con Ctrl + F y reemplácenlo por código real.
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.
localStorage, un servidor…). El Modelo solo conoce su «contrato»: cargar() y guardar().main.js) que crea las piezas y las conecta. Las demás las reciben.