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

ProductoService

Ya sacamos de Main la coordinación de los botones. Pero ahora nuestro ProductoController empieza a acumular tareas que no dependen de la pantalla. Vamos a separar la coordinación de la interfaz de las reglas y operaciones del negocio.

CET1ServiceLógica de negocioResponsabilidadesValidaciónModularidad
1 · Retomamos el problema

El Controller resolvió un problema… pero empezó a heredar otros

Después del material anterior, Main quedó pequeño. Ahora miremos con lupa el método agregarProducto() del Controller.

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);
}

En pocas líneas aparecen tareas bastante diferentes:

Leer

Pedir datos a la Vista.

Convertir

Pasar texto a números.

Negocio

Decidir si un producto es válido y crearlo.

Datos

Guardar el producto en la colección.

Nueva pregunta: ¿todo eso forma parte de “controlar la interfaz”, o estamos empezando a trasladar al Controller el mismo problema que antes tenía Main?
2 · Separamos dos tipos de problema

“No puedo convertirlo” no es lo mismo que “no debería aceptarlo”

Antes de crear ninguna clase nueva, comparemos dos entradas posibles para el precio.

El usuario escribe hola

Double.parseDouble("hola") no puede convertir ese texto en un número.

Es un problema de entrada o formato.

El usuario escribe -500

Java puede convertirlo perfectamente a double.

Pero una tienda no debería aceptar un precio negativo. Es una regla del negocio.

PreguntaResponsabilidad
¿Lo escrito puede convertirse al tipo que necesitamos?Controller, porque está interpretando la entrada que llega desde la Vista.
¿Ese valor tiene sentido para nuestra aplicación?Service, porque la regla seguiría existiendo aunque cambiáramos Swing por otra interfaz.
Prueba mental: si mañana reemplazáramos Swing por una aplicación web, “el precio debe ser mayor que cero” seguiría siendo cierto. Esa regla no pertenece a la ventana.
3 · Aparece una nueva responsabilidad

Necesitamos un componente que conozca las operaciones del negocio

No vamos a crear ProductoService porque “la arquitectura dice que tiene que estar”. Lo necesitamos porque encontramos tareas que no deberían depender de la Vista.

Vista → Controller → Service
Controller

Coordina la interacción: escucha eventos, lee la Vista, convierte entradas y decide qué mostrar.

Service

Aplica reglas y realiza operaciones del negocio: crear, agregar, buscar, eliminar, etc.

Vista

Muestra componentes, recibe datos del usuario y expone eventos y valores.

Idea clave: Service no debería saber que existen JButton, JTextField ni JOptionPane.
4 · Creamos el paquete y la clase

Primero, una clase vacía con una responsabilidad clara

Creamos el paquete:

uy.edu.utu.programacion.service

y dentro:

ProductoService.java
package uy.edu.utu.programacion.service;

public class ProductoService {

}

Por ahora no hace nada. Antes de agregar métodos, decidimos qué responsabilidad vamos a trasladarle primero.

La primera mudanza será la colección. El ArrayList<Producto> representa los productos con los que trabaja la aplicación; no es un detalle de la pantalla.
5 · La colección cambia de dueño

El ArrayList sale del Controller y pasa al Service

En la versión anterior teníamos:

public class ProductoController {
    private Producto_Vista vista;
    private ArrayList<Producto> listaProductos;
    ...
}

Ahora el Service será quien mantenga esa colección:

package uy.edu.utu.programacion.service;

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

public class ProductoService {

    private ArrayList<Producto> listaProductos;

    public ProductoService() {
        listaProductos = new ArrayList<Producto>();
    }
}
¿Por qué la lista es private? Porque queremos que los demás componentes pidan operaciones al Service, en lugar de modificar libremente su colección desde afuera.

Necesitamos una operación para consultar los productos

En lugar de entregar la lista interna para que cualquiera pueda modificarla, devolvemos una copia:

public ArrayList<Producto> listarProductos() {
    return new ArrayList<Producto>(listaProductos);
}
¿Qué hace ese new ArrayList(...)? Construye otra lista con los mismos productos. Quien la recibe puede recorrerla o mostrarla sin tener acceso directo a la colección que administra el Service.
6 · El Service empieza a hacer su trabajo

Antes de crear un Producto, comprobamos las reglas del negocio

Para nuestra Tienda de Cómics estableceremos estas reglas mínimas:

Ahora sí creamos una operación del Service:

public Producto agregarProducto(long codigo, String nombre,
                                double precio, int stock) {

    if (codigo <= 0) {
        throw new IllegalArgumentException("El código debe ser mayor que cero");
    }

    if (nombre == null || nombre.isBlank()) {
        throw new IllegalArgumentException("El nombre no puede estar vacío");
    }

    if (precio <= 0) {
        throw new IllegalArgumentException("El precio debe ser mayor que cero");
    }

    if (stock < 0) {
        throw new IllegalArgumentException("El stock no puede ser negativo");
    }

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

    return producto;
}
Observá el cambio: el Service no recibe textos de JTextField. Recibe datos ya convertidos. Su pregunta no es “¿puedo transformar esto en un número?”, sino “¿estos valores cumplen las reglas de la aplicación?”.
7 · ¿Y qué significa throw?

El Service debe poder avisar que una operación no puede realizarse

Acaba de aparecer una instrucción nueva, así que no la usamos como si fuera mágica.

throw new IllegalArgumentException("El precio debe ser mayor que cero");

throw significa: interrumpí esta operación y avisá que ocurrió un problema. En este caso usamos IllegalArgumentException porque los datos recibidos no cumplen las condiciones que exige el método.

