Patrón MVC (Modelo-Vista-Controlador)
Separa una aplicación en el modelo (los datos y sus reglas), la vista (lo que se pinta) y el controlador (recibe lo que pide el usuario y los conecta). Es la base de Spring MVC, Laravel y ASP.NET.
nivel intermedioTambién: modelo vista controlador, model view controller, MVVM, MVP
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
MVC
Escribe las órdenes del usuario separadas por comas: nueva texto o hecha número.
Paso 1
El usuario escribe «nueva Leer el tema». Lo recibe el controlador, que no guarda datos ni pinta nada: decide a quién llamar.
1void orden(String linea) { // tareas = 0, pendientes = 0
2 try {
3 if (linea.startsWith("nueva ")) modelo.anadir(linea.substring(6));
4 else modelo.completar(Integer.parseInt(linea.substring(6)));
5 vista.mostrar(modelo);
6 } catch (IllegalArgumentException e) {
7 vista.error(e.getMessage());
8 }
9}Variables
- tareas
- 0
- pendientes
- 0
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
En una aplicación pequeña es tentador hacerlo todo en el mismo sitio: el método que lee el formulario también valida, guarda en la base de datos y escribe el HTML. Funciona hasta que hay que cambiar el diseño sin romper las reglas, añadir una versión para el móvil o probar las reglas sin abrir el navegador.
El patrón MVC reparte el trabajo. El modelo guarda los datos y hace cumplir sus reglas (una tarea no puede estar vacía), sin saber nada de pantallas. La vista solo pinta lo que le dan. El controlador recibe cada orden del usuario (una petición HTTP, un clic, una línea de texto), le pide al modelo que cambie y elige qué vista mostrar.
Gracias a esa separación se pueden tener varias vistas del mismo modelo (texto, barras, una API JSON), cambiar el diseño sin tocar las reglas y probar el modelo sin interfaz. Spring MVC, Laravel, ASP.NET MVC, Django y Ruby on Rails siguen esta idea; en Android y en los frameworks de JavaScript se usan variantes como MVVM, donde la vista se actualiza sola cuando cambia el modelo.
Cuándo usarlo
- Aplicaciones con interfaz (web, escritorio, móvil) que van a crecer o que mantiene más de una persona.
- El mismo dato se muestra de varias formas (una página, un PDF, una API) o en varios dispositivos.
- Quieres probar las reglas del negocio sin depender de la interfaz.
Cuándo no
- Un script o una página de un solo uso: la separación añade archivos sin aportar nada.
- Si el framework ya impone otra organización (por ejemplo, componentes con estado en React): hay que seguir la suya.
Participantes
- Modelo. Los datos y las reglas del negocio (
ModeloTareas). No conoce ni la vista ni el controlador. - Vista. Pinta el modelo (
VistaConsola): consola, HTML, una pantalla de Android. No decide nada. - Controlador. Recibe las órdenes del usuario (
ControladorTareas), actualiza el modelo y elige qué vista mostrar.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2class ModeloTareas {
3 -tareas : List<String>
4 -hechas : Set<Integer>
5 +anadir(t : String) : void
6 +completar(n : int) : void
7 +pendientes() : int
8}
9class VistaConsola {
10 +mostrar(m : ModeloTareas) : void
11 +error(mensaje : String) : void
12}
13class ControladorTareas {
14 -modelo : ModeloTareas
15 -vista : VistaConsola
16 +orden(linea : String) : void
17}
18class Usuario
19Usuario ..> ControladorTareas : órdenes
20ControladorTareas --> "1" ModeloTareas : modelo
21ControladorTareas --> "1" VistaConsola : vista
22VistaConsola ..> ModeloTareas : lee
23@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Una lista de tareas en tres capas
El controlador traduce cada orden en una llamada al modelo; si el modelo rechaza algo, el controlador se lo pasa a la vista para que lo muestre. La vista nunca cambia los datos.
1import java.util.*;
2
3/** El modelo: los datos y sus reglas. No sabe nada de pantallas ni de teclados. */
4class ModeloTareas {
5 private final List<String> tareas = new ArrayList<>();
6 private final Set<Integer> hechas = new HashSet<>();
7
8 void anadir(String t) {
9 if (t.isBlank()) throw new IllegalArgumentException("la tarea no puede estar vacía");
10 tareas.add(t.trim());
11 }
12
13 void completar(int n) {
14 if (n < 1 || n > tareas.size()) throw new IllegalArgumentException("no existe la tarea " + n);
15 hechas.add(n - 1);
16 }
17
18 List<String> tareas() { return Collections.unmodifiableList(tareas); }
19 boolean hecha(int i) { return hechas.contains(i); }
20 int pendientes() { return tareas.size() - hechas.size(); }
21}
22
23/** La vista: solo sabe pintar el modelo. Cambiarla por una web o una app no toca lo demás. */
24class VistaConsola {
25 void mostrar(ModeloTareas m) {
26 List<String> t = m.tareas();
27 for (int i = 0; i < t.size(); i++) System.out.println(" " + (m.hecha(i) ? "[x] " : "[ ] ") + (i + 1) + ". " + t.get(i));
28 System.out.println(" Pendientes: " + m.pendientes());
29 }
30
31 void error(String mensaje) { System.out.println(" Error: " + mensaje); }
32}
33
34/** El controlador: recibe lo que pide el usuario, actualiza el modelo y le dice a la vista qué mostrar. */
35class ControladorTareas {
36 private final ModeloTareas modelo;
37 private final VistaConsola vista;
38
39 ControladorTareas(ModeloTareas modelo, VistaConsola vista) {
40 this.modelo = modelo;
41 this.vista = vista;
42 }
43
44 void orden(String linea) {
45 try {
46 if (linea.startsWith("nueva ")) modelo.anadir(linea.substring(6));
47 else if (linea.startsWith("hecha ")) modelo.completar(Integer.parseInt(linea.substring(6).trim()));
48 else throw new IllegalArgumentException("orden desconocida");
49 vista.mostrar(modelo);
50 } catch (NumberFormatException e) {
51 vista.error("número no válido");
52 } catch (IllegalArgumentException e) {
53 vista.error(e.getMessage());
54 }
55 }
56}
57
58public class Main {
59 public static void main(String[] args) {
60 ControladorTareas c = new ControladorTareas(new ModeloTareas(), new VistaConsola());
61 for (String orden : List.of("nueva Leer el tema de MVC", "nueva Hacer la práctica", "hecha 1", "hecha 7", "borrar 1")) {
62 System.out.println("> " + orden);
63 c.orden(orden); // en una web, esto sería una petición HTTP
64 }
65 }
66}class ModeloTareas:
"""El modelo: los datos y sus reglas. No sabe nada de pantallas ni de teclados."""
def __init__(self):
self._tareas = []
self._hechas = set()
def anadir(self, t):
if not t.strip():
raise ValueError("la tarea no puede estar vacía")
self._tareas.append(t.strip())
def completar(self, n):
if n < 1 or n > len(self._tareas):
raise ValueError(f"no existe la tarea {n}")
self._hechas.add(n - 1)
def tareas(self):
return tuple(self._tareas)
def hecha(self, i):
return i in self._hechas
def pendientes(self):
return len(self._tareas) - len(self._hechas)
class VistaConsola:
"""La vista: solo sabe pintar el modelo. Cambiarla por una web o una app no toca lo demás."""
def mostrar(self, m):
for i, t in enumerate(m.tareas()):
print(f" {'[x]' if m.hecha(i) else '[ ]'} {i + 1}. {t}")
print(f" Pendientes: {m.pendientes()}")
def error(self, mensaje):
print(f" Error: {mensaje}")
class ControladorTareas:
"""El controlador: recibe lo que pide el usuario, actualiza el modelo y le dice a la vista qué mostrar."""
def __init__(self, modelo, vista):
self._modelo, self._vista = modelo, vista
def orden(self, linea):
try:
if linea.startswith("nueva "):
self._modelo.anadir(linea[6:])
elif linea.startswith("hecha "):
texto = linea[6:].strip()
if not texto.lstrip("-").isdigit():
self._vista.error("número no válido")
return
self._modelo.completar(int(texto))
else:
raise ValueError("orden desconocida")
self._vista.mostrar(self._modelo)
except ValueError as e:
self._vista.error(str(e))
c = ControladorTareas(ModeloTareas(), VistaConsola())
for orden in ["nueva Leer el tema de MVC", "nueva Hacer la práctica", "hecha 1", "hecha 7", "borrar 1"]:
print(f"> {orden}")
c.orden(orden) # en una web, esto sería una petición HTTP/** El modelo: los datos y sus reglas. No sabe nada de pantallas ni de teclados. */
class ModeloTareas {
#tareas = [];
#hechas = new Set();
anadir(t) {
if (!t.trim()) throw new Error("la tarea no puede estar vacía");
this.#tareas.push(t.trim());
}
completar(n) {
if (n < 1 || n > this.#tareas.length) throw new Error(`no existe la tarea ${n}`);
this.#hechas.add(n - 1);
}
tareas() { return [...this.#tareas]; }
hecha(i) { return this.#hechas.has(i); }
pendientes() { return this.#tareas.length - this.#hechas.size; }
}
/** La vista: solo sabe pintar el modelo. Cambiarla por una web o una app no toca lo demás. */
class VistaConsola {
mostrar(m) {
m.tareas().forEach((t, i) => console.log(` ${m.hecha(i) ? "[x]" : "[ ]"} ${i + 1}. ${t}`));
console.log(` Pendientes: ${m.pendientes()}`);
}
error(mensaje) { console.log(` Error: ${mensaje}`); }
}
/** El controlador: recibe lo que pide el usuario, actualiza el modelo y le dice a la vista qué mostrar. */
class ControladorTareas {
constructor(modelo, vista) { this.modelo = modelo; this.vista = vista; }
orden(linea) {
try {
if (linea.startsWith("nueva ")) this.modelo.anadir(linea.slice(6));
else if (linea.startsWith("hecha ")) {
const texto = linea.slice(6).trim();
if (!/^-?\d+$/.test(texto)) return this.vista.error("número no válido");
this.modelo.completar(Number(texto));
} else throw new Error("orden desconocida");
this.vista.mostrar(this.modelo);
} catch (e) {
this.vista.error(e.message);
}
}
}
const c = new ControladorTareas(new ModeloTareas(), new VistaConsola());
for (const orden of ["nueva Leer el tema de MVC", "nueva Hacer la práctica", "hecha 1", "hecha 7", "borrar 1"]) {
console.log(`> ${orden}`);
c.orden(orden); // en una web, esto sería una petición HTTP
}using System;
using System.Collections.Generic;
// El modelo: los datos y sus reglas. No sabe nada de pantallas ni de teclados.
class ModeloTareas {
private readonly List<string> tareas = new();
private readonly HashSet<int> hechas = new();
public void Anadir(string t) {
if (string.IsNullOrWhiteSpace(t)) throw new ArgumentException("la tarea no puede estar vacía");
tareas.Add(t.Trim());
}
public void Completar(int n) {
if (n < 1 || n > tareas.Count) throw new ArgumentException(quot;no existe la tarea {n}");
hechas.Add(n - 1);
}
public IReadOnlyList<string> Tareas => tareas;
public bool Hecha(int i) => hechas.Contains(i);
public int Pendientes => tareas.Count - hechas.Count;
}
// La vista: solo sabe pintar el modelo. Cambiarla por una web o una app no toca lo demás.
class VistaConsola {
public void Mostrar(ModeloTareas m) {
for (int i = 0; i < m.Tareas.Count; i++) Console.WriteLine(quot; {(m.Hecha(i) ? "[x]" : "[ ]")} {i + 1}. {m.Tareas[i]}");
Console.WriteLine(quot; Pendientes: {m.Pendientes}");
}
public void Error(string mensaje) => Console.WriteLine(quot; Error: {mensaje}");
}
// El controlador: recibe lo que pide el usuario, actualiza el modelo y le dice a la vista qué mostrar.
class ControladorTareas {
private readonly ModeloTareas modelo;
private readonly VistaConsola vista;
public ControladorTareas(ModeloTareas modelo, VistaConsola vista) { this.modelo = modelo; this.vista = vista; }
public void Orden(string linea) {
try {
if (linea.StartsWith("nueva ")) modelo.Anadir(linea[6..]);
else if (linea.StartsWith("hecha ")) {
if (!int.TryParse(linea[6..].Trim(), out int n)) { vista.Error("número no válido"); return; }
modelo.Completar(n);
} else throw new ArgumentException("orden desconocida");
vista.Mostrar(modelo);
} catch (ArgumentException e) {
vista.Error(e.Message);
}
}
}
class Program {
static void Main() {
var c = new ControladorTareas(new ModeloTareas(), new VistaConsola());
foreach (var orden in new[] { "nueva Leer el tema de MVC", "nueva Hacer la práctica", "hecha 1", "hecha 7", "borrar 1" }) {
Console.WriteLine(quot;> {orden}");
c.Orden(orden); // en una web, esto sería una petición HTTP
}
}
}<?php
/** El modelo: los datos y sus reglas. No sabe nada de pantallas ni de teclados. */
class ModeloTareas {
private array $tareas = [];
private array $hechas = [];
public function anadir(string $t): void {
if (trim($t) === "") throw new InvalidArgumentException("la tarea no puede estar vacía");
$this->tareas[] = trim($t);
}
public function completar(int $n): void {
if ($n < 1 || $n > count($this->tareas)) throw new InvalidArgumentException("no existe la tarea $n");
$this->hechas[$n - 1] = true;
}
public function tareas(): array { return $this->tareas; }
public function hecha(int $i): bool { return isset($this->hechas[$i]); }
public function pendientes(): int { return count($this->tareas) - count($this->hechas); }
}
/** La vista: solo sabe pintar el modelo. Cambiarla por una web o una app no toca lo demás. */
class VistaConsola {
public function mostrar(ModeloTareas $m): void {
foreach ($m->tareas() as $i => $t) echo " ", $m->hecha($i) ? "[x]" : "[ ]", " ", $i + 1, ". $t\n";
echo " Pendientes: {$m->pendientes()}\n";
}
public function error(string $mensaje): void { echo " Error: $mensaje\n"; }
}
/** El controlador: recibe lo que pide el usuario, actualiza el modelo y le dice a la vista qué mostrar. */
class ControladorTareas {
public function __construct(private ModeloTareas $modelo, private VistaConsola $vista) {}
public function orden(string $linea): void {
try {
if (str_starts_with($linea, "nueva ")) $this->modelo->anadir(substr($linea, 6));
elseif (str_starts_with($linea, "hecha ")) {
$texto = trim(substr($linea, 6));
if (!preg_match('/^-?\d+$/', $texto)) { $this->vista->error("número no válido"); return; }
$this->modelo->completar((int) $texto);
} else throw new InvalidArgumentException("orden desconocida");
$this->vista->mostrar($this->modelo);
} catch (InvalidArgumentException $e) {
$this->vista->error($e->getMessage());
}
}
}
$c = new ControladorTareas(new ModeloTareas(), new VistaConsola());
foreach (["nueva Leer el tema de MVC", "nueva Hacer la práctica", "hecha 1", "hecha 7", "borrar 1"] as $orden) {
echo "> $orden\n";
$c->orden($orden); // en una web, esto sería una petición HTTP
}Salida al ejecutarlo (la misma en los 5 lenguajes)
> nueva Leer el tema de MVC [ ] 1. Leer el tema de MVC Pendientes: 1 > nueva Hacer la práctica [ ] 1. Leer el tema de MVC [ ] 2. Hacer la práctica Pendientes: 2 > hecha 1 [x] 1. Leer el tema de MVC [ ] 2. Hacer la práctica Pendientes: 1 > hecha 7 Error: no existe la tarea 7 > borrar 1 Error: orden desconocida
MVC en un framework web
En Spring MVC el controlador recibe la petición HTTP, añade los datos al modelo y devuelve el nombre de la vista que los pinta.
1// Spring MVC: el controlador recibe la petición, pide los datos al modelo (servicio) y elige la vista
2@Controller
3public class TareasController {
4 private final TareasService servicio;
5
6 public TareasController(TareasService servicio) { this.servicio = servicio; }
7
8 @GetMapping("/tareas")
9 public String listar(Model model) {
10 model.addAttribute("tareas", servicio.todas()); // los datos para la vista
11 return "tareas"; // el nombre de la vista: tareas.html (Thymeleaf)
12 }
13
14 @PostMapping("/tareas")
15 public String crear(@RequestParam String texto) {
16 servicio.anadir(texto);
17 return "redirect:/tareas";
18 }
19}
20
21// tareas.html (la vista) solo pinta: <li th:each="t : ${tareas}" th:text="${t.texto}">…</li># Django: la vista de Django hace de controlador; la plantilla es la vista; el modelo, el modelo
def listar(request):
tareas = Tarea.objects.order_by("id") # el modelo
return render(request, "tareas.html", {"tareas": tareas}) # la plantilla pinta
def crear(request):
Tarea.objects.create(texto=request.POST["texto"])
return redirect("listar")
# tareas.html solo pinta: {% for t in tareas %}<li>{{ t.texto }}</li>{% endfor %}// Express: la ruta hace de controlador, el modelo se encarga de los datos y la plantilla es la vista
app.get("/tareas", async (req, res) => {
const tareas = await Tarea.findAll(); // el modelo
res.render("tareas", { tareas }); // la vista (tareas.ejs) pinta
});
app.post("/tareas", async (req, res) => {
await Tarea.create({ texto: req.body.texto });
res.redirect("/tareas");
});// ASP.NET Core MVC: el controlador devuelve una vista Razor con su modelo
public class TareasController : Controller {
private readonly TareasService servicio;
public TareasController(TareasService servicio) => this.servicio = servicio;
public IActionResult Index() => View(servicio.Todas()); // Views/Tareas/Index.cshtml pinta
[HttpPost]
public IActionResult Crear(string texto) {
servicio.Anadir(texto);
return RedirectToAction(nameof(Index));
}
}<?php
// Laravel: la ruta lleva al controlador, que pide los datos al modelo (Eloquent) y devuelve una vista Blade
class TareaController extends Controller {
public function index() {
return view("tareas.index", ["tareas" => Tarea::orderBy("id")->get()]);
}
public function store(Request $request) {
Tarea::create($request->validate(["texto" => "required|max:200"]));
return redirect()->route("tareas.index");
}
}
// resources/views/tareas/index.blade.php solo pinta: @foreach ($tareas as $t) <li>{{ $t->texto }}</li> @endforeachEn la práctica
- Spring MVC (
@Controller+ Thymeleaf), Laravel (rutas, controladores y vistas Blade), ASP.NET MVC y Django (que lo llama MTV). - JavaFX con FXML: la vista en el
.fxml, el controlador en su clase y el modelo aparte. - Android con MVVM: el
ViewModelexpone los datos y la actividad o la pantalla de Compose los pinta. - Los proyectos de 2º de DAW y DAM y el proyecto intermodular: casi todos se organizan así.
Errores típicos
- Poner reglas del negocio en el controlador o en la vista (validar en el HTML y no en el modelo): otra vista podría saltárselas.
- Que el modelo escriba en pantalla o lea del teclado: deja de poder usarse con otra interfaz.
- Controladores gordos que hacen consultas, cálculos y formato: el controlador solo coordina.
- Que la vista consulte la base de datos directamente: los datos le llegan del controlador.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Un conversor de unidades con MVC
El modelo (Conversor) ya sabe convertir y guardar el historial, y la vista (Vista) ya sabe escribir resultados, historiales y errores. Falta el controlador: escribe Controlador, que recibe cada línea, llama al modelo y le dice a la vista qué mostrar.
- Órdenes:
convertir valor de a(por ejemploconvertir 10 km millasoconvertir 100 c f; el valor puede llevar coma) ehistorial. - Todo lo que se escribe pasa por la vista: el resultado, el historial o
Error: mensaje. Errores:número no válido, el mensaje del modelo si no sabe convertir esas unidades, yorden no válidapara lo demás.
Ejemplo
convertir 10 km millas convertir 100 c f convertir 5,5 kg libras historial
10 km = 6,21 millas 100 c = 212 f 5,5 kg = 12,13 libras Historial: 10 km → millas · 100 c → f · 5,5 kg → libras
Ver la solución explicada
1import java.math.*;
2import java.util.*;
3
4/** El modelo: sabe convertir unidades y recuerda las conversiones hechas. */
5class Conversor {
6 private static final Map<String, Double> FACTOR = Map.of(
7 "km-millas", 0.621371, "millas-km", 1.609344,
8 "kg-libras", 2.20462, "libras-kg", 0.453592,
9 "m-pies", 3.28084, "pies-m", 0.3048);
10 private final List<String> historial = new ArrayList<>();
11
12 double convertir(double valor, String de, String a) {
13 double r;
14 if (de.equals("c") && a.equals("f")) r = valor * 9 / 5 + 32;
15 else if (de.equals("f") && a.equals("c")) r = (valor - 32) * 5 / 9;
16 else {
17 Double f = FACTOR.get(de + "-" + a);
18 if (f == null) throw new IllegalArgumentException("no sé convertir de " + de + " a " + a);
19 r = valor * f;
20 }
21 historial.add(formato(valor) + " " + de + " → " + a);
22 return r;
23 }
24
25 /** El modelo tiene su propio formato para el historial: no depende de la vista. */
26 private static String formato(double x) {
27 return BigDecimal.valueOf(x).stripTrailingZeros().toPlainString().replace('.', ',');
28 }
29
30 List<String> historial() { return Collections.unmodifiableList(historial); }
31}
32
33/** La vista: solo escribe. No calcula ni decide nada. */
34class Vista {
35 static String num(double x) {
36 return BigDecimal.valueOf(x).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString().replace('.', ',');
37 }
38
39 void resultado(double valor, String de, double r, String a) {
40 System.out.println(num(valor) + " " + de + " = " + num(r) + " " + a);
41 }
42
43 void historial(List<String> h) {
44 System.out.println(h.isEmpty() ? "Historial vacío" : "Historial: " + String.join(" · ", h));
45 }
46
47 void error(String mensaje) { System.out.println("Error: " + mensaje); }
48}
49
50/** El controlador: traduce cada orden en llamadas al modelo y a la vista. */
51class Controlador {
52 private final Conversor modelo;
53 private final Vista vista;
54
55 Controlador(Conversor modelo, Vista vista) {
56 this.modelo = modelo;
57 this.vista = vista;
58 }
59
60 void procesar(String linea) {
61 String[] p = linea.trim().split("\\s+");
62 try {
63 if (p[0].equals("convertir") && p.length == 4) {
64 double v = Double.parseDouble(p[1].replace(',', '.'));
65 double r = modelo.convertir(v, p[2], p[3]);
66 vista.resultado(v, p[2], r, p[3]);
67 } else if (p[0].equals("historial") && p.length == 1) {
68 vista.historial(modelo.historial());
69 } else {
70 vista.error("orden no válida");
71 }
72 } catch (NumberFormatException e) {
73 vista.error("número no válido");
74 } catch (IllegalArgumentException e) {
75 vista.error(e.getMessage());
76 }
77 }
78}
79
80public class Main {
81 public static void main(String[] args) {
82 Controlador c = new Controlador(new Conversor(), new Vista());
83 Scanner sc = new Scanner(System.in);
84 while (sc.hasNextLine()) {
85 String linea = sc.nextLine();
86 if (!linea.isBlank()) c.procesar(linea);
87 }
88 }
89}Cada parte hace una cosa: el modelo calcula, la vista escribe y el controlador decide a quién llamar.
Si mañana el conversor es una página web, el modelo se reutiliza entero y solo cambian la vista y el controlador.
2. Una encuesta con dos vistas
La encuesta tiene un controlador y una vista de texto. Completa el modelo Encuesta (votar, total, porcentaje y ganador) y escribe una segunda vista, VistaBarras, que pinte el mismo modelo con barras. El controlador cambia de vista sin tocar el modelo.
- La primera línea es
opciones A,B,C. Después:votar opción(si no existe,No existe la opción X),vista texto|barrasyresultado. - El porcentaje se redondea al entero más cercano (0 si no hay votos). El ganador es
Gana X,Empate entre X y Y(en el orden de las opciones) oSin votos todavía. - Barras: una línea por opción con el nombre alineado a la izquierda al ancho del más largo,
|, una#por cada 5 % (redondeado) rellenado con espacios hasta 20,|y el porcentaje en 3 caracteres con%; al final,N votos · ganador.
Ejemplo
opciones Java,Python,PHP votar Java votar Python votar Java votar PHP votar Java resultado vista barras resultado
Java: 3 votos Python: 1 voto PHP: 1 voto Gana Java Java |############ | 60 % Python |#### | 20 % PHP |#### | 20 % 5 votos · Gana Java
Ver la solución explicada
1import java.util.*;
2
3/** La vista: una interfaz, para poder tener varias que pintan el mismo modelo. */
4interface Vista {
5 void mostrar(Encuesta e);
6}
7
8class VistaTexto implements Vista {
9 public void mostrar(Encuesta e) {
10 for (String o : e.opciones()) System.out.println(o + ": " + e.votos(o) + (e.votos(o) == 1 ? " voto" : " votos"));
11 System.out.println(e.ganador());
12 }
13}
14
15/** El modelo: las opciones, los votos y las reglas (votar, porcentajes, quién gana). */
16class Encuesta {
17 private final Map<String, Integer> votos = new LinkedHashMap<>();
18
19 Encuesta(List<String> opciones) {
20 for (String o : opciones) votos.put(o, 0);
21 }
22
23 void votar(String opcion) {
24 if (!votos.containsKey(opcion)) throw new IllegalArgumentException("No existe la opción " + opcion);
25 votos.merge(opcion, 1, Integer::sum);
26 }
27
28 List<String> opciones() { return new ArrayList<>(votos.keySet()); }
29 int votos(String opcion) { return votos.get(opcion); }
30 int total() { return votos.values().stream().mapToInt(Integer::intValue).sum(); }
31
32 /** Porcentaje redondeado al entero más cercano (0 si no hay votos). */
33 int porcentaje(String opcion) {
34 return total() == 0 ? 0 : (int) Math.round(100.0 * votos(opcion) / total());
35 }
36
37 String ganador() {
38 if (total() == 0) return "Sin votos todavía";
39 int max = Collections.max(votos.values());
40 List<String> primeros = new ArrayList<>();
41 for (var e : votos.entrySet()) if (e.getValue() == max) primeros.add(e.getKey());
42 return primeros.size() == 1 ? "Gana " + primeros.get(0) : "Empate entre " + String.join(" y ", primeros);
43 }
44}
45
46/** Otra vista del mismo modelo: barras de # (una por cada 5 %). */
47class VistaBarras implements Vista {
48 public void mostrar(Encuesta e) {
49 int ancho = 0;
50 for (String o : e.opciones()) ancho = Math.max(ancho, o.length());
51 for (String o : e.opciones()) {
52 int pct = e.porcentaje(o);
53 String barra = "#".repeat((int) Math.round(pct / 5.0));
54 System.out.println(String.format("%-" + ancho + "s |%-20s| %3d %%", o, barra, pct));
55 }
56 System.out.println(e.total() + (e.total() == 1 ? " voto" : " votos") + " · " + e.ganador());
57 }
58}
59
60/** El controlador: recibe las órdenes, cambia el modelo y elige la vista. */
61class Controlador {
62 private final Encuesta modelo;
63 private final Map<String, Vista> vistas = Map.of("texto", new VistaTexto(), "barras", new VistaBarras());
64 private Vista vista = vistas.get("texto");
65
66 Controlador(Encuesta modelo) { this.modelo = modelo; }
67
68 void procesar(String linea) {
69 String[] p = linea.trim().split("\\s+", 2);
70 try {
71 switch (p[0]) {
72 case "votar" -> modelo.votar(p.length > 1 ? p[1] : "");
73 case "vista" -> {
74 Vista v = vistas.get(p.length > 1 ? p[1] : "");
75 if (v == null) System.out.println("No existe la vista " + (p.length > 1 ? p[1] : ""));
76 else vista = v;
77 }
78 case "resultado" -> vista.mostrar(modelo);
79 default -> System.out.println("Orden no válida: " + linea.trim());
80 }
81 } catch (IllegalArgumentException e) {
82 System.out.println(e.getMessage());
83 }
84 }
85}
86
87public class Main {
88 public static void main(String[] args) {
89 Scanner sc = new Scanner(System.in);
90 String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
91 if (!primera.startsWith("opciones ")) {
92 System.out.println("Faltan las opciones");
93 return;
94 }
95 Controlador c = new Controlador(new Encuesta(List.of(primera.substring(9).split(","))));
96 while (sc.hasNextLine()) {
97 String linea = sc.nextLine();
98 if (!linea.isBlank()) c.procesar(linea);
99 }
100 }
101}Todas las reglas (votos válidos, porcentajes, empates) están en el modelo: las dos vistas muestran siempre lo mismo.
Añadir una tercera vista (un gráfico, un JSON) no exige tocar ni el modelo ni el controlador, salvo para registrarla.
Test
Test: MVC (Modelo-Vista-Controlador)
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué parte de MVC contiene las reglas del negocio?
2.¿Qué hace el controlador?
3.¿Qué ventaja da separar la vista del modelo?
4.En Spring MVC, ¿qué devuelve normalmente un método de un @Controller?
5.¿Dónde no debería estar la validación de que una tarea no está vacía?