mirror of
https://github.com/cursos-uai/sap_tfi_2026.git
synced 2026-09-17 17:29:21 -03:00
feat(ejercicios): agregar laboratorio interactivo ICONIX
Co-authored-by: wsf <alejandro.sartorio@gmail.com> Signed-off-by: wsf <alejandro.sartorio@gmail.com>
This commit is contained in:
@@ -58,6 +58,9 @@ sap_tfi_2026/
|
||||
> **Abrir la versión web:**
|
||||
> [https://cursos-uai.github.io/sap_tfi_2026/](https://cursos-uai.github.io/sap_tfi_2026/)
|
||||
|
||||
> **Practicar con ejercicios interactivos:**
|
||||
> [https://cursos-uai.github.io/sap_tfi_2026/ejercicios.html](https://cursos-uai.github.io/sap_tfi_2026/ejercicios.html)
|
||||
|
||||
El directorio **`prototipo/`** contiene una representación interactiva de las
|
||||
pantallas que actúan como fronteras (*boundaries*) en los diagramas de robustez.
|
||||
Permite observar cómo los botones, formularios, tablas, mensajes y otros
|
||||
|
||||
@@ -32,6 +32,21 @@ Luego visitar <http://localhost:8000>.
|
||||
| Reserva | CU-ENT-002 | D-ROB-ENT-002 |
|
||||
| Facturación | CU-FAC-001 | D-ROB-FAC-001 |
|
||||
|
||||
## Ejercicios interactivos
|
||||
|
||||
Abrir `ejercicios.html` para recorrer seis estaciones de aprendizaje:
|
||||
|
||||
1. fundamentos de diseño para el cambio y ocultamiento de información;
|
||||
2. modelo de dominio;
|
||||
3. casos de uso;
|
||||
4. diagramas de robustez;
|
||||
5. diagramas de secuencia;
|
||||
6. diagramas de clases.
|
||||
|
||||
Los ejercicios ofrecen corrección inmediata, explicación de la respuesta y
|
||||
progreso persistente en el navegador. La base teórica se encuentra citada al
|
||||
pie de cada estación.
|
||||
|
||||
## Alcance
|
||||
|
||||
Es un prototipo educativo, no una reproducción exacta ni conectada a Odoo. Los
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Laboratorio interactivo para aprender diseño de software e ICONIX">
|
||||
<title>Ejercicios ICONIX · SAP-TFI-2026</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="ejercicios.css">
|
||||
</head>
|
||||
<body class="lab-body">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="index.html"><span class="brand-mark">S</span><span><strong>SAP · TFI</strong><small>Laboratorio ICONIX</small></span></a>
|
||||
<nav class="course lab-links"><a href="index.html">Prototipo</a><a class="active" href="ejercicios.html">Ejercicios</a></nav>
|
||||
</header>
|
||||
|
||||
<main class="lab-layout">
|
||||
<aside class="lesson-nav">
|
||||
<div><p class="eyebrow">Tu recorrido</p><h2>Del cambio a las clases</h2><div class="progress-meter"><i id="progress-bar"></i></div><small id="progress-copy">0 de 6 estaciones completadas</small></div>
|
||||
<nav id="lesson-list"></nav>
|
||||
<button id="reset-progress" class="reset-button">Reiniciar progreso</button>
|
||||
</aside>
|
||||
|
||||
<section class="lesson-stage">
|
||||
<header class="lesson-header"><div><p class="eyebrow" id="lesson-kicker">Fundamentos</p><h1 id="lesson-title">Diseñar para el cambio</h1><p id="lesson-summary"></p></div><span id="lesson-score" class="score-chip">0 / 0</span></header>
|
||||
<div id="lesson-content"></div>
|
||||
<footer class="lesson-footer"><button id="previous-lesson">← Anterior</button><button id="next-lesson" class="primary">Siguiente →</button></footer>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog id="completion-dialog"><div class="celebration">✦</div><h2>Recorrido completado</h2><p>Ya conectaste fundamentos de diseño con los cinco artefactos principales de ICONIX.</p><button id="close-dialog">Seguir practicando</button></dialog>
|
||||
<script src="ejercicios.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
const lessons=[
|
||||
{id:"fundamentos",kicker:"Fundamentos",title:"Diseñar para el cambio",summary:"Antes de dibujar clases hay que decidir por qué se divide el sistema y qué cambio intenta aislar cada parte.",theory:{title:"Idea central",text:"Diseñar es descomponer el software, asignar funciones y establecer relaciones. El criterio no es producir muchas partes: es reducir el costo de los cambios sin degradar la integridad conceptual.",concepts:[["Cambio probable","Anticipar qué requisito, algoritmo, dato o tecnología puede variar."],["Secreto del módulo","Cada módulo aísla una decisión de diseño con alta probabilidad de cambio."],["Interfaz estable","Los clientes usan servicios abstractos y no dependen de la implementación."]]},exercises:[
|
||||
{type:"choice",q:"Una empresa puede cambiar el proveedor de pagos varias veces. ¿Qué decisión sigue mejor a Parnas?",options:["Incluir condiciones del proveedor en Pedido","Aislar la integración tras una interfaz de pagos","Exponer el SDK del proveedor a todas las clases"],answer:1,why:"El proveedor es un ítem de cambio. Una interfaz estable evita propagar ese cambio."},
|
||||
{type:"choice",q:"¿Cuál es el objetivo principal de un buen diseño?",options:["Usar la mayor cantidad posible de clases","Minimizar el costo de cambio y preservar la integridad conceptual","Copiar directamente el modelo funcional"],answer:1,why:"La cantidad de clases no es el criterio; importa la descomposición frente al cambio."}
|
||||
],source:"Apuntes Una Teoría para el Diseño de Software y Diseño de Software, secciones sobre Diseño para el Cambio y DBOI."},
|
||||
{id:"dominio",kicker:"ICONIX · Modelo de dominio",title:"Encontrar conceptos, no soluciones",summary:"El modelo de dominio establece el vocabulario conceptual antes de introducir detalles técnicos.",theory:{title:"Regla de trabajo",text:"Extraé conceptos del problema, sus atributos significativos y relaciones. No confundas entidades del dominio con pantallas, controladores o tablas de una tecnología particular.",concepts:[["Concepto","Objeto reconocible del mundo del problema."],["Atributo","Propiedad significativa del concepto."],["Relación","Vínculo semántico con cardinalidad."]]},exercises:[
|
||||
{type:"multi",q:"Seleccioná sólo conceptos del dominio de ventas.",options:["Cliente","PedidoVenta","Botón Confirmar","PostgreSQL","Producto"],answer:[0,1,4],why:"Cliente, PedidoVenta y Producto pertenecen al dominio; el botón y PostgreSQL son detalles de solución."},
|
||||
{type:"choice",q:"¿Cuál relación expresa mejor el dominio?",options:["PedidoVenta 1 contiene 1..* LíneaPedido","Form llama a SQL","Controlador importa ORM"],answer:0,why:"La primera relación usa conceptos y cardinalidad sin contaminar el modelo conceptual."}
|
||||
],source:"Proceso ICONIX del repositorio: modelo de dominio conceptual y separación entre requerimientos y diseño."},
|
||||
{id:"casos",kicker:"ICONIX · Casos de uso",title:"Narrar comportamiento observable",summary:"Los casos de uso conectan objetivos de actores con respuestas del sistema y preparan el análisis de robustez.",theory:{title:"Del objetivo al flujo",text:"Nombrá el caso de uso con un verbo, identificá actor y objetivo, y redactá pasos en voz activa. Usá include para comportamiento obligatorio reutilizado y extend para comportamiento condicional.",concepts:[["Actor","Rol externo que persigue un objetivo."],["include","Subflujo obligatorio compartido."],["extend","Comportamiento opcional bajo condición."]]},exercises:[
|
||||
{type:"choice",q:"Enviar el presupuesto por email es opcional. ¿Qué relación corresponde?",options:["include","extend","generalización de actor"],answer:1,why:"El envío aparece sólo si el vendedor decide realizarlo."},
|
||||
{type:"sort",q:"Ordená el camino feliz de Crear presupuesto.",items:["El sistema calcula los totales","El vendedor selecciona el cliente","El vendedor guarda el presupuesto","El vendedor agrega productos"],answer:["El vendedor selecciona el cliente","El vendedor agrega productos","El sistema calcula los totales","El vendedor guarda el presupuesto"],why:"El orden conserva causa y efecto y luego podrá trazarse al diagrama de robustez."}
|
||||
],source:"Casos de uso CU-VEN-001 y D-CU-GEN-002 del repositorio SAP-TFI-2026."},
|
||||
{id:"robustez",kicker:"ICONIX · Robustez",title:"Separar frontera, control y entidad",summary:"El análisis de robustez verifica que el texto del caso de uso pueda convertirse en colaboraciones de objetos.",theory:{title:"Tres responsabilidades",text:"El actor interactúa con fronteras; las fronteras invocan controles; los controles coordinan entidades. Evitá que la interfaz manipule directamente la persistencia o que una entidad asuma decisiones de presentación.",concepts:[["Boundary","Pantalla, formulario, API o mensaje visible."],["Control","Coordinador de un flujo o regla de aplicación."],["Entity","Concepto persistente con estado del dominio."]]},exercises:[
|
||||
{type:"classify",q:"Clasificá cada elemento del flujo Confirmar pedido.",items:[["Botón Confirmar","Boundary"],["action_confirm()","Control"],["sale.order","Entity"]],options:["Boundary","Control","Entity"],why:"La clasificación mantiene separadas interacción, coordinación y estado."},
|
||||
{type:"choice",q:"¿Qué interacción revela mayor acoplamiento indebido?",options:["Actor → Formulario","Formulario → Control","Formulario → PostgreSQL"],answer:2,why:"La frontera conoce un detalle de persistencia y saltea el control y las entidades."}
|
||||
],source:"Diagramas D-ROB del repositorio y principio de bajo acoplamiento del apunte Diseño de Software."},
|
||||
{id:"secuencia",kicker:"ICONIX · Secuencia",title:"Asignar mensajes en el tiempo",summary:"El diagrama de secuencia refina el flujo de robustez y asigna operaciones concretas a participantes.",theory:{title:"Trazabilidad temporal",text:"Cada mensaje debe derivarse de un paso del caso de uso y de una interacción de robustez. Las alternativas se representan con condiciones explícitas y las operaciones se asignan al objeto que posee la responsabilidad.",concepts:[["Lifeline","Participante que existe durante la interacción."],["Mensaje","Solicitud de un servicio en orden temporal."],["alt / opt","Variación condicional del flujo."]]},exercises:[
|
||||
{type:"sort",q:"Ordená los mensajes de Confirmar pedido.",items:["Stock crea picking","Formulario invoca action_confirm()","Control valida pedido","sale.order cambia a sale"],answer:["Formulario invoca action_confirm()","Control valida pedido","sale.order cambia a sale","Stock crea picking"],why:"Primero se recibe la intención, luego se valida y sólo después se modifican entidades."},
|
||||
{type:"choice",q:"Si la validación puede fallar, ¿qué fragmento UML corresponde?",options:["loop","alt / else","par"],answer:1,why:"alt / else muestra caminos mutuamente excluyentes: éxito o error."}
|
||||
],source:"Diagramas D-SEC del repositorio y trazabilidad definida por el proceso ICONIX."},
|
||||
{id:"clases",kicker:"ICONIX · Diagrama de clases",title:"Construir clases que oculten decisiones",summary:"El modelo de clases asigna datos, operaciones y relaciones sin exponer decisiones que probablemente cambien.",theory:{title:"De Parnas a UML",text:"Una clase no se justifica sólo porque exista un sustantivo: debe reunir una responsabilidad coherente y proteger un secreto. La interfaz pública debe ser pequeña, abstracta e insensible a cambios anticipados.",concepts:[["Alta cohesión","Los elementos internos contribuyen a un único secreto o responsabilidad."],["Bajo acoplamiento","Las clases conocen la menor cantidad necesaria de otras clases."],["Encapsulamiento","Los parámetros y retornos no revelan la representación interna."]]},exercises:[
|
||||
{type:"choice",q:"Un catálogo puede pasar de arreglo a base de datos. ¿Cuál interfaz oculta mejor esa decisión?",options:["Producto[] getArray()","Producto buscarPorId(IdProducto id)","Producto get(int posicion)"],answer:1,why:"Buscar por identidad expresa una necesidad del dominio sin exponer arreglo, posición ni capacidad."},
|
||||
{type:"builder",q:"Construí un diseño para descuentos variables.",tokens:["Pedido","PoliticaDescuento","BaseDatosDescuento","calcularTotal()","aplicarA(Pedido)","getFila(int)"],answer:["Pedido","PoliticaDescuento","calcularTotal()","aplicarA(Pedido)"],why:"Pedido compone una Política de Descuento mediante servicios del dominio; la tabla y el acceso por fila quedan ocultos."}
|
||||
],source:"Diseño de Software, secciones DBOI, interfaz/implementación, abstracción, encapsulamiento, cohesión/acoplamiento y diseño orientado a objetos."}
|
||||
];
|
||||
|
||||
let lessonIndex=0;
|
||||
let progress=JSON.parse(localStorage.getItem("iconix-progress")||"{}");
|
||||
const selections={};
|
||||
|
||||
function saveProgress(){localStorage.setItem("iconix-progress",JSON.stringify(progress))}
|
||||
function allCorrect(lesson){return lesson.exercises.every((_,i)=>progress[`${lesson.id}-${i}`])}
|
||||
function renderNav(){
|
||||
document.querySelector("#lesson-list").innerHTML=lessons.map((lesson,i)=>`<button class="lesson-button ${i===lessonIndex?"active":""} ${allCorrect(lesson)?"done":""}" data-index="${i}"><span>${allCorrect(lesson)?"✓":i+1}</span><strong>${lesson.title}</strong><small>${lesson.kicker.replace("ICONIX · ","")}</small></button>`).join("");
|
||||
const completed=lessons.filter(allCorrect).length;
|
||||
document.querySelector("#progress-bar").style.width=`${completed/lessons.length*100}%`;
|
||||
document.querySelector("#progress-copy").textContent=`${completed} de ${lessons.length} estaciones completadas`;
|
||||
}
|
||||
function renderLesson(){
|
||||
const lesson=lessons[lessonIndex];
|
||||
document.querySelector("#lesson-kicker").textContent=lesson.kicker;document.querySelector("#lesson-title").textContent=lesson.title;document.querySelector("#lesson-summary").textContent=lesson.summary;
|
||||
document.querySelector("#lesson-content").innerHTML=`<article class="theory-card"><p class="eyebrow">${lesson.theory.title}</p><p>${lesson.theory.text}</p></article><div class="concept-grid">${lesson.theory.concepts.map(c=>`<article class="concept-card"><b>${c[0]}</b><p>${c[1]}</p></article>`).join("")}</div>${lesson.exercises.map((e,i)=>exerciseHTML(e,i,lesson.id)).join("")}<p class="source-note"><b>Base conceptual:</b> ${lesson.source} Fuentes: <a href="https://www.fceia.unr.edu.ar/ingsoft/intro-diseno.pdf" target="_blank" rel="noreferrer">Introducción al diseño ↗</a> · <a href="https://www.fceia.unr.edu.ar/ingsoft/diseno-a.pdf" target="_blank" rel="noreferrer">Diseño y ocultamiento de información ↗</a></p>`;
|
||||
document.querySelector("#previous-lesson").disabled=lessonIndex===0;document.querySelector("#next-lesson").textContent=lessonIndex===lessons.length-1?"Finalizar ✓":"Siguiente →";
|
||||
updateScore();renderNav();
|
||||
}
|
||||
function exerciseHTML(e,i,id){
|
||||
const common=`<header><div><h3>${i+1}. ${e.q}</h3></div><span class="exercise-type">${({choice:"Una respuesta",multi:"Selección múltiple",sort:"Ordenar",classify:"Clasificar",builder:"Construir"})[e.type]}</span></header>`;
|
||||
let body="";
|
||||
if(e.type==="choice"||e.type==="multi")body=`<div class="options">${e.options.map((o,n)=>`<button class="option" data-option="${n}"><span>${e.type==="multi"?"□":"○"}</span>${o}</button>`).join("")}</div>`;
|
||||
if(e.type==="sort")body=`<div class="sort-list">${e.items.map((item,n)=>`<div class="sort-item" data-value="${item}"><i>${n+1}</i><span>${item}</span><div class="sort-controls"><button data-move="up">↑</button><button data-move="down">↓</button></div></div>`).join("")}</div>`;
|
||||
if(e.type==="classify")body=`<div class="options">${e.items.map((item,n)=>`<label class="option">${item[0]}<select data-classify="${n}"><option value="">Elegir…</option>${e.options.map(o=>`<option>${o}</option>`).join("")}</select></label>`).join("")}</div>`;
|
||||
if(e.type==="builder")body=`<div class="builder"><div class="palette"><h4>Elementos disponibles</h4>${e.tokens.map((t,n)=>`<button class="token" data-token="${n}">${t}</button>`).join("")}</div><div class="canvas"><h4>Tu diagrama</h4><div class="uml-preview" data-preview><p>Elegí clases y operaciones.</p></div></div></div>`;
|
||||
return `<article class="exercise-card" data-exercise="${i}" data-type="${e.type}" data-lesson="${id}">${common}${body}<button class="check-button" data-check>Comprobar</button><div class="feedback"></div></article>`
|
||||
}
|
||||
function updateScore(){const lesson=lessons[lessonIndex];const score=lesson.exercises.filter((_,i)=>progress[`${lesson.id}-${i}`]).length;document.querySelector("#lesson-score").textContent=`${score} / ${lesson.exercises.length}`}
|
||||
function checkExercise(card){
|
||||
const lesson=lessons[lessonIndex],i=Number(card.dataset.exercise),e=lesson.exercises[i];let correct=false;
|
||||
if(e.type==="choice")correct=Number(card.querySelector(".option.selected")?.dataset.option)===e.answer;
|
||||
if(e.type==="multi"){const got=[...card.querySelectorAll(".option.selected")].map(x=>Number(x.dataset.option)).sort();correct=JSON.stringify(got)===JSON.stringify(e.answer)}
|
||||
if(e.type==="sort")correct=JSON.stringify([...card.querySelectorAll(".sort-item")].map(x=>x.dataset.value))===JSON.stringify(e.answer);
|
||||
if(e.type==="classify")correct=[...card.querySelectorAll("select")].every((x,n)=>x.value===e.items[n][1]);
|
||||
if(e.type==="builder"){const got=selections[`${lesson.id}-${i}`]||[];correct=e.answer.every(x=>got.includes(x))&&got.every(x=>e.answer.includes(x))}
|
||||
const feedback=card.querySelector(".feedback");feedback.className=`feedback visible ${correct?"success":"error"}`;feedback.textContent=`${correct?"✓ Correcto.":"Todavía no."} ${e.why}`;
|
||||
progress[`${lesson.id}-${i}`]=correct;saveProgress();updateScore();renderNav();
|
||||
}
|
||||
document.querySelector("#lesson-content").addEventListener("click",event=>{
|
||||
const card=event.target.closest(".exercise-card");if(!card)return;
|
||||
if(event.target.closest("[data-option]")){const option=event.target.closest("[data-option]");if(card.dataset.type==="choice")card.querySelectorAll(".option").forEach(o=>o.classList.remove("selected"));option.classList.toggle("selected")}
|
||||
const mover=event.target.closest("[data-move]");if(mover){const row=mover.closest(".sort-item"),sibling=mover.dataset.move==="up"?row.previousElementSibling:row.nextElementSibling;if(sibling)row.parentElement.insertBefore(mover.dataset.move==="up"?row:sibling,mover.dataset.move==="up"?sibling:row)}
|
||||
const token=event.target.closest("[data-token]");if(token){const key=`${lessons[lessonIndex].id}-${card.dataset.exercise}`,value=lessons[lessonIndex].exercises[card.dataset.exercise].tokens[token.dataset.token];selections[key]=selections[key]||[];selections[key]=selections[key].includes(value)?selections[key].filter(x=>x!==value):[...selections[key],value];token.classList.toggle("active");card.querySelector("[data-preview]").innerHTML=builderPreview(selections[key])}
|
||||
if(event.target.closest("[data-check]"))checkExercise(card);
|
||||
});
|
||||
function builderPreview(items){const classes=items.filter(x=>!x.includes("(")&&!x.startsWith("Base"));const ops=items.filter(x=>x.includes("("));if(!items.length)return"<p>Elegí clases y operaciones.</p>";return classes.map((c,i)=>`<div class="uml-box"><strong>${c}</strong><div>${ops[i]||"+ interfaz abstracta"}</div></div>`).join('<div class="relationship">◇── usa ──▷</div>')}
|
||||
document.querySelector("#lesson-list").addEventListener("click",e=>{const b=e.target.closest("[data-index]");if(b){lessonIndex=Number(b.dataset.index);renderLesson()}});
|
||||
document.querySelector("#previous-lesson").addEventListener("click",()=>{if(lessonIndex>0){lessonIndex--;renderLesson();scrollTo(0,0)}});
|
||||
document.querySelector("#next-lesson").addEventListener("click",()=>{if(lessonIndex<lessons.length-1){lessonIndex++;renderLesson();scrollTo(0,0)}else document.querySelector("#completion-dialog").showModal()});
|
||||
document.querySelector("#reset-progress").addEventListener("click",()=>{progress={};saveProgress();renderLesson()});document.querySelector("#close-dialog").addEventListener("click",()=>document.querySelector("#completion-dialog").close());
|
||||
renderLesson();
|
||||
@@ -13,7 +13,7 @@
|
||||
<span class="brand-mark">S</span>
|
||||
<span><strong>SAP · TFI</strong><small>Laboratorio ICONIX</small></span>
|
||||
</a>
|
||||
<div class="course"><span>UAI · Ingeniería</span><span class="status-dot">Prototipo educativo</span></div>
|
||||
<div class="course lab-links"><a class="active" href="index.html">Prototipo</a><a href="ejercicios.html">Ejercicios</a></div>
|
||||
</header>
|
||||
|
||||
<main class="workspace">
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user