Programación Avanzada 2.º BT TI · DGETP / UTU
Profe Macón
Programación Avanzada
2.º BT · Tecnologías de la Información
PM
Tienda de Cómics · Arquitectura de componentes

ProductoController

En el material anterior descubrimos que Main estaba haciendo demasiado. Ahora vamos a crear el primer componente que nos permitirá sacarle una responsabilidad concreta: coordinar las acciones que nacen en la Vista.

CET1ControllerResponsabilidadesRefactorizaciónModularidadEventos
1 · Retomamos el problema

Main todavía conoce demasiado

Nuestra aplicación funciona, pero Main sigue decidiendo qué hacer cuando el usuario pulsa los botones.

Producto_Vista ventana = new Producto_Vista();
ventana.setVisible(true);

ventana.listenerCancelar(e ->
    ventana.limpiarCampos());

ventana.listenerAgregar(e -> {
    long codigo = Long.parseLong(ventana.getTxtCodigo());
    String nombre = ventana.getTxtNombre();
    double precio = Double.parseDouble(ventana.getTxtPrecio());
    int stock = Integer.parseInt(ventana.getTxtStock());

    Producto p = new Producto(codigo, nombre, precio, stock);
    listaProductos.add(p);

    JOptionPane.showMessageDialog(ventana, listaProductos);
});

ventana.listenerEliminar(e -> {
    JOptionPane.showConfirmDialog(ventana, "¿Está seguro?");
});
La pregunta de hoy: si la Vista ya sabe qué botón fue presionado, ¿quién debería coordinar qué ocurre después?

En el proyecto actual la clase Swing se llama Producto_Vista. Mantendremos ese nombre para que el material coincida con el código que ya tienen.

2 · El papel del Controller

La Vista recibe la interacción. El Controller coordina la respuesta.

El Controller es el componente que escucha o recibe acciones originadas en la interfaz y organiza qué debe ocurrir a continuación.

Usuario
↓ clic
Producto_Vista
↓ evento
ProductoController
↓ coordina
Service · más adelante

Controller sí debería…

  • registrar qué hacer ante los eventos;
  • pedir datos a la Vista;
  • coordinar una operación;
  • decirle a la Vista cuándo debe limpiarse o actualizarse.

Controller no debería terminar…

  • diseñando botones y paneles;
  • acumulando todas las reglas del negocio;
  • encargándose para siempre de guardar los datos;
  • convirtiéndose en un nuevo Main gigante.
Importante: hoy no vamos a resolver toda la arquitectura. Vamos a mover una responsabilidad por vez. Que el Controller quede todavía con tareas de más será justamente el problema del próximo material.
3 · Creamos el nuevo componente

Un paquete nuevo para una responsabilidad nueva

Dentro del proyecto creamos el paquete:

uy.edu.utu.programacion.controller

y dentro de él la clase:

ProductoController.java
Primera versióntodavía no hace nada
package uy.edu.utu.programacion.controller;

public class ProductoController {

}

La clase ya existe, pero todavía no puede controlar nada porque no conoce la Vista.

Pregunta guía: ¿conviene que ProductoController cree una ventana nueva por su cuenta, o que Main le entregue la misma ventana que ya creó?
4 · Conectamos Controller y Vista

Main crea las piezas. El constructor las conecta.

El Controller necesita trabajar con la Vista existente. Por eso guardamos una referencia como atributo y la recibimos por el constructor.

package uy.edu.utu.programacion.controller;

import uy.edu.utu.programacion.view.Producto_Vista;

public class ProductoController {

    private Producto_Vista vista;

    public ProductoController(Producto_Vista vista) {
        this.vista = vista;
    }
}
Main crea Vista → Main crea Controller → le entrega la Vista

¿Qué significa this.vista = vista?

El parámetro vista existe solamente durante la ejecución del constructor. Con this.vista guardamos esa referencia en el atributo del objeto para poder usarla después desde otros métodos del Controller.

Una decisión de diseño: el Controller no crea su propia Vista. La recibe. Así Main conserva la tarea de construir y conectar los componentes principales.
5 · Primera mudanza

Empezamos por Cancelar porque es la acción más simple

Hoy la respuesta al botón está dentro de Main:

ventana.listenerCancelar(e ->
    ventana.limpiarCampos());

