Skip to main content

Diseño de sistemas y documentación de aplicaciones web

Cómo Planificar una App Web para que Sea Mantenible

El diseño de sistemas es la fase en la que se define cómo estará construido un software antes de implementarlo: cómo se divide en partes, cómo se comunican esas partes y qué responsabilidades asume cada una. No es un ejercicio de dibujo, es una decisión técnica que determina si el proyecto será mantenible o si quedará atrapado en dependencias y cambios peligrosos.

La documentación acompaña al diseño porque el software no se mantiene solo con código. Se mantiene con decisiones explícitas: qué se pretendía, qué reglas se siguieron, cómo se organiza el repositorio y cómo se despliega. En un proyecto web con Node.js, Express y SQLite, esto marca la diferencia entre un proyecto que puede continuar otro desarrollador y uno que muere cuando cambia el equipo.

Diseñar un sistema es como diseñar un edificio público.

Hay que definir accesos, zonas y normas antes de construir.

La documentación son los planos y el manual de mantenimiento.

Sin planos, cualquier reforma futura se convierte en una demolición parcial.

Fundamentos del diseño de software

Diseñar software consiste en dividir un sistema en piezas con responsabilidades claras. Cuando esas piezas están bien definidas, el sistema es modificable: se pueden cambiar partes sin tocarlo todo. En una aplicación web, esto afecta directamente a cómo separas rutas, controladores, servicios, acceso a SQLite y lógica de validación.

La modularidad significa que el sistema se construye por módulos independientes. En la práctica, es lo que evita que un archivo “server.mjs” termine conteniendo rutas, SQL, validaciones, renderizado y lógica de negocio mezclados. La abstracción consiste en definir interfaces internas estables: una capa “servicios” expone funciones claras, sin obligar a que el resto del sistema conozca los detalles de SQLite.

El encapsulamiento implica que cada módulo protege su estado y su lógica, evitando accesos directos e inconsistentes. En web esto se traduce en que nadie debería manipular directamente la base de datos desde cualquier punto del código; se accede a través de una capa específica. La responsabilidad única define que cada módulo tenga una sola razón para cambiar: si un módulo cambia por múltiples motivos, se convierte en un punto frágil.

El principio abierto/cerrado se aplica cuando el diseño permite añadir capacidades sin reescribir lo ya estable. En proyectos web esto se ve cuando puedes añadir un nuevo caso de uso (por ejemplo, “exportar informes”) sin romper rutas existentes ni duplicar lógica en diez archivos.

Mapeo de responsabililidaes simplificado- Eldoria Chronicles

Sistema: Gestor de Personajes RPG (Versión Simplificada)

Objetivo didáctico: Mostrar separación de responsabilidades básica para estudiantes/junior developers.

1. Estructura de carpetas

eldoria-api/
├── 📁 routes/ # Control de entrada: Definir URLs
├── 📁 controllers/ # Coordinación: Conectar rutas con servicios
├── 📁 services/ # Dominio: Reglas de negocio del juego
├── 📁 models/ # Dominio: Estructura de datos
├── 📁 repositories/ # Persistencia: Acceso a base de datos
└── 📁 db/ # Infraestructura: Conexión BD + config

2. Responsabilidad por carpetas (una sentencia cada carpeta)

CarpetaResponsabilidad Única¿Qué NO hace?
routes/Recibir peticiones HTTP y redirigirlas al controlador correctoNo valida datos, no hace lógica de negocio
controllers/Recibir datos de la ruta, llamar al servicio, devolver respuesta HTTPNo contiene reglas de negocio, no toca la BD
services/Aplicar reglas del juego (ej: nivel máximo, stats válidos)No sabe de HTTP, no sabe de SQL
models/Definir cómo se ve un personaje/arma (atributos y métodos básicos)No guarda en BD, no maneja requests
repositories/Guardar/leer datos de la base de datosNo aplica reglas de negocio
db/Conectar con SQLite y ejecutar migracionesNo sabe nada del juego

3. Ejemplo concreto: “Quiero subir el nivel de un personaje”

Flujo paso a paso:

Usuario → POST /characters/1/level-up

routes/characterRoutes.js → "Llama al controlador"

controllers/characterController.js → "Extrae datos, llama al servicio"

services/characterService.js → "Aplica reglas: ¿Puede subir nivel?"

repositories/characterRepository.js → "Guarda nuevo nivel en BD"

db/connection.js → "Ejecuta SQL: UPDATE characters SET level = ?"

4. Código minimalista por capa

routes/characterRoutes.js (Control de entrada)

// Solo rutas, nada más
const express = require('express');
const router = express.Router();
const characterController = require('../controllers/characterController');

router.post('/:id/level-up', characterController.levelUp);
router.get('/:id', characterController.getCharacter);
router.post('/', characterController.createCharacter);

module.exports = router;

controllers/characterController.js (Coordinación)

// Solo coordina, no hace lógica
const characterService = require('../services/characterService');

exports.levelUp = async (req, res) => {
try {
const characterId = req.params.id;
const updatedCharacter = await characterService.levelUp(characterId);
res.json(updatedCharacter); // Solo envía respuesta
} catch (error) {
res.status(400).json({ error: error.message });
}
};

exports.getCharacter = async (req, res) => {
const character = await characterService.getById(req.params.id);
res.json(character);
};

services/characterService.js (Dominio - Reglas del juego)

// Aquí está la lógica REAL del juego
const characterRepository = require('../repositories/characterRepository');

exports.levelUp = async (characterId) => {
// 1. Obtener personaje
const character = await characterRepository.findById(characterId);

// 2. REGLA DE NEGOCIO: Nivel máximo 20
if (character.level >= 20) {
throw new Error('Nivel máximo alcanzado (20)');
}

// 3. REGLA DE NEGOCIO: Subir stats al subir nivel
character.level += 1;
character.health += 10;
character.attack += 2;

// 4. Guardar cambios
return await characterRepository.update(character);
};