Controller pide agregar → Service detecta regla inválida → lanza excepción

El Service no muestra un JOptionPane. Solo comunica el problema. Quien conoce la interfaz decidirá cómo presentarlo.

Separación importante: una regla de negocio puede fallar sin saber si el programa usa Swing, consola, web o cualquier otra interfaz.
8 · El Controller queda como coordinador

Ahora le entregamos un Service al Controller

Igual que hicimos con la Vista, el Controller no crea el Service por su cuenta: lo recibe por el constructor y guarda una referencia.

private Producto_Vista vista;
private ProductoService service;

public ProductoController(Producto_Vista vista,
                          ProductoService service) {
    this.vista = vista;
    this.service = service;
    configurarEventos();
}

Después simplificamos agregarProducto(). Primero intentamos convertir los textos. Si el formato es correcto, pedimos al Service que realice la operación.

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

        service.agregarProducto(codigo, nombre, precio, stock);

        JOptionPane.showMessageDialog(
            vista,
            service.listarProductos()
        );

    } catch (NumberFormatException e) {
        JOptionPane.showMessageDialog(
            vista,
            "Código, precio y stock deben ser números válidos"
        );

    } catch (IllegalArgumentException e) {
        JOptionPane.showMessageDialog(
            vista,
            e.getMessage()
        );
    }
}

¿Por qué hay dos catch?

ExcepciónQué significa aquíQuién originó el problema
NumberFormatExceptionNo se pudo convertir un texto a número.La entrada recibida desde la Vista.
IllegalArgumentExceptionLos datos sí tienen el tipo correcto, pero violan una regla.El Service detectó una condición inválida.
El Controller traduce resultados para la interfaz. Recibe un problema del Service y decide mostrarlo con JOptionPane. El Service sigue sin conocer Swing.
9 · Main conecta tres piezas

Ahora Main arma la aplicación

Como el Controller necesita una Vista y un Service, Main crea ambos y se los entrega:

package uy.edu.utu.programacion;

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

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

        Producto_Vista vista = new Producto_Vista();
        ProductoService service = new ProductoService();

        ProductoController controller =
                new ProductoController(vista, service);

        vista.setVisible(true);
    }
}
Main · crea y conecta
↓
ProductoController · coordina
↓
ProductoService · aplica reglas y operaciones
↓
ArrayList<Producto> · almacenamiento temporal
La frase que queremos conservar: Main arma la aplicación. Controller coordina. Service resuelve el negocio.
10 · Seguimos una operación completa

¿Qué ocurre ahora cuando el usuario agrega un producto?

1Vista

El usuario escribe los datos y pulsa Agregar.

2Controller

Lee los textos e intenta convertir código, precio y stock.

3Service

Comprueba las reglas, crea el objeto y lo agrega a su colección.

4Controller

Pide la lista y decide cómo mostrar el resultado en la Vista.

Usuario → Vista → Controller → Service → ArrayList

Probemos tres situaciones

EntradaQué ocurre
Precio = holaController no puede convertirlo y muestra un mensaje de formato.
Precio = -500Controller lo convierte; Service rechaza la regla y comunica el motivo.
Precio = 500Service valida, crea el Producto y lo agrega a la colección.
11 · Práctica graduada

Ahora te toca separar el negocio del control

Mínimo viable

Crear ProductoService, mover allí el ArrayList y crear agregarProducto(...).

Checkpoint: Controller ya no tiene una colección de productos.

Estándar

Agregar las cuatro reglas de negocio y adaptar Controller con los dos tipos de error: formato y regla inválida.

Checkpoint: Service no importa ninguna clase de Swing.

Desafío

Agregar la regla “no puede existir otro producto con el mismo código”. La búsqueda debe hacerse dentro del Service.

Pregunta: ¿por qué esta regla necesita conocer la colección completa?

Plan de rescate: si se mezclan demasiados conceptos, hacé la mudanza en dos pasos: primero solo mover ArrayList y agregar; después incorporar reglas y excepciones.
12 · Comprobamos la idea

¿Controller o Service?

Elegí dónde ubicarías cada responsabilidad en la arquitectura que acabamos de construir.

1. Convertir el texto "250.50" recibido de un JTextField a double.

2. Comprobar que el precio sea mayor que cero.

3. Crear el objeto Producto y agregarlo a la colección cuando cumple las reglas.

4. Mostrar con JOptionPane el mensaje que explica por qué la operación fue rechazada.

5. Comprobar que no exista otro producto con el mismo código.

Regla práctica: preguntate “¿esta decisión seguiría siendo necesaria si cambiara completamente la interfaz?”. Si la respuesta es sí, probablemente no pertenece al Controller.
13 · Cierre

Ahora cada componente tiene una responsabilidad más clara

Main

Crea y conecta los componentes principales.

Controller

Coordina la interacción entre la Vista y las operaciones de la aplicación.

Service

Aplica reglas, crea productos y administra las operaciones sobre ellos.

Pero todavía hay una limitación evidente:

Service → ArrayList en memoria → cerramos el programa → los datos desaparecen
El próximo problema ya está preparado: el Service no debería tener que saber cómo se almacenan permanentemente los productos. Más adelante necesitaremos separar también esa responsabilidad.

Micro-entrega de documentación

Ticket de salida: “Si mañana cambiamos Swing por otra interfaz, ¿qué parte de ProductoService debería seguir funcionando sin cambios? ¿Por qué?”

Puente hacia lo que viene: hoy el Service guarda en un ArrayList. Cuando necesitemos persistencia, esa tarea dejará de ser responsabilidad del Service y aparecerá una nueva pieza: el acceso a datos.

Material en curso