Patrones arquitectónicos en TypeScript: cómo dejar de escribir "espagueti" y empezar a vivir
\n\n¿Alguna vez has abierto un proyecto en TypeScript, escrito hace seis meses, y te has dado cuenta con horror de que no podías entender tus propias clases? ¿O quizás estabas añadiendo una nueva funcionalidad y la aplicación fallaba en tres lugares inesperados? ¿Te suena familiar? Bienvenido al club de desarrolladores que alguna vez decidieron que "así está bien" — y lo pagaron caro.
\n\nTypeScript no es solo una extensión de JavaScript con tipos. Es una herramienta poderosa que permite construir sistemas complejos, escalables y mantenibles. Pero los tipos por sí solos no te salvarán del caos arquitectónico. Necesitas patrones arquitectónicos — recetas probadas a lo largo del tiempo que te ayudarán a organizar el código para que sea comprensible, testeable y flexible.
\n\nEn este artículo, analizaremos cinco patrones arquitectónicos clave de TypeScript que se utilizan con mayor frecuencia en proyectos reales. Verás ejemplos de código, entenderás dónde y por qué aplicar cada patrón, y podrás implementarlos de inmediato en tu trabajo. ¡Allá vamos!
\n\n1. MVC (Modelo-Vista-Controlador) — el clásico del género
\n\nEmpecemos con el patrón más conocido. MVC en TypeScript es la división de la aplicación en tres capas:
\n\n- \n
- Model — datos y lógica de negocio. \n
- View — visualización (UI). \n
- Controller — el enlace, procesa la entrada del usuario. \n
En las aplicaciones TypeScript, MVC es especialmente bueno porque podemos tipificar estrictamente todas las capas, eliminando errores en tiempo de compilación.
\n\nEjemplo: una lista de tareas simple (Todo List)
\n\n// Model\ninterface Task {\n id: number;\n title: string;\n completed: boolean;\n}\n\nclass TaskModel {\n private tasks: Task[] = [];\n\n addTask(title: string): Task {\n const newTask: Task = {\n id: Date.now(),\n title,\n completed: false,\n };\n this.tasks.push(newTask);\n return newTask;\n }\n\n getTasks(): Task[] {\n return this.tasks;\n }\n}\n\n// View\nclass TaskView {\n render(tasks: Task[]): void {\n console.clear();\n tasks.forEach(task => {\n console.log(`${task.id}: [${task.completed ? 'x' : ' '}] ${task.title}`);\n });\n }\n}\n\n// Controller\nclass TaskController {\n constructor(\n private model: TaskModel,\n private view: TaskView\n ) {}\n\n addTask(title: string): void {\n this.model.addTask(title);\n this.view.render(this.model.getTasks());\n }\n\n showAll(): void {\n this.view.render(this.model.getTasks());\n }\n}\n\n// Uso\nconst model = new TaskModel();\nconst view = new TaskView();\nconst controller = new TaskController(model, view);\ncontroller.addTask('Estudiar patrones TypeScript');\ncontroller.addTask('Escribir artículo');\n\n¿Por qué es genial? Puedes cambiar la View (por ejemplo, pasar de consola a React) sin tocar el Model. Puedes probar la lógica de negocio por separado de la UI. La arquitectura de la aplicación TypeScript se vuelve transparente.
\n\n2. Observer — el rey de los eventos
\n\nSi tu aplicación vive de eventos (clics, respuestas del servidor, temporizadores), el patrón Observer TypeScript es tu mejor amigo. Permite que unos objetos (suscriptores) observen los cambios de otro objeto (publicador).
\n\nEjemplo: sistema de notificaciones
\n\n// Interfaz del observador\ninterface Observer {\n update(data: string): void;\n}\n\n// Publicador\nclass NewsPublisher {\n private observers: Observer[] = [];\n\n subscribe(observer: Observer): void {\n this.observers.push(observer);\n }\n\n unsubscribe(observer: Observer): void {\n this.observers = this.observers.filter(obs => obs !== observer);\n }\n\n publishNews(news: string): void {\n this.observers.forEach(obs => obs.update(news));\n }\n}\n\n// Observador concreto: notificación por correo electrónico\nclass Em