exports.getById = async (id) => {
return await characterRepository.findById(id);
};

models/Character.js (Dominio - Estructura)

// Define QUÉ es un personaje, no CÓMO se guarda
class Character {
constructor(id, name, level, health, attack, classType) {
this.id = id;
this.name = name;
this.level = level;
this.health = health;
this.attack = attack;
this.classType = classType; // 'warrior', 'mage', 'rogue'
}

// Método de DOMINIO (regla del juego)
calculateDamage() {
let damage = this.attack;

// REGLA: Los guerreros hacen +50% de daño cuerpo a cuerpo
if (this.classType === 'warrior') {
damage *= 1.5;
}

return damage;
}
}

module.exports = Character;

repositories/characterRepository.js (Persistencia)

// Solo sabe de SQL, no de reglas del juego
const db = require('../db/connection');
const Character = require('../models/Character');

exports.findById = async (id) => {
const row = await db.get('SELECT * FROM characters WHERE id = ?', [id]);

// Convierte fila BD a objeto Character
return new Character(
row.id,
row.name,
row.level,
row.health,
row.attack,
row.class_type
);
};

exports.update = async (character) => {
// Solo ejecuta SQL, no valida nada
await db.run(
'UPDATE characters SET level = ?, health = ?, attack = ? WHERE id = ?',
[character.level, character.health, character.attack, character.id]
);

return character;
};

db/connection.js (Infraestructura)

// Solo configuración técnica
const sqlite3 = require('sqlite3').verbose();

const db = new sqlite3.Database('./eldoria.db');

// Métodos helper simples
db.get = (sql, params) => {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
};

db.run = (sql, params) => {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
};

module.exports = db;

5. Diagrama visual sencillo

6. Ejemplo práctico: Crear un nuevo personaje dentro del juego

Paso a paso en código:

// 1. Usuario envía: POST /characters
// Body: { "name": "Aragorn", "classType": "warrior" }

// 2. routes/characterRoutes.js
router.post('/', characterController.createCharacter);

// 3. controllers/characterController.js
exports.createCharacter = async (req, res) => {
const { name, classType } = req.body;
const newChar = await characterService.create(name, classType);
res.status(201).json(newChar);
};

// 4. services/characterService.js
exports.create = async (name, classType) => {
// REGLA: Nombres entre 2-50 caracteres
if (name.length < 2 || name.length > 50) {
throw new Error('Nombre debe tener 2-50 caracteres');
}

// REGLA: Solo clases válidas
const validClasses = ['warrior', 'mage', 'rogue'];
if (!validClasses.includes(classType)) {
throw new Error('Clase inválida');
}

// Crear personaje con stats iniciales
const character = new Character(
null, // ID lo genera BD
name,
1, // Nivel inicial
100, // Salud inicial
10, // Ataque inicial
classType
);

return await characterRepository.save(character);
};

// 5. repositories/characterRepository.js
exports.save = async (character) => {
const result = await db.run(
'INSERT INTO characters (name, level, health, attack, class_type) VALUES (?, ?, ?, ?, ?)',
[character.name, character.level, character.health, character.attack, character.classType]
);

character.id = result.lastID;
return character;
};

7. Analogía

Imagina un restaurante:

CarpetaEn restauranteResponsabilidad
routes/HostessRecibe al cliente, lo lleva a la mesa
controllers/CamareroToma pedido, lo lleva a cocina, trae comida
services/ChefCocina siguiendo recetas (reglas)
models/RecetasDefine cómo es una pizza (ingredientes)
repositories/AlmacénBusca ingredientes en nevera/despensa
db/Nevera/DespensaDonde están los ingredientes

Flujo:

  1. Hostess (routes) recibe cliente
  2. Camarero (controller) toma pedido de "pizza"
  3. Chef (service) cocina siguiendo receta (model)
  4. Chef pide ingredientes al almacén (repository)
  5. Almacén los saca de la nevera (db)
  6. Camarero lleva pizza al cliente

8. Preguntas de compresión

Pregunta 1: ¿Dónde pondrías esta validación?

"Los magos no pueden usar armadura pesada"

Respuesta: En services/ - Es regla de negocio del juego.

Pregunta 2: ¿Dónde pondrías esto?

app.use(cors()) para permitir peticiones de otros dominios

Respuesta: En app.js (fuera de carpetas) - Es configuración de infraestructura.

Pregunta 3: ¿Qué pasa si cambio SQLite por MySQL?

Respuesta: Solo toco db/connection.js - Las otras capas no se enteran.

Pregunta 4: ¿Y si añado un nuevo campo "mana" a los personajes?

Respuesta:

  1. models/Character.js - Añadir propiedad mana
  2. repositories/ - Incluir en consultas SQL
  3. services/ - Usar mana en reglas (ej: hechizos consumen mana)

9. Reglas de oro (Para recordar)

  1. Routes → Solo URLs
  2. Controllers → Solo coordinan (no piensan)
  3. Services → Aquí está la inteligencia (reglas del juego)
  4. Models → Definen "qué es" cada cosa
  5. Repositories → Solo hablan con BD
  6. DB → Solo conexión y SQL básico

10. Beneficios de esta estructura sencilla

  • Fácil de entender para estudiantes
  • Fácil de cambiar (ej: cambiar BD solo toca 1 archivo)
  • Fácil de testear (services testables sin HTTP/BD)
  • Fácil de escalar (añadir nueva funcionalidad = añadir carpeta similar)

