Patrón MVC: Modelo, Vista y Controlador Explicados
Separa una aplicación en capas Modelo, Vista y Controlador. Cuándo aporta MVC, cuándo es sobre-ingeniería y cómo interactúan las piezas.
Visión General
Un miembro nuevo del equipo pregunta dónde va la lógica de validación de usuarios y la respuesta honesta es “está en tres sitios”. MVC existe para que cada trozo de código tenga exactamente un hogar: el Modelo gestiona los datos y las reglas de negocio, la Vista gestiona la presentación, y el Controlador gestiona la traducción entre la entrada del usuario y las operaciones del modelo. Es el principio organizador detrás de Django, Rails, ASP.NET MVC y todo framework web del lado del servidor que vino después — y la misma división en tres aparece en los frameworks frontend, solo que con las piezas renombradas.
Cuándo Usarlo
Usa el patrón MVC cuando:
- Datos, presentación y manejo de entrada se enredan en los mismos archivos
- Varias vistas muestran el mismo estado (un panel admin y una página pública leyendo un solo modelo)
- Varios desarrolladores trabajan en paralelo y los límites de capa evitan el infierno de merges
- Necesitas el mismo modelo detrás de interfaces distintas (web UI, CLI, API
Sáltalo cuando:
- Toda la app son cien líneas: tres capas de ceremonia para un script no compra nada
- Estás dentro de un framework frontend que ya impone su propio sabor (Vue/Angular tiran a MVVM, React a flujo unidireccional)
- El “controlador” es solo un router: una API REST que valida, llama a un servicio y serializa apenas necesita una división formal MVC
Solución
Python
class UserModel:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class UserView:
def display(self, user: UserModel):
print(f"User: {user.name} ({user.email})")
class UserController:
def __init__(self, model: UserModel, view: UserView):
self.model = model
self.view = view
def update_name(self, name: str):
self.model.name = name
self.view.display(self.model)
# Uso
controller = UserController(UserModel("Alice", "alice@example.com"), UserView())
controller.update_name("Alicia") # -> User: Alicia (alice@example.com)
JavaScript
class UserModel {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserView {
display(user) {
console.log(`User: ${user.name} (${user.email})`);
}
}
class UserController {
constructor(model, view) {
this.model = model;
this.view = view;
}
updateName(name) {
this.model.name = name;
this.view.display(this.model);
}
}
const controller = new UserController(
new UserModel("Alice", "alice@example.com"),
new UserView()
);
controller.updateName("Alicia");
Java
class UserModel {
String name;
String email;
UserModel(String name, String email) {
this.name = name;
this.email = email;
}
}
class UserView {
void display(UserModel user) {
System.out.println("User: " + user.name + " (" + user.email + ")");
}
}
class UserController {
private final UserModel model;
private final UserView view;
UserController(UserModel model, UserView view) {
this.model = model;
this.view = view;
}
void updateName(String name) {
model.name = name;
view.display(model);
}
}
Explicación
El flujo tiene siempre la misma forma:
- Modelo: gestiona los datos y las reglas sobre ellos: validación, transiciones, persistencia. No sabe nada de cómo se muestran.
- Vista: renderiza el estado del modelo. Lo lee (o un observador la notifica) pero nunca lo muta.
- Controlador: la única pieza que puede tocar ambos lados. Parsea la entrada, llama a las operaciones del modelo y elige qué muestra la vista a continuación.
En los frameworks web los nombres se difuminan: Django llama a su capa de vista “template” y al controlador “view”; Rails mantiene los nombres. Lo que sobrevive en todos es la regla de tres: un lugar para los datos, uno para el render, uno para la orquestación. El patrón observer es cómo las implementaciones reales dejan que el modelo notifique a las vistas sin saber que existen.
Variantes
| Variante | Acoplamiento | Cuándo encaja |
|---|---|---|
| MVC clásico | La Vista lee el Modelo directamente | Apps web server-rendered, UIs de escritorio |
| MVP | La Vista es tonta; el Presenter lo media todo | Capas de UI muy testeables |
| MVVM | El ViewModel se vincula a la Vista automáticamente | Frameworks frontend con binding bidireccional |
Para los trade-offs específicos de frontend, ver MVC en aplicaciones frontend.
Uso Avanzado
Código companion: el repo companion de mvc-pattern tiene las cuatro implementaciones listas para ejecutar.
Un dashboard que de verdad se actualiza solo
Los ejemplos de arriba imprimen una vez y salen. El MVC real importa cuando la vista se refresca sin que el controlador haga polling. Esta versión TypeScript usa suscripciones para que el modelo, al empujar cambios, dispare el render:
interface Metric {
name: string;
value: number;
}
// Modelo: datos + suscripción, sin idea de quién lo renderiza
class DashboardModel {
private metrics: Metric[] = [];
private subscribers: (() => void)[] = [];
addMetric(metric: Metric) {
this.metrics.push(metric);
this.notify();
}
getMetrics(): Metric[] {
return [...this.metrics];
}
subscribe(cb: () => void) {
this.subscribers.push(cb);
}
private notify() {
this.subscribers.forEach((cb) => cb());
}
}
// Vista: renderiza lo que el modelo le entrega
class DashboardView {
constructor(private container: HTMLElement) {}
render(metrics: Metric[]) {
this.container.innerHTML = metrics
.map((m) => `<div class="ql"><h3>${m.name}</h3><span class="value">${m.value}</span></div>`)
.join("");
}
}
// Controlador: obtiene datos hacia el modelo; la suscripción re-renderiza
class DashboardController {
constructor(private model: DashboardModel, private view: DashboardView) {
this.model.subscribe(() => this.view.render(this.model.getMetrics()));
}
async loadMetrics() {
const data: Metric[] = await fetch("/api/metrics").then((r) => r.json());
data.forEach((m) => this.model.addMetric(m));
}
}
La versión anterior de este ejemplo traía un bug sutil: al callback de map() se le pasaban tres template strings extra como argumentos ignorados, así que cada métrica renderizaba un <div> vacío y sin cerrar. Vale la pena recordarlo: map toma exactamente un argumento, el callback.
| Patrón | ¿La Vista conoce el Modelo? | Acoplamiento | Data binding |
|---|---|---|---|
| MVC | Sí (directamente) | Alto | Manual |
| MVP | No (vía Presenter) | Medio | Manual |
| MVVM | No (vía ViewModel) | Bajo | Automático |
Lo Que Funciona
- Mantén el modelo ignorante de las vistas; notifica, no sabe quién lo pinta
- Las vistas leen el modelo, nunca lo escriben; las mutaciones pasan por el controlador
- Controladores delgados: orquestación en el controlador, reglas de negocio en el modelo
- Usa el patrón observer para actualizaciones modelo→vista en lugar de llamadas directas
- Si te encuentras explicando por qué la vista necesita un campo del modelo, probablemente dibujaste mal el límite: esa lógica va una capa más abajo
Errores Comunes
- Controladores gordos: validación, cálculos y workflows viviendo en controladores en lugar del modelo: la podredumbre clásica de Rails/Django
- Vistas que mutan: una plantilla que escribe de vuelta al modelo es un controlador oculto que nadie revisa
- “MVC-ear” un script: tres carpetas para código que cabe en un archivo añade indirección sin retorno
- Tratar la capa HTTP como el controlador: en frameworks web el “controlador” suele ser un adaptador delgado; la coordinación real vive un nivel más abajo en servicios/casos de uso
- Esperar que un patrón arregle el acoplamiento: MVC define las cajas, no qué va dentro; un modelo mal factorizado sigue mal factorizado
Lectura Adicional
- Martin Fowler sobre arquitecturas GUI: la taxonomía original de MVC, MVP y MVVM
- La versión de Django sobre MVC/MVT: por qué los nombres difieren entre frameworks
- Patterns of Enterprise Application Architecture: dónde encaja el patrón entre las alternativas de capa de presentación
Preguntas frecuentes
¿Sigue siendo relevante MVC con los frameworks frontend modernos?
Sí — la división en tres sobrevive aunque los nombres cambien. El flujo unidireccional de React separa estado de render igual que MVC separa modelo de vista. Angular implementa MVVM, que es un descendiente directo. El vocabulario cambió; la separación no.
¿Cuál es la diferencia real entre MVC y MVVM?
El ViewModel. MVVM reemplaza el controlador con un ViewModel al que la vista se vincula automáticamente, así que no llamas a render(), la capa de binding mantiene la vista sincronizada. MVVM encaja en frameworks con binding reactivo (Vue, Angular); MVC encaja en server-side rendering y frameworks sin binding (Django, Rails, la mayoría de backends).
¿Puedo usar MVC en una arquitectura serverless?
Sí, de forma laxa: el controlador se convierte en tu handler Lambda o el mapping del API Gateway, el modelo es tu capa de datos más lógica de dominio, y la vista es la respuesta serializada o la plantilla renderizada. El patrón aguanta; el "controlador" solo se hizo más pequeño.
¿Cómo evito que los controladores engorden?
Baja la lógica: cualquier cosa que sobreviva si la UI cambia es del lado del modelo, no del controlador. Validación de reglas de negocio, cálculos, pasos de workflow — si la versión CLI también lo necesitaría, no es código de controlador. Un test útil: ¿puedes cambiar la capa HTTP por un cron job sin tocar el controlador? Si no, parte de su código pertenece al modelo.
¿Cuándo es MVC sobre-ingeniería?
Cuando la app es lo bastante pequeña para que la división cueste más de lo que ahorra: un script único, una API CRUD delgada donde cada endpoint hace una query, un prototipo que vas a tirar. La separación se amortiza cuando las piezas cambian de forma independiente; si cambian juntas, es ceremonia.
Recursos Relacionados
Patrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.
PatternPatrón Observer
Define un mecanismo de suscripción para notificar a múltiples objetos sobre eventos. Patrón de diseño conductual para comunicación basada en eventos.
PatternMVC Pattern en Aplicaciones Frontend Modernas
Aplica el Model-View-Controller pattern en aplicaciones React y Vue para separar datos, UI y logica de interaccion en una arquitectura de componentes mantenible
PatternPatrón Model-View-ViewModel (MVVM)
Vincula componentes UI declarativamente a un ViewModel que expone datos y comandos, habilitando sincronización automática entre view y estado.
GuideGuía de Arquitectura de Software
Una guía para diseñar arquitectura de software: monolitos vs microservicios, arquitectura en capas, flujo de datos y criterios de selección de tecnología.
GuideGuía de Diseño de APIs REST
Una Referencia Detallada para diseñar APIs REST limpias, escalables y mantenibles.