Modelo-vista-controlador (MVC) es un patrón de arquitectura de software muy utilizado en la actualidad en desarrollo web (y también en muchas aplicaciones de escritorio). Este patrón propone separar la aplicación en componentes distintos: el modelo, la vista y el controlador:
Además de estos componentes usualmente tendremos otros como:
Este patrón de arquitectura de software se basa en las ideas de reutilización de código y la separación de conceptos, características que buscan facilitar la tarea de desarrollo de aplicaciones y su posterior mantenimiento.
Si una aplicación no utiliza este modelo la función que responde a una acción del usuario debe:
Por ejemplo vamos a hacer una aplicación para gestionar un almacén. Entre otras muchas cosas tendrá una función (podemos llamarle addProduct) que se encargue de añadir un nuevo producto al almacén y dicha función deberá realizar:
// La función que se ejecuta cuando el usuario envía el
// formulario para añadir un producto debería hacer:
document.getElementById('product-form').addEventListener('submit', async (event) => {
event.preventDefault()
// Coge los datos del formlario
const name = document.getElementById('product-form-name').value
const price = document.getElementById('product-form-name').price
...
// Valida cada dato
if (!name || name.length < 5 || ...)
...
// Gestiona los posibles errores en la validación
// Añade el producto a la BBDD
let newProd = {}
try {
const prod = await addProductToDatabase(payload)
newProd = new Product(prod.id, prod.name, prod.price, prod.units)
this.products.push(newProd)
} catch(err) {
// Gestiona los posibles errores producidos al añadir el producto
return
}
...
// Pinta en la página el nuevo producto
const DOMproduct = document.createElement('tr')
DOMproduct.innerHTML = `
<td>${newProd.id}</td>
<td>${newProd.name}</td>
<td>${newProd.price}</td>
<td>${newProd.units}</td>`
document.getElementById('products-table').apendChild(DOMproduct)
...
})
Como vemos, se va a convertir en una función muy grande y que se encarga de muchas cosas distintas por lo que va a ser difícil mantener ese código. Además toda la función es muy dependiente del HTML (en muchas partes se buscan elementos por su id).
En una aplicación muy sencilla podemos no seguir este modelo pero en cuanto la misma se complica un poco es imprescindible programar siguiendo buenas prácticas ya que si no lo hacemos nuestro código se volverá rápidamente muy difícil de mantener.
Hay muchas formas de implementar este modelo. Si estamos haciendo un proyecto con OOP podemos seguir el patrón MVC usando clases. Si sólo usamos programación estructurada será igual pero en vez de clases y métodos tendremos funciones.
Para organizar el código crearemos subcarpetas dentro de la carpeta src
:
model
: aquí incluiremos las clases que constituyen el modelo de nuestra aplicaciónview
: aquí crearemos un fichero JS que será el encargado de la GUI de nuestra aplicación, el único dependiente del HTML. Nuestro fichero será una clase que representa toda la vista aunque en aplicaciones mayores lo normal es tener clases para cada página, etccontroller
: aquí crearemos el fichero JS que contendrá el controlador de la aplicaciónservices
: aquí crearemos el fichero JS que se encargará de comunicarse con el servidor y proporcionar los datos al modeloDe este forma, si quiero cambiar la forma en que se muestra algo voy directamente a la vista y modifico la función que se ocupa de ello.
La vista será una clase cuyas propiedades serán elementos de la página HTML a los que accedamos frecuentemente, para no tener que buscarlos cada vez y por si tienen que estar disponibles para el controlador. Contendrá métodos para renderizar los distintos elementos de la vista.
El controlador será una clase cuyas propedades serán el modelo y la vista, de forma que pueda acceder a ambos elementos. Tendrá métodos para las distintas acciones que pueda hacer el usuario (y que se ejecutarán como respuesta a dichas acciones, como veremos en el tema de eventos). Cada uno de esos métodos llamará a métodos del modelo (para obtener o cambiar la información necesaria) y posteriormente de la vista (para reflejar esos cambios en lo que ve el usuario).
Por su parte el modelo gestionará los datos de la aplicación llamando a los servicios para obtener datos del servidor o guardar en él las modificaciones pertinentes.
El fichero principal de la aplicación instanciará un controlador y lo inicializará.
Por ejemplo, siguiendo con la aplicación para gestionar un almacén. El modelo constará de la clase Store que es nuestro almacén de productos (con métodos para añadir o eliminar productos, etc) y la clase Product que gestiona cada producto del almacén (con métodos para crear un nuevo producto, etc).
El fichero principal sería algo como:
const storeApp = new Controller() // crea el controlador
storeApp.init() // lo inicializa
// En desarrollo podemos añadir algunas líneas que luego quitaremos para
// imitar acciones del usuario y así ver el funcionamiento de la aplicación:
storeApp.addProductToStore({ name: 'Portátil Acer Travelmate E2100', price: 523.12 })
storeApp.changeProduct({ id: 1, price: 515.95 })
storeApp.deleteProduct(1)
export default class Controller {
constructor() {
this.store = new Store(1) // crea el modelo, un Store con id 1
this.view = new View() // crea la vista
}
init() {
// inicializa la vista y el modelo, si es necesario
this.store.init()
this.view.init()
// Le indica a la vista qué funciones callback se encargarán
// de responder a los eventos del usuario (siguiente tema)
this.view.setSubmitHandler(this.handleSubmitProductForm.bind(this))
}
handleSubmitProductForm(payload) {
// haría las comprobaciones necesarias sobre los datos
if (!payload.name || payload.name.length < 5 || ...) {
this.view.showErrorMessage('error', 'Datos incorrectos')
return
}
...
// y luego dice al modelo que añada el producto
try {
const newProd = this.store.addProduct(prod)
// si lo ha hecho le dice a la vista que lo pinte
this.view.renderNewProduct(newProd)
} catch(err) {
this.view.showErrorMessage('error', 'Error al añadir el producto')
}
}
...
}
export default class Store {
constructor (id) {
this.id=Number(id)
this.products=[]
}
addProduct(payload) {
// llama a métodos de los servicios para que añada el producto
const prod = await addProductToDatabase(payload)
let newProd = new Product(prod.id, prod.name, prod.price, prod.units)
this.products.push(newProd)
return newProd
}
...
}
export default class Product {
constructor (id, name, price, units) {
this.id = id
this.name = name
this.price = price
this.units = units
}
...
}
export default class View {
constructor {
this.messageDiv = document.getElementById('messages')
this.productForm = document.getElementById('product-form')
this.productsList = document.getElementById('products-table')
}
init() {
... // inicializa la vista, si es necesario
}
setSubmitHandler(callback) {
// código para que el controlador llame a la función callback
// cuando se envíe el formulario de añadir un producto
this.productForm.addEventListener('submit', (event) => {
event.preventDefault()
const name = document.getElementById('product-form-name').value
const price = document.getElementById('product-form-price').value
...
callback({ name, price, ... })
})
}
renderNewProduct(prod) {
// código para añadir a la tabla el producto pasado añadiendo una nueva fila
const DOMproduct = document.createElement('tr')
DOMproduct.innerHTML = `
<td>${newProd.id}</td>
<td>${newProd.name}</td>
<td>${newProd.price}</td>
<td>${newProd.units}</td>`
this.productsList.apendChild(DOMproduct)
}
showMessage(type, message) {
// código para mostrar mensajes al usuario y no tener que usar los alert
const DOMmessage = document.createElement('div')
...
this.messageDiv.apendChild(DOMmessage)
}
}
Podéis obtener más información y ver un ejemplo más completo en https://www.natapuntes.es/patron-mvc-en-vanilla-javascript/