Para proyectos reales se añaden más capas, pero esta es la base fundamental que todo desarrollador debe entender primero.

Modelos y patrones usados en diseño de sistemas web

Un modelo de arquitectura es una decisión de alto nivel sobre cómo se organiza la aplicación completa.

Un patrón de diseño es una solución recurrente para un problema más localizado. Confundirlos es habitual en principiantes: la arquitectura define el “mapa” del sistema; los patrones resuelven “puntos” concretos dentro de ese mapa.

En una aplicación web habitual, el modelo cliente-servidor es el estándar: el navegador solicita recursos y el servidor responde.

Dentro del servidor, una arquitectura monolítica es frecuente en proyectos pequeños: todo el backend está en un solo proceso. Esto no es malo, siempre que haya modularidad interna.

Microservicios es una arquitectura distribuida: separa funcionalidades en servicios independientes, pero incrementa la complejidad operativa y de comunicación. No se elige por moda, se elige por necesidad real.

Microservicios VS. Monolítico: Complejidad y Comunicación

El diseño basado en componentes se aplica con fuerza en frontends modernos: la interfaz se construye con piezas reutilizables. En backend, el equivalente práctico es la modularización por capas y módulos. Los sistemas basados en eventos son útiles cuando muchas partes deben reaccionar a cambios sin acoplarse directamente, pero también dificultan el seguimiento de errores si se abusa.

Estrategias de desarrollo de software

A nivel de patrones, en web hay que usar criterio. Singleton, Factory, Observer, Decorator y Strategy existen, pero su valor depende del contexto. En Node.js moderno, por ejemplo, “una conexión única” suele resolverse con un módulo de base de datos importado y compartido, sin forzar patrones clásicos de POO. El objetivo es siempre el mismo: reducir acoplamiento y evitar duplicación, no “meter patrones”.

Ejemplo del patrón Singleton en Eldoria Chronicles

El principio es bien claro. Una clase solo tiene una instancia y proporciona un punto de acceso global a ella. Es como el Director de una escuela: solo hay uno, y todos van a él.

Situación: El oráculo místico del bosque.

En Eldoria, hay un Oráculo Místico que solo puede ser consultado 3 veces por día en todo el reino. Todos los aventureros deben usar el mismo oráculo.

Analogía Visual

Explicación: Todos los aventureros acceden al mismo oráculo. Cuando llega al límite de 3 consultas, nadie más puede consultar ese día.

Implementación sencilla

class OracleSingleton {
// La única instancia que existirá
static instance = null;

constructor() {
// Si ya existe, devuelve la existente
if (OracleSingleton.instance) {
return OracleSingleton.instance;
}

// Solo se ejecuta la primera vez
this.consultasHoy = 0;
this.maxConsultas = 3;

// Guarda esta instancia como la única
OracleSingleton.instance = this;
}

consultar(pregunta) {
if (this.consultasHoy >= this.maxConsultas) {
return "El oráculo está agotado. Vuelve mañana.";
}

this.consultasHoy++;
return `El oráculo responde: ${pregunta}`;
}
}

Cómo se usa en el juego

// En diferentes partes del mapa...

// En la Posada del Pueblo
const oraculo1 = new OracleSingleton();
const respuesta1 = oraculo1.consultar("¿Dónde está el tesoro?");
// consultasHoy: 1

// En las Montañas del Norte
const oraculo2 = new OracleSingleton(); // ¡MISMO oráculo!
const respuesta2 = oraculo2.consultar("¿Cómo vencer al dragón?");
// consultasHoy: 2 (del MISMO oráculo)

// En las Ruinas Antiguas
const oraculo3 = new OracleSingleton(); // ¡MISMO oráculo!
const respuesta3 = oraculo3.consultar("¿Dónde está mi aliado?");
// consultasHoy: 3

// En el Castillo Real
const oraculo4 = new OracleSingleton(); // ¡MISMO oráculo!
const respuesta4 = oraculo4.consultar("Otra pregunta");
// "El oráculo está agotado. Vuelve mañana."

Por qué es Singleton

NECESIDAD: "Solo un oráculo en todo el reino"

PROBLEMA SIN SINGLETON:
- Aventurero 1 crea oráculo → límite: 3 consultas
- Aventurero 2 crea OTRO oráculo → límite: 3 consultas más
- Aventurero 3 crea OTRO oráculo → límite: 3 consultas más
- ¡Cada uno tiene 3 consultas! → Total: 9 consultas

SOLUCIÓN CON SINGLETON:
- Todos obtienen el MISMO oráculo
- Un solo contador: consultasHoy
- Límite REAL de 3 consultas para TODOS

Analogía sencilla

El Singleton es como...

  • El rey del reino → Solo hay uno
  • El sol → Todos ven el mismo
  • El reloj del pueblo → Todos miran la misma hora
  • El pozo de agua → Todos beben del mismo

En programación:

  • Conexión a base de datos → Una sola para toda la app
  • Configuración global → Todos leen la misma configuración
  • Logger del sistema → Todos escriben en el mismo log
  • Cache compartida → Todos usan la misma caché

Ejemplo del patrón Factory en Eldoria Chronicles

El principio es claro. Una clase crea objetos sin especificar su clase concreta. Es como la armería del reino: pides un arma, y el herrero te da la adecuada según tu clase.

Situación: La forja de armas del herrero real

En Eldoria, el Herrero Real fabrica armas según el tipo de personaje. No necesitas saber cómo se forja cada arma, solo decir qué necesitas.

Analogía Visual

Explicación: El personaje solo dice su clase. El herrero (factory) decide qué arma crear, sin que el personaje sepa los detalles de fabricación.

Implementación sencilla

// Interfaz base para armas
class Arma {
usar() {
return "Arma genérica";
}
}

