Lección 6 de 911 min
MVC, MVP, MVVM e inyección de dependencias
Cómo separar la interfaz de la lógica y cómo dar a cada pieza lo que necesita sin acoplarla.
Un formulario de registro empieza con veinte líneas en un componente. Seis meses después, ese mismo componente valida campos, llama a la API, formatea fechas, decide qué mensaje mostrar y abre un modal. Nadie se atreve a tocarlo y no hay forma de probarlo sin un navegador.
Los patrones de presentación separan lo que se ve de lo que se decide. La inyección de dependencias resuelve un problema vecino: cómo recibe cada pieza los servicios que necesita sin crearlos ella misma.
Separar la interfaz de la lógica
MVC, MVP y MVVM coinciden en tener un modelo (los datos y las reglas) y una vista (lo que se muestra). Se diferencian en la pieza que las une y en cómo viaja la información entre ellas.
MVC (Modelo-Vista-Controlador)
El controlador recibe la acción, actualiza el modelo y elige qué vista mostrar.
- Origen:
- Smalltalk, finales de los 70
MVC nació a finales de los años 70 en Smalltalk y es el más antiguo de los tres. El modelo contiene los datos y las reglas, la vista los muestra y el controlador recibe las acciones del usuario, decide qué hacer con el modelo y qué vista devolver.
En la web se hizo famoso en el servidor: Ruby on Rails, Django (que lo llama MTV), Laravel, ASP.NET MVC o Spring MVC. Una petición HTTP llega a un controlador, que consulta el modelo y renderiza una plantilla con los datos.
Úsalo cuando
En aplicaciones web renderizadas en el servidor y en APIs, donde cada petición sigue un ciclo claro: entra, se procesa y sale una respuesta.
Ten en cuenta
Hay tantas variantes que «MVC» significa cosas distintas en cada framework. El error típico es el «controlador gordo», que acumula lógica de negocio que debería estar en el modelo o en servicios.
// Controlador: recibe la petición, usa el modelo y elige la vista
app.get("/productos/:id", async (req, res) => {
const producto = await Producto.buscarPorId(req.params.id); // modelo
if (!producto) return res.status(404).render("no-encontrado");
res.render("producto", { producto }); // vista: una plantilla
});MVP (Modelo-Vista-Presentador)
Una vista pasiva que delega en un presentador, que la controla mediante una interfaz.
En MVP, la vista es lo más simple posible: muestra lo que le dicen y avisa de lo que hace el usuario. El presentador recibe esos avisos, habla con el modelo y le dice a la vista qué mostrar llamando a métodos de una interfaz, como mostrarError() o mostrarCargando().
Como el presentador solo conoce esa interfaz, puedes probarlo con una vista falsa, sin pantalla ni navegador. Fue muy popular en Windows Forms, en GWT y en las primeras apps de Android.
Úsalo cuando
Cuando la tecnología de interfaz no ofrece enlace de datos (data binding) y quieres que toda la lógica de presentación se pueda probar con facilidad.
Ten en cuenta
Exige mucho código de pegamento: un método en la interfaz por cada cosa que la vista sabe hacer. Con frameworks que tienen bindings, MVVM suele resultar más natural.
interface VistaLogin {
mostrarError(mensaje: string): void;
irAInicio(): void;
}
class PresentadorLogin {
constructor(private vista: VistaLogin, private auth: ServicioAuth) {}
async alEnviar(email: string, clave: string) {
if (!email.includes("@")) return this.vista.mostrarError("Email no válido");
if (await this.auth.entrar(email, clave)) this.vista.irAInicio();
else this.vista.mostrarError("Credenciales incorrectas");
}
}
// En los tests, VistaLogin es un objeto falso que anota lo que le pidenMVVM (Modelo-Vista-VistaModelo)
La vista se enlaza a un ViewModel que expone estado y acciones; los bindings sincronizan.
- Origen:
- Microsoft (WPF), hacia 2005
MVVM surgió en Microsoft para WPF, hacia 2005. El ViewModel expone el estado que la vista necesita (total, cargando) y las acciones que puede invocar (guardar()), pero no sabe nada de la vista. Es el enlace de datos (data binding) el que los mantiene sincronizados: si cambia el estado, la vista se actualiza sola.
Los componentes de Angular se parecen mucho a este patrón: la clase del componente hace de ViewModel, la plantilla es la vista y los bindings ({{ }}, [propiedad], (evento)) las conectan. Vue y Knockout siguen la misma idea.
Úsalo cuando
Con frameworks que ofrecen enlace de datos o reactividad, que hoy son casi todos los de frontend.
Ten en cuenta
El ViewModel tiende a engordar con lógica de negocio: sácala a servicios. Y con bindings en las dos direcciones es fácil perder la pista de quién cambió qué; por eso muchos frameworks prefieren que los datos fluyan en un solo sentido.
@Component({
selector: "app-carrito",
template: `
<p>{{ unidades() }} × {{ precio }} € = {{ total() }} €</p>
<button (click)="sumar()">Añadir otra</button>
`,
})
export class CarritoComponent {
// Estado y acciones del «ViewModel»: no sabe nada del DOM
unidades = signal(1);
precio = 20;
total = computed(() => this.unidades() * this.precio);
sumar() {
this.unidades.update((n) => n + 1);
}
}En la práctica, los límites son borrosos
Pocos frameworks siguen uno de estos patrones al pie de la letra. Lo que importa es la idea común: que la vista no contenga reglas de negocio y que la lógica de presentación se pueda probar sin pintar nada.
Dar a cada pieza lo que necesita
Inversión de control
En lugar de que tu código llame al framework, el framework llama a tu código.
- También llamada:
- Principio de Hollywood
En un programa tradicional, tu código manda: llama a las funciones de una biblioteca cuando quiere. Con inversión de control (IoC, Inversion of Control), el flujo se invierte: un framework lleva el control y llama a tu código en el momento oportuno. Se resume en el «principio de Hollywood»: «no nos llames, ya te llamaremos».
La usas constantemente: el navegador llama a tu función cuando ocurre un clic, y Angular crea tus componentes y llama a ngOnInit cuando le toca. Es, de hecho, lo que distingue un framework de una biblioteca. La inyección de dependencias es una forma concreta de inversión de control, aplicada a cómo se obtienen los objetos.
Úsalo cuando
Es la base de cualquier framework. Conviene reconocerla para entender por qué tu código no tiene un punto de entrada evidente y cuándo se ejecuta cada cosa.
Ten en cuenta
Abusar de la «magia» hace el flujo difícil de seguir: cuando todo lo llama el framework, depurar exige saber en qué orden lo hace. Lee bien el ciclo de vida de tu framework.
// Biblioteca: tú decides cuándo llamarla
const fecha = new Intl.DateTimeFormat("es").format(new Date());
// Inversión de control: el navegador decide cuándo llamarte
boton.addEventListener("click", () => guardar());
// Y Angular también: crea el componente y te avisa
export class PerfilComponent implements OnInit {
ngOnInit() {
// lo llama Angular, no tú
}
}Inyección de dependencias
Cada clase declara lo que necesita y un inyector se lo entrega ya creado.
- En Angular:
- inject(), @Injectable
Si un componente hace new ServicioPedidos(new ClienteHttp()), queda atado a esas clases concretas y a su forma de construirse. Con inyección de dependencias (DI), la pieza solo declara lo que necesita, y otro, el inyector, se encarga de crearlo y entregárselo. Así puedes cambiar una implementación por otra, por ejemplo una falsa en los tests, sin tocar a quien la usa.
Angular tiene uno de los sistemas de DI más completos del frontend. Marcas un servicio con @Injectable, lo pides con inject() o en el constructor, y el inyector te da una instancia, compartida por toda la aplicación si se declaró con providedIn: "root". En los tests puedes sustituir cualquier proveedor. Lo verás a fondo en la lección de servicios del módulo de frameworks frontend.
Úsalo cuando
En cualquier aplicación mediana o grande, y siempre que quieras probar una pieza aislada de sus dependencias reales: la API, el reloj, el almacenamiento.
Ten en cuenta
Un inyector mal configurado falla al ejecutar, con errores del tipo «No provider for…». Y no todo necesita inyectarse: una función pura que formatea un precio se importa y ya está.
@Injectable({ providedIn: "root" })
export class ServicioPedidos {
private http = inject(HttpClient); // pide lo que necesita
listar() {
return this.http.get<Pedido[]>("/api/pedidos");
}
}
@Component({ selector: "app-pedidos", template: "…" })
export class PedidosComponent {
private pedidos = inject(ServicioPedidos); // no hace new: se lo dan
}
// En un test, se cambia por una versión falsa sin tocar el componente
TestBed.configureTestingModule({
providers: [{ provide: ServicioPedidos, useValue: { listar: () => of([]) } }],
});Interactivo
MVC, MVP y MVVM
Activa o desactiva opciones para compararlas cara a cara.
| Característica | MVC | MVP | MVVM |
|---|---|---|---|
| Quién recibe la acción del usuario | El controlador | La vista, que avisa al presentador | La vista, que invoca acciones del ViewModel |
| Cómo se actualiza la vista | El controlador elige una vista y le pasa los datos | El presentador llama a métodos de la vista | Sola, mediante enlace de datos |
| La vista conoce a… | Al modelo, en muchas variantes | Solo al presentador | Al ViewModel, por bindings |
| Facilidad para probar la lógica | Media | Alta: vista falsa en los tests | Alta: el ViewModel no conoce la vista |
| Dónde lo verás | Rails, Laravel, Spring MVC, ASP.NET MVC | Windows Forms, GWT, el Android de los inicios | Angular, Vue, WPF |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Construyes una web renderizada en el servidor: cada petición a una URL consulta datos y devuelve una plantilla HTML distinta según el resultado.
Para recordar
- MVC, MVP y MVVM separan la vista del modelo; cambia la pieza que los une: un controlador, un presentador con una vista pasiva o un ViewModel enlazado con bindings.
- Los componentes de Angular, con su clase y su plantilla, se parecen mucho a MVVM.
- Inversión de control: el framework llama a tu código. La inyección de dependencias es la forma de entregarte los servicios que necesitas.
- Pedir las dependencias en lugar de crearlas permite cambiarlas por versiones falsas en los tests.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué pieza de MVVM se corresponde con la clase de un componente de Angular?
2.¿Por qué el presentador de MVP es fácil de probar?
3.¿Qué problema resuelve la inyección de dependencias?
4.Según la idea de inversión de control, ¿qué distingue a un framework de una biblioteca?
5.Un controlador de MVC tiene 600 líneas con reglas de descuentos y cálculo de impuestos. ¿Qué harías?