La movemos al Controller. Pero antes de escribir código nuevo aparece una decisión de organización: ¿dónde vamos a registrar los listeners de los botones?

configurarEventos() no es un método de Java ni de Swing. Es un método que creamos nosotros para reunir en un mismo lugar las conexiones entre los eventos de la Vista y las acciones del Controller. Podríamos poner esas líneas directamente en el constructor, pero cuando tengamos varios botones el constructor empezaría a llenarse de listeners.

Por eso agregamos al Controller un método privado llamado configurarEventos(). El nombre también es una decisión nuestra: describe su única tarea.

private void configurarEventos() {
    // Aquí iremos registrando los eventos de la Vista
}

Ahora sí registramos el primer evento. Cuando la Vista avise que se pulsó Cancelar, el Controller ejecutará su método cancelar():

private void configurarEventos() {
    vista.listenerCancelar(e -> cancelar());
}
Importante: este método tampoco se ejecuta «solo». Más abajo veremos que el constructor de ProductoController llama explícitamente a configurarEventos() cuando se crea el controlador.

Después separamos la acción concreta en otro método:

private void cancelar() {
    vista.limpiarCampos();
}

Finalmente hacemos que el constructor configure los eventos al crear el Controller:

public ProductoController(Producto_Vista vista) {
    this.vista = vista;
    configurarEventos();
}
¿Por qué separar configurarEventos() de cancelar()? Porque una cosa es decir qué evento dispara una acción y otra es definir qué hace esa acción. El código queda más fácil de leer y después podremos agregar más eventos sin llenar el constructor.
Probamos ahora. El usuario pulsa Cancelar y ocurre exactamente lo mismo que antes. No agregamos una funcionalidad nueva: cambiamos la organización interna.
¿Eso tiene un nombre?

Sí. Llamamos refactorización a modificar la estructura interna del código procurando mantener su comportamiento observable. La pantalla hace lo mismo; el código queda mejor organizado.

6 · Movemos Agregar

Ahora trasladamos una acción que tiene más pasos

Para mantener el mismo comportamiento que ya tiene el proyecto, por ahora el Controller también tendrá el ArrayList<Producto>. No asumimos que este sea el diseño definitivo.

import java.util.ArrayList;
import uy.edu.utu.programacion.model.Producto;

public class ProductoController {

    private Producto_Vista vista;
    private ArrayList<Producto> listaProductos;

    public ProductoController(Producto_Vista vista) {
        this.vista = vista;
        this.listaProductos = new ArrayList<Producto>();
        configurarEventos();
    }
    ...
}

Registramos el evento:

private void configurarEventos() {
    vista.listenerCancelar(e -> cancelar());
    vista.listenerAgregar(e -> agregarProducto());
}

y trasladamos la lógica que antes estaba dentro de la lambda de Main:

private void agregarProducto() {
    long codigo = Long.parseLong(vista.getTxtCodigo());
    String nombre = vista.getTxtNombre();
    double precio = Double.parseDouble(vista.getTxtPrecio());
    int stock = Integer.parseInt(vista.getTxtStock());

    Producto p = new Producto(codigo, nombre, precio, stock);
    listaProductos.add(p);

    JOptionPane.showMessageDialog(vista, listaProductos);
}
Atención: todavía no estamos resolviendo errores de formato ni reglas como “precio mayor que cero”. Primero queremos que el programa conserve el mismo comportamiento. En el próximo SCORM distinguiremos mejor qué validaciones pertenecen al Controller y cuáles al Service.
7 · Movemos Eliminar

Misma idea, otro evento

En el proyecto actual el botón Eliminar todavía solo pregunta si el usuario está seguro. Trasladamos exactamente esa conducta; no inventamos aún el borrado completo.

private void configurarEventos() {
    vista.listenerCancelar(e -> cancelar());
    vista.listenerAgregar(e -> agregarProducto());
    vista.listenerEliminar(e -> confirmarEliminar());
}
private void confirmarEliminar() {
    JOptionPane.showConfirmDialog(vista, "¿Está seguro?");
}
Regla de la refactorización: primero conservamos el comportamiento. Después agregaremos funcionalidades nuevas. Si cambiamos organización y comportamiento al mismo tiempo, será más difícil descubrir de dónde viene un error.