// Armas concretas
class Espada extends Arma {
usar() {
return "¡Corte con espada! Daño: 15";
}
}

class Baculo extends Arma {
usar() {
return "¡Lanza hechizo! Daño mágico: 20";
}
}

class Arco extends Arma {
usar() {
return "¡Dispara flecha! Daño: 12, Alcance: largo";
}
}

// La Factory (Herrero)
class HerreroFactory {
crearArma(tipoPersonaje) {
switch(tipoPersonaje.toLowerCase()) {
case 'guerrero':
return new Espada();
case 'mago':
return new Baculo();
case 'arquero':
return new Arco();
default:
throw new Error("Clase de personaje no reconocida");
}
}
}

Cómo se usa en el juego

// Personajes obtienen sus armas iniciales
const herrero = new HerreroFactory();

// Guerrero comienza su aventura
const guerrero = herrero.crearArma('guerrero');
console.log(guerrero.usar()); // "¡Corte con espada! Daño: 15"

// Mago llega a la forja
const mago = herrero.crearArma('mago');
console.log(mago.usar()); // "¡Lanza hechizo! Daño mágico: 20"

// Arquero necesita equipo
const arquero = herrero.crearArma('arquero');
console.log(arquero.usar()); // "¡Dispara flecha! Daño: 12, Alcance: largo"

// NPC que otorga misiones también usa la factory
const armaParaMision = herrero.crearArma('guerrero');
// Entrega la espada al jugador para una misión especial

Por qué es Factory

NECESIDAD: "Crear armas adecuadas sin exponer la lógica de creación"

PROBLEMA SIN FACTORY:
- Guerrero: const arma = new Espada()
- Mago: const arma = new Baculo()
- Arquero: const arma = new Arco()
- ¡Cada personaje debe conocer TODAS las clases de armas!

SOLUCIÓN CON FACTORY:
- Personaje: const arma = herrero.crearArma(miClase)
- Factory decide QUÉ arma crear
- Personaje solo usa el arma, no sabe cómo se creó

Beneficios en el juego

  1. Nueva clase "Sacerdote" añadida:

    class Cetro extends Arma {
    usar() {
    return "¡Bendice aliados! Curación: 25";
    }
    }

    // Solo modificar la Factory:
    class HerreroFactory {
    crearArma(tipoPersonaje) {
    switch(tipoPersonaje.toLowerCase()) {
    // ... casos anteriores
    case 'sacerdote':
    return new Cetro(); // ¡Nuevo!
    default:
    throw new Error("Clase no reconocida");
    }
    }
    }

  2. Armas especiales por región:

    class HerreroRegionFactory extends HerreroFactory {
    crearArma(tipoPersonaje, region) {
    let arma = super.crearArma(tipoPersonaje);

    if (region === 'desierto') {
    // Armas adaptadas al desierto
    return this._adaptarParaDesierto(arma);
    }

    return arma;
    }
    }

Analogía sencilla

El Factory es como...

  • Restaurante de comida rápida → Pides "combo #1", te traen hamburguesa+patatas+refresco
  • Concesionario de coches → Pides "familiar", te dan minivan con 7 asientos
  • Impresora 3D → Subes diseño, imprime el objeto correcto

En programación:

  • Conexiones a bases de datos → Factory crea MySQL, PostgreSQL o SQLite según configuración
  • Loggers → Factory crea logger para consola, archivo o servicio web
  • Parsers → Factory crea parser para JSON, XML o YAML según el archivo

Variante: Factory Method

// Cada tipo de herrero especializado
class HerreroDeElfos {
crearArma() {
return new ArcoElfico(); // Arco especial élfico
}
}

class HerreroDeEnanos {
crearArma() {
return new HachaEnana(); // Hacha pesada enana
}
}

// Uso según la raza del personaje
function obtenerHerrero(raza) {
if (raza === 'elfo') return new HerreroDeElfos();
if (raza === 'enano') return new HerreroDeEnanos();
return new HerreroFactory(); // Herrero humano por defecto
}

Resumen del Factory en Eldoria

El herrero (Factory) sabe:

  • Qué materiales usar para cada arma
  • Cómo forjar cada tipo de arma
  • Qué arma es mejor para cada clase

El aventurero (Cliente) solo sabe:

  • "Soy un guerrero, necesito un arma"
  • Usar el arma que le den

Ventaja: Si añadimos una nueva clase (ej: "Lanzador"), solo el herrero necesita aprender a hacer lanzas. Los lanzadores solo piden "un arma para lanzador".

Ejemplo del patrón Observer en Eldoria Chronicles

El principio es claro. Un objeto notifica cambios a múltiples observadores automáticamente. Es como el sistema de campanas del castillo: cuando suena, todos los guardias, ciudadanos y sirvientes reaccionan.

Situación: El sistema de alertas del reino

En Eldoria, cuando el Dragón del Norte despierta, múltiples sistemas deben reaccionar automáticamente sin que cada uno tenga que verificar constantemente.

Analogía Visual

Explicación: Cuando el dragón despierta (cambio de estado), todos los sistemas registrados (observadores) son notificados automáticamente y reaccionan.

Implementación sencilla

// Sujeto Observable (lo que se observa)
class DragonObservable {
constructor() {
this.observadores = [];
this.estaDespierto = false;
}

// Métodos para manejar observadores
agregarObservador(observador) {
this.observadores.push(observador);
}

eliminarObservador(observador) {
const index = this.observadores.indexOf(observador);
if (index > -1) {
this.observadores.splice(index, 1);
}
}

notificarObservadores() {
for (const observador of this.observadores) {
observador.actualizar(this.estaDespierto);
}
}

// Cambia el estado y notifica
despertar() {
console.log("¡El Dragón del Norte ha despertado!");
this.estaDespierto = true;
this.notificarObservadores();
}

dormir() {
console.log("El dragón vuelve a dormir...");
this.estaDespierto = false;
this.notificarObservadores();
}
}

// Observadores (los que reaccionan)
class SistemaAlarmas {
actualizar(dragonDespierto) {
if (dragonDespierto) {
console.log("Sistema de Alarmas: ¡CAMPANAS DE ALERTA!");
} else {
console.log("Sistema de Alarmas: Alarma desactivada");
}
}
}

class GuardiasReales {
actualizar(dragonDespierto) {
if (dragonDespierto) {
console.log("Guardias Reales: ¡A las armas! Preparad las defensas.");
} else {
console.log("Guardias Reales: Guardia normal restaurada");
}
}
}

class TablonMisiones {
actualizar(dragonDespierto) {
if (dragonDespierto) {
console.log("Tablón de Misiones: MISIÓN AÑADIDA - 'Derrotar al Dragón del Norte'");
} else {
console.log("Tablón de Misiones: Misión del dragón completada");
}
}
}

Cómo se usa en el juego

// Configuración inicial del sistema
const dragonDelNorte = new DragonObservable();

// Registramos todos los sistemas que deben reaccionar
const alarmas = new SistemaAlarmas();
const guardias = new GuardiasReales();
const misiones = new TablonMisiones();

dragonDelNorte.agregarObservador(alarmas);
dragonDelNorte.agregarObservador(guardias);
dragonDelNorte.agregarObservador(misiones);

// Evento en el juego: el dragón despierta
console.log("=== EL DRAGÓN DESPIERTA ===");
dragonDelNorte.despertar();
// Salida:
// ¡El Dragón del Norte ha despertado!
// Sistema de Alarmas: ¡CAMPANAS DE ALERTA!
// Guardias Reales: ¡A las armas! Preparad las defensas.
// Tablón de Misiones: MISIÓN AÑADIDA - 'Derrotar al Dragón del Norte'

// El jugador derrota al dragón
console.log("\\n=== EL DRAGÓN ES DERROTADO ===");
dragonDelNorte.dormir();
// Salida:
// El dragón vuelve a dormir...
// Sistema de Alarmas: Alarma desactivada
// Guardias Reales: Guardia normal restaurada
// Tablón de Misiones: Misión del dragón completada

Por qué es Observer

NECESIDAD: "Múltiples sistemas deben reaccionar a un evento"

PROBLEMA SIN OBSERVER:
- Sistema de alarmas: if (dragon.estaDespierto) sonarAlarma()
- Guardias: if (dragon.estaDespierto) prepararDefensas()
- Misiones: if (dragon.estaDespierto) añadirMision()
- ¡Cada sistema pregunta constantemente!

SOLUCIÓN CON OBSERVER:
- Dragón: "Me despierto" → notificaATodos()
- Cada sistema recibe notificación automática
- Dragón no necesita conocer a cada sistema

Analogía sencilla

El Observer es como...

  • Sistema de suscripciones a newsletter → Nuevo artículo → todos los suscriptores reciben email
  • Grupo de WhatsApp familiar → Un mensaje → todos los miembros lo ven
  • Alarma de incendios → Se activa → luces, sprinklers y alarmas suenan

En programación:

  • Eventos de UI → Click en botón → múltiples handlers se ejecutan
  • Sistema de logs → Error ocurre → log a consola, archivo y servicio de monitoreo
  • Conexiones en tiempo real → Mensaje recibido → actualizar chat, notificaciones y estado

Resumen del Observer en Eldoria

El dragón (Observable) solo hace:

  • Mantener lista de quién quiere enterarse
  • Notificar a todos cuando algo cambia

Los sistemas (Observadores) solo hacen:

  • Registrarse para recibir notificaciones
  • Reaccionar cuando son notificados

Ventaja: Si el mago de la corte quiere añadir un hechizo de protección automático cuando el dragón despierte, solo necesita registrarse como observador. El dragón no necesita modificación alguna.

Ejemplo del patrón Decorator en Eldoria Chronicles

El principio es claro. Agrega funcionalidad a un objeto de forma dinámica sin modificar su estructura. Es como engarzar gemas mágicas en un arma: la espada base sigue siendo una espada, pero ahora tiene poderes adicionales.

Situación: El sistema de mejoras de armas del herrero

En Eldoria, los aventureros pueden mejorar sus armas añadiendo gemas mágicas que otorgan efectos especiales, sin tener que crear una nueva arma desde cero.

Analogía Visual

Explicación: Partimos de una espada básica y le añadimos gemas (decoradores) que agregan efectos sin cambiar la espada original.

Implementación sencilla

// Componente base: el arma
class Arma {
constructor() {
this.descripcion = "Arma básica";
this.dano = 10;
}

obtenerDescripcion() {
return this.descripcion;
}

calcularDano() {
return this.dano;
}
}

// Decorador base
class GemaDecorator extends Arma {
constructor(arma) {
super();
this.arma = arma;
}

obtenerDescripcion() {
return this.arma.obtenerDescripcion();
}

calcularDano() {
return this.arma.calcularDano();
}
}

// Decoradores concretos (gemas mágicas)
class GemaFuego extends GemaDecorator {
obtenerDescripcion() {
return this.arma.obtenerDescripcion() + " + Gema de Fuego";
}

calcularDano() {
return this.arma.calcularDano() + 5; // +5 daño de fuego
}

efectoFuego() {
return "Inflige quemaduras durante 3 segundos";
}
}

