StackPractices
intermediate Por Mathias Paulenko

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:

flowchart diagram: Usuario
  • 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

VarianteAcoplamientoCuándo encaja
MVC clásicoLa Vista lee el Modelo directamenteApps web server-rendered, UIs de escritorio
MVPLa Vista es tonta; el Presenter lo media todoCapas de UI muy testeables
MVVMEl ViewModel se vincula a la Vista automáticamenteFrameworks 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?AcoplamientoData binding
MVCSí (directamente)AltoManual
MVPNo (vía Presenter)MedioManual
MVVMNo (vía ViewModel)BajoAutomático

Lo Que Funciona

  1. Mantén el modelo ignorante de las vistas; notifica, no sabe quién lo pinta
  2. Las vistas leen el modelo, nunca lo escriben; las mutaciones pasan por el controlador
  3. Controladores delgados: orquestación en el controlador, reglas de negocio en el modelo
  4. Usa el patrón observer para actualizaciones modelo→vista en lugar de llamadas directas
  5. 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

  1. Controladores gordos: validación, cálculos y workflows viviendo en controladores en lugar del modelo: la podredumbre clásica de Rails/Django
  2. Vistas que mutan: una plantilla que escribe de vuelta al modelo es un controlador oculto que nadie revisa
  3. “MVC-ear” un script: tres carpetas para código que cabe en un archivo añade indirección sin retorno
  4. 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
  5. 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

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.