Hasta ahora, ¿qué ganamos?

MainYa no necesita saber qué hacen los botones.
VistaSigue mostrando componentes y exponiendo eventos, sin decidir la lógica.
ControllerConcentra la coordinación de las acciones del usuario.
8 · Volvemos a Main

El cambio se hace visible cuando miramos el punto de entrada

Ahora Main puede quedar así:

package uy.edu.utu.programacion;

import uy.edu.utu.programacion.controller.ProductoController;
import uy.edu.utu.programacion.view.Producto_Vista;

public class Main {
    public static void main(String[] args) {

        Producto_Vista ventana = new Producto_Vista();
        ProductoController controller =
                new ProductoController(ventana);

        ventana.setVisible(true);
    }
}

Antes

Main creaba, escuchaba, leía campos, convertía, construía productos, guardaba y mostraba mensajes.

→

Ahora

Main crea los componentes principales, los conecta y pone en marcha la aplicación.

¿Hace falta usar después la variable controller dentro de Main? No necesariamente. La dejamos porque hace explícito que existe un objeto Controller conectado con la Vista y porque más adelante le entregaremos también un Service.
9 · Seguimos un clic

¿Qué pasa ahora cuando el usuario pulsa “Agregar”?

1Vista

El botón genera un evento.

2Controller

El listener registrado llama a agregarProducto().

3Controller

Pide los textos a la Vista, los convierte y crea el objeto.

4Controller

Lo agrega temporalmente a la colección y muestra el resultado.

clic → Vista → Controller → Producto / ArrayList

La Vista sigue siendo pasiva

Los botones continúan privados dentro de Producto_Vista. La Vista solamente ofrece métodos como listenerAgregar(...) y getters como getTxtCodigo(). Así, el Controller no necesita acceder directamente a los componentes internos.

Encapsulamiento aplicado: el Controller pide lo que necesita mediante la interfaz pública de la Vista. No entra a manipular directamente sus atributos privados.
10 · Práctica graduada

Ahora te toca mover responsabilidades

Mínimo viable

Crear ProductoController, recibir la Vista por constructor y trasladar únicamente la acción Cancelar.

Checkpoint: Main ya no contiene el listener de Cancelar.

Estándar

Trasladar Cancelar, Agregar y Eliminar, usando configurarEventos() y métodos privados para cada acción.

Checkpoint: Main solo crea, conecta y muestra.

Desafío

Sin cambiar el comportamiento, identificar con comentarios cuáles líneas de agregarProducto() son coordinación de interfaz y cuáles empiezan a parecer lógica que quizá debería vivir en otro componente.

Pista: pensá en el futuro Service.

Si algo deja de funcionar: mové una sola acción, ejecutá y verificá. No traslades todo el código de una vez. Una refactorización pequeña permite encontrar errores mucho más rápido.
11 · Comprobamos la idea

Controller, Vista o Main

Elegí el componente que mejor encaja con cada responsabilidad en la arquitectura que estamos construyendo.

1. Registrar qué método debe ejecutarse cuando se pulsa “Agregar”.

2. Mantener privados los JButton y JTextField y exponer getters o métodos de listeners.

3. Crear la Vista y el Controller y conectarlos al iniciar el programa.

4. Pedir a la Vista el texto escrito en el campo precio después de que el usuario inicia la operación Agregar.

Chequeo conceptual: el Controller no reemplaza a la Vista. La Vista sigue siendo la dueña de los componentes gráficos; el Controller coordina las acciones que se originan allí.
12 · Cierre

Resolvimos un problema… y apareció el siguiente

Logramos nuestro objetivo inmediato:

Main grande → ProductoController → Main pequeño

Pero ahora miremos con atención a ProductoController. Además de coordinar eventos, está:

Nueva pregunta: ¿estamos empezando a trasladar el problema de Main hacia el Controller?

Micro-entrega de documentación

Ticket de salida: completá la frase: “El Controller no existe para dibujar la interfaz ni para guardar permanentemente los datos; existe principalmente para…”

Próximo material: ProductoService. Vamos a distinguir coordinación de interfaz, validación de entrada y lógica de negocio, y sacaremos del Controller las operaciones que no deberían depender de Swing.

Material en curso