class GemaHielo extends GemaDecorator {
obtenerDescripcion() {
return this.arma.obtenerDescripcion() + " + Gema de Hielo";
}

calcularDano() {
return this.arma.calcularDano() + 3; // +3 daño de hielo
}

efectoHielo() {
return "Ralentiza al enemigo 50% por 2 segundos";
}
}

class GemaElectrica extends GemaDecorator {
obtenerDescripcion() {
return this.arma.obtenerDescripcion() + " + Gema Eléctrica";
}

calcularDano() {
return this.arma.calcularDano() + 4; // +4 daño eléctrico
}

efectoElectrocutar() {
return "Daño en área a enemigos cercanos";
}
}

Cómo se usa en el juego

// Crear una espada básica
let miEspada = new Arma();
console.log(miEspada.obtenerDescripcion()); // "Arma básica"
console.log("Daño base:", miEspada.calcularDano()); // Daño base: 10

// Mejorar con gema de fuego
miEspada = new GemaFuego(miEspada);
console.log(miEspada.obtenerDescripcion()); // "Arma básica + Gema de Fuego"
console.log("Daño con fuego:", miEspada.calcularDano()); // Daño con fuego: 15

// Añadir gema de hielo
miEspada = new GemaHielo(miEspada);
console.log(miEspada.obtenerDescripcion()); // "Arma básica + Gema de Fuego + Gema de Hielo"
console.log("Daño con fuego+hielo:", miEspada.calcularDano()); // Daño con fuego+hielo: 18

// Añadir gema eléctrica
miEspada = new GemaElectrica(miEspada);
console.log(miEspada.obtenerDescripcion()); // "Arma básica + Gema de Fuego + Gema de Hielo + Gema Eléctrica"
console.log("Daño total:", miEspada.calcularDano()); // Daño total: 22

// Usar efectos especiales
console.log("Efecto fuego:", miEspada.efectoFuego()); // "Inflige quemaduras durante 3 segundos"
console.log("Efecto hielo:", miEspada.efectoHielo()); // "Ralentiza al enemigo 50% por 2 segundos"

Por qué es Decorator

NECESIDAD: "Añadir funcionalidad a armas sin crear clases explosivas"

PROBLEMA SIN DECORATOR:
- class EspadaFuegoHieloElectrica extends Arma { ... }
- class EspadaFuegoHielo extends Arma { ... }
- class EspadaFuegoElectrica extends Arma { ... }
- ¡Combinación explosiva de clases!

SOLUCIÓN CON DECORATOR:
- Arma básica
- + GemaFuego
- + GemaHielo
- + GemaElectrica
- Combinaciones dinámicas

Otro ejemplo: Armaduras con mejoras

// Componente base
class Armadura {
constructor() {
this.defensa = 20;
this.descripcion = "Armadura de cuero";
}

obtenerDefensa() {
return this.defensa;
}
}

// Decorador para placas de metal
class PlacasMetal extends Armadura {
constructor(armadura) {
super();
this.armadura = armadura;
}

obtenerDefensa() {
return this.armadura.obtenerDefensa() + 15;
}

obtenerDescripcion() {
return this.armadura.descripcion + " con placas de metal";
}
}

// Decorador para runas de protección
class RunasProteccion extends Armadura {
constructor(armadura) {
super();
this.armadura = armadura;
}

obtenerDefensa() {
return this.armadura.obtenerDefensa() + 10;
}

obtenerDescripcion() {
return this.armadura.descripcion + " con runas de protección";
}

efectoRunas() {
return "Reduce daño mágico en 30%";
}
}

// Uso
let armaduraJugador = new Armadura();
console.log("Defensa base:", armaduraJugador.obtenerDefensa()); // 20

// Añadir placas de metal
armaduraJugador = new PlacasMetal(armaduraJugador);
console.log("Defensa con placas:", armaduraJugador.obtenerDefensa()); // 35

// Añadir runas de protección
armaduraJugador = new RunasProteccion(armaduraJugador);
console.log("Defensa total:", armaduraJugador.obtenerDefensa()); // 45
console.log("Efecto:", armaduraJugador.efectoRunas()); // "Reduce daño mágico en 30%"

Beneficios en Eldoria

  1. Flexibilidad total: El jugador puede crear cualquier combinación:

    // Espada con solo fuego
    let espadaFuego = new GemaFuego(new Arma());

    // Espada con hielo y eléctrica
    let espadaHieloElectrica = new GemaElectrica(new GemaHielo(new Arma()));

    // Armadura completa
    let armaduraCompleta = new RunasProteccion(
    new PlacasMetal(
    new Armadura()
    )
    );

  2. Mejoras temporales: Decoradores que expiran:

    class PocionFuerzaTemporal extends GemaDecorator {
    constructor(arma, duracion) {
    super(arma);
    this.duracion = duracion; // segundos
    this.timer = setTimeout(() => {
    console.log("¡La poción de fuerza ha expirado!");
    // Lógica para remover el efecto
    }, duracion * 1000);
    }

    calcularDano() {
    return this.arma.calcularDano() + 8; // Bonus temporal
    }
    }

  3. Efectos visuales dinámicos:

    class EfectoVisual extends GemaDecorator {
    obtenerEfectoVisual() {
    const efectos = this.arma.obtenerEfectoVisual ?
    this.arma.obtenerEfectoVisual() : [];
    efectos.push("partículas de fuego");
    return efectos;
    }
    }

Analogía sencilla

El Decorator es como...

  • Decorar un pastel → Pastel base + crema + frutas + chocolate
  • Personalizar un coche → Coche base + llantas deportivas + spoiler + pintura especial
  • Vestirse en capas → Camiseta + suéter + chaqueta + bufanda

En programación:

  • Streams en Java/C# → FileStream + BufferedStream + CryptoStream
  • Middleware en Express → app.use(authentication) + app.use(logging) + app.use(compression)
  • Validadores encadenados → Validar email + validar longitud + validar formato

Resumen del Decorator en Eldoria

La espada base (Componente):

  • Sabe hacer daño básico
  • No sabe nada sobre fuego, hielo o electricidad

Las gemas (Decoradores):

  • Envuelven la espada existente
  • Añaden su propio efecto
  • Delegan al objeto envuelto

Ventaja: Si añadimos una nueva gema de "Veneno", solo creamos una clase GemaVeneno. Todas las armas existentes pueden usarla inmediatamente sin modificación.

Ejemplo del patrón Strategy en Eldoria Chronicles

El principio es claro. Define una familia de algoritmos, encapsula cada uno, y los hace intercambiables. Es como el manual de tácticas del general: diferentes estrategias de combate que puedes cambiar según el enemigo.

Situación: El sistema de inteligencia de los enemigos

En Eldoria, los enemigos eligen diferentes estrategias de ataque según la situación: si estás cerca usan espada, si estás lejos usan arco, si eres mago usan magia.

Analogía Visual

Explicación: El enemigo tiene un selector de estrategias que cambia según la situación, pero su interfaz de ataque sigue siendo la misma.

Implementación sencilla

// Interfaz Strategy (comportamiento)
class EstrategiaAtaque {
atacar() {
throw new Error("Método 'atacar' debe ser implementado");
}

obtenerNombre() {
return "Estrategia genérica";
}
}

// Estrategias concretas
class EstrategiaEspada extends EstrategiaAtaque {
atacar() {
return "¡Golpe con espada! Daño: 15, Alcance: corto";
}

obtenerNombre() {
return "Estrategia Espada";
}

esAdecuada(distanciaJugador, tipoJugador) {
return distanciaJugador < 5; // Menos de 5 metros
}
}

class EstrategiaArco extends EstrategiaAtaque {
atacar() {
return "¡Flecha precisa! Daño: 12, Alcance: largo";
}

obtenerNombre() {
return "Estrategia Arco";
}

esAdecuada(distanciaJugador, tipoJugador) {
return distanciaJugador >= 5; // 5 metros o más
}
}

class EstrategiaMagia extends EstrategiaAtaque {
atacar() {
return "¡Bola de fuego! Daño: 20, Coste maná: 10";
}

obtenerNombre() {
return "Estrategia Magia";
}

esAdecuada(distanciaJugador, tipoJugador) {
return tipoJugador === 'mago'; // Contra magos
}
}

class EstrategiaDefensa extends EstrategiaAtaque {
atacar() {
return "¡Bloqueo defensivo! Defensa +10, Recupera 5 HP";
}

obtenerNombre() {
return "Estrategia Defensa";
}

esAdecuada(distanciaJugador, tipoJugador) {
return distanciaJugador < 3 && tipoJugador === 'guerrero';
}
}

// Contexto que usa las estrategias
class Enemigo {
constructor(nombre, hp) {
this.nombre = nombre;
this.hp = hp;
this.estrategia = new EstrategiaEspada(); // Estrategia por defecto
}

// Método para cambiar estrategia
cambiarEstrategia(nuevaEstrategia) {
this.estrategia = nuevaEstrategia;
console.log(`${this.nombre} cambia a: ${nuevaEstrategia.obtenerNombre()}`);
}

// Método que usa la estrategia actual
atacar() {
console.log(`${this.nombre} ataca:`);
console.log(this.estrategia.atacar());
}

// Elige estrategia basada en situación
elegirMejorEstrategia(distanciaJugador, tipoJugador) {
const estrategias = [
new EstrategiaEspada(),
new EstrategiaArco(),
new EstrategiaMagia(),
new EstrategiaDefensa()
];

// Encuentra la estrategia más adecuada
for (const estrategia of estrategias) {
if (estrategia.esAdecuada(distanciaJugador, tipoJugador)) {
this.cambiarEstrategia(estrategia);
return;
}
}

// Si ninguna es perfecta, usa la por defecto
this.cambiarEstrategia(new EstrategiaEspada());
}
}

Cómo se usa en el juego

// Crear un enemigo
const orco = new Enemigo("Grishnak el Orco", 100);
console.log("=== ENCUENTRO CON EL ORCO ===");

// Situación 1: Jugador guerrero cerca
console.log("\\nSituación: Guerrero a 2 metros");
orco.elegirMejorEstrategia(2, 'guerrero');
orco.atacar();
// Salida:
// Grishnak el Orco cambia a: Estrategia Espada
// Grishnak el Orco ataca:
// ¡Golpe con espada! Daño: 15, Alcance: corto

// Situación 2: Jugador se aleja
console.log("\\nSituación: Guerrero a 8 metros");
orco.elegirMejorEstrategia(8, 'guerrero');
orco.atacar();
// Salida:
// Grishnak el Orco cambia a: Estrategia Arco
// Grishnak el Orco ataca:
// ¡Flecha precisa! Daño: 12, Alcance: largo

// Situación 3: Aparece un mago
console.log("\\nSituación: Mago a 4 metros");
orco.elegirMejorEstrategia(4, 'mago');
orco.atacar();
// Salida:
// Grishnak el Orco cambia a: Estrategia Magia
// Grishnak el Orco ataca:
// ¡Bola de fuego! Daño: 20, Coste maná: 10

// Situación 4: Guerrero muy cerca
console.log("\\nSituación: Guerrero a 1 metro");
orco.elegirMejorEstrategia(1, 'guerrero');
orco.atacar();
// Salida:
// Grishnak el Orco cambia a: Estrategia Defensa
// Grishnak el Orco ataca:
// ¡Bloqueo defensivo! Defensa +10, Recupera 5 HP

Por qué es Strategy

NECESIDAD: "Comportamiento que cambia según la situación"

PROBLEMA SIN STRATEGY:
- class Enemigo {
if (distancia < 5) { atacarConEspada() }
else if (tipo === 'mago') { atacarConMagia() }
else { atacarConArco() }
}
- ¡Código lleno de condicionales!
- Difícil añadir nuevas tácticas

SOLUCIÓN CON STRATEGY:
- Estrategias separadas: Espada, Arco, Magia, Defensa
- Enemigo solo: this.estrategia.atacar()
- Cambiar estrategia = cambiar comportamiento

Otro ejemplo: Sistema de movimiento de NPCs

// Estrategias de movimiento
class EstrategiaMovimiento {
mover() {
throw new Error("Método 'mover' debe ser implementado");
}
}

class MovimientoAleatorio extends EstrategiaMovimiento {
mover(npc) {
const direcciones = ['norte', 'sur', 'este', 'oeste'];
const direccion = direcciones[Math.floor(Math.random() * direcciones.length)];
return `${npc.nombre} se mueve al ${direccion} aleatoriamente`;
}
}

class MovimientoPatrulla extends EstrategiaMovimiento {
mover(npc) {
const puntos = ['entrada', 'torre', 'puente', 'muralla'];
const punto = puntos[npc.contadorPatrulla % puntos.length];
npc.contadorPatrulla++;
return `${npc.nombre} patrulla hacia la ${punto}`;
}
}

class MovimientoPerseguir extends EstrategiaMovimiento {
mover(npc) {
return `${npc.nombre} persigue al jugador agresivamente`;
}
}

class MovimientoHuir extends EstrategiaMovimiento {
mover(npc) {
return `${npc.nombre} huye del jugador asustado`;
}
}

// NPC que usa estrategias de movimiento
class NPC {
constructor(nombre) {
this.nombre = nombre;
this.movimiento = new MovimientoAleatorio();
this.contadorPatrulla = 0;
}

cambiarMovimiento(nuevoMovimiento) {
this.movimiento = nuevoMovimiento;
}

realizarMovimiento() {
console.log(this.movimiento.mover(this));
}
}

// Uso
const guardia = new NPC("Guardia Real");

console.log("=== RUTINA DEL GUARDIA ===");
guardia.realizarMovimiento(); // Se mueve aleatoriamente

// Cambia a patrulla cuando comienza su turno
guardia.cambiarMovimiento(new MovimientoPatrulla());
guardia.realizarMovimiento(); // Patrulla hacia la entrada
guardia.realizarMovimiento(); // Patrulla hacia la torre

// Si ve al jugador, cambia a persecución
guardia.cambiarMovimiento(new MovimientoPerseguir());
guardia.realizarMovimiento(); // Persigue al jugador

// Si el jugador es muy fuerte, huye
guardia.cambiarMovimiento(new MovimientoHuir());
guardia.realizarMovimiento(); // Huye del jugador

Beneficios en Eldoria

  1. NPCs más inteligentes: Diferentes comportamientos según situación:

    class EstrategiaCombateGrupo extends EstrategiaAtaque {
    atacar() {
    return "¡Ataque coordinado! Todos los enemigos atacan juntos";
    }

    esAdecuada(numeroAliados) {
    return numeroAliados >= 3; // Si hay 3+ aliados
    }
    }

  2. Sistema de dificultad adaptable:

    class SelectorDificultad {
    constructor(dificultad) {
    this.dificultad = dificultad;
    this.estrategias = {
    facil: new EstrategiaFacil(),
    normal: new EstrategiaNormal(),
    dificil: new EstrategiaDificil(),
    heroico: new EstrategiaHeroica()
    };
    }

    obtenerEstrategia() {
    return this.estrategias[this.dificultad];
    }
    }

  3. Comportamiento según estado del enemigo:

    class EnemigoHerido extends EstrategiaAtaque {
    atacar() {
    return "¡Ataque desesperado! Daño extra pero defensa baja";
    }

    esAdecuada(hpPorcentaje) {
    return hpPorcentaje < 30; // Menos del 30% HP
    }
    }

Analogía sencilla

El Strategy es como...

  • Navegación GPS → Ruta rápida, ruta corta, evitar peajes, panorámica
  • Modos de cámara → Automático, retrato, deportes, nocturno
  • Estilos de natación → Crol, espalda, braza, mariposa

En programación:

  • Algoritmos de ordenación → QuickSort, MergeSort, BubbleSort
  • Métodos de pago → Tarjeta, PayPal, Transferencia, Efectivo
  • Compresión de archivos → ZIP, RAR, 7Z, TAR

Resumen del Strategy en Eldoria

El enemigo (Contexto):

  • Tiene una estrategia actual
  • Delega el comportamiento a esa estrategia
  • Puede cambiar de estrategia cuando quiera

Las estrategias (Algoritmos):

  • Implementan un comportamiento específico
  • Son intercambiables
  • No conocen al contexto que las usa

Ventaja: Si añadimos una nueva estrategia "Ataque Sorpresa", solo creamos la clase y los enemigos pueden usarla inmediatamente sin modificar su código.

Conclusión: Llevándolo a la práctica

Diseño y documentación no son tareas “extra”; son el mecanismo que permite que el software sobreviva al cambio. Un diseño correcto reduce puntos frágiles, delimita responsabilidades y hace que los cambios futuros sean previsibles. La documentación conserva el porqué de las decisiones y evita que el equipo reinterprete el sistema a ciegas.

En tu carrera, esto es lo que separa a alguien que solo implementa funcionalidades de alguien que construye sistemas mantenibles. Cuando el proyecto crece, el código no se salva por ser más listo, se salva por estar mejor diseñado y mejor documentado.