Programación Avanzada 2.º BT TI · DGETP / UTU
REFERENCIA INTEGRADA · JAVA / JDBC / SWING / MAVEN

Tienda de Cómics: proyecto completo y comentado

Versión ordenada del proyecto Producto, con separación por clases y paquetes para que se vea con claridad dónde empieza y dónde termina cada parte del sistema.

1. Qué vamos a ver aquí

Esta página reúne la versión de referencia del proyecto Tienda de Cómics. La idea no es solo mostrar el código, sino también explicar la función de cada clase y la relación entre las capas.

Decisiones que conservamos: la vista construye la interfaz, el controlador coordina eventos, el servicio valida reglas del negocio y el DAO trabaja con SQL mediante JDBC. ProductoDAO implementa CrudDAO<Producto>.
Vistacaptura datos y muestra resultados.
Controllerrecibe eventos y coordina acciones.
Servicevalida reglas del negocio.
DAOejecuta operaciones SQL.
Configabre conexiones JDBC.
Mainconecta todos los objetos.
Infografía de la estructura Maven y paquetes del proyecto
Tomamos esta organización como base y la ajustamos a nuestra versión final: model, view, controller, service, dao, config y exceptions.

2. Organización final del proyecto Maven

Esta es la estructura que conviene mostrar a los estudiantes en la versión integrada final.

uy.edu.utu.programacion ├── Main.java ├── model │ └── Producto.java ├── view │ └── Producto_Vista.java ├── controller │ └── ProductoController.java ├── service │ └── ProductoService.java ├── dao │ ├── CrudDAO.java │ └── ProductoDAO.java ├── config │ └── Conexion.java └── exceptions ├── CampoVacioException.java └── NumeroNegativoException.java

model

Representa los datos del sistema. Aquí vive Producto.

view

Construye la interfaz gráfica y expone métodos para que el controlador trabaje con ella.

controller

Registra listeners, convierte datos y coordina las operaciones entre vista y servicio.

service

Aplica las reglas de negocio y usa el contrato DAO, no la implementación concreta.

dao

Define el contrato CRUD y la clase que lo implementa con SQL.

config / exceptions

Centralizan la conexión JDBC y las excepciones de negocio.

3. Recorrido de una operación

Cuando el usuario pulsa Agregar, la información sigue este recorrido:

Producto_Vista→ ProductoController→ ProductoService→ CrudDAO<Producto>→ ProductoDAO→ Conexion→ MySQL
model

Producto.java

Entidad del sistema. Representa un producto de la tienda sin conocer ni JDBC ni MySQL.

JAVA · model
package uy.edu.utu.programacion.model;

public class Producto {

    private long codigo;
    private String nombre;
    private double precio;
    private int stock;

    public Producto(long codigo, String nombre,
                    double precio, int stock) {
        this.codigo = codigo;
        this.nombre = nombre;
        this.precio = precio;
        this.stock = stock;
    }

    // getters
    public long getCodigo() { return codigo; }
    public String getNombre() { return nombre; }
    public double getPrecio() { return precio; }
    public int getStock() { return stock; }

    // setters de los datos modificables
    public void setNombre(String nombre) { this.nombre = nombre; }
    public void setPrecio(double precio) { this.precio = precio; }
    public void setStock(int stock) { this.stock = stock; }

    @Override
    public String toString() {
        return codigo + " - " + nombre
                + " - $" + precio
                + " - Stock: " + stock;
    }
}

Idea clave: el código identifica al producto. Por eso lo tratamos como un dato estable y no ofrecemos un setter para cambiarlo.

config

Conexion.java

Centraliza la apertura de la conexión JDBC con MySQL.

JAVA · config
package uy.edu.utu.programacion.config;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Conexion {

    private Connection conexion;

    private static final String URL =
            "jdbc:mysql://localhost:3306/tiendacomics";
    private static final String USUARIO = "usuario_tienda";
    private static final String CLAVE = "clave_de_ejemplo";

    public Conexion() throws SQLException {
        conexion = DriverManager.getConnection(URL, USUARIO, CLAVE);
    }

    public Connection getConexion() {
        return conexion;
    }
}

Ojo: en este diseño, cada operación del DAO crea su propia conexión. Es una solución sencilla y adecuada para nuestra etapa de aprendizaje.

dao

CrudDAO.java

Contrato genérico con las operaciones CRUD que el servicio necesita usar.

JAVA · dao
package uy.edu.utu.programacion.dao;

import java.sql.SQLException;
import java.util.List;

public interface CrudDAO<T> {

    void guardar(T objeto) throws SQLException;
    void modificar(T objeto) throws SQLException;
    void eliminar(long codigo) throws SQLException;
    List<T> listar() throws SQLException;
}

Cuando escribimos CrudDAO<Producto>, el servicio sabe qué operaciones existen, pero no necesita saber cómo se implementan.

dao

ProductoDAO.java

Implementación concreta del contrato. Aquí aparece SQL, JDBC y el trabajo con ResultSet.

JAVA · dao
package uy.edu.utu.programacion.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import uy.edu.utu.programacion.config.Conexion;
import uy.edu.utu.programacion.model.Producto;

public class ProductoDAO implements CrudDAO<Producto> {

    @Override
    public void guardar(Producto producto) throws SQLException {
        String sql = "INSERT INTO producto (codigo, nombre, precio, stock) VALUES (?, ?, ?, ?)";

        try (Connection conexion = new Conexion().getConexion();
             PreparedStatement sentencia = conexion.prepareStatement(sql)) {

            sentencia.setLong(1, producto.getCodigo());
            sentencia.setString(2, producto.getNombre());
            sentencia.setDouble(3, producto.getPrecio());
            sentencia.setInt(4, producto.getStock());
            sentencia.executeUpdate();
        }
    }

    @Override
    public void modificar(Producto producto) throws SQLException {
        String sql = "UPDATE producto SET nombre = ?, precio = ?, stock = ? WHERE codigo = ?";
        try (Connection conexion = new Conexion().getConexion();
             PreparedStatement sentencia = conexion.prepareStatement(sql)) {
            sentencia.setString(1, producto.getNombre());
            sentencia.setDouble(2, producto.getPrecio());
            sentencia.setInt(3, producto.getStock());
            sentencia.setLong(4, producto.getCodigo());
            sentencia.executeUpdate();
        }
    }

    @Override
    public void eliminar(long codigo) throws SQLException {
        String sql = "DELETE FROM producto WHERE codigo = ?";
        try (Connection conexion = new Conexion().getConexion();
             PreparedStatement sentencia = conexion.prepareStatement(sql)) {
            sentencia.setLong(1, codigo);
            sentencia.executeUpdate();
        }
    }

    @Override
    public List<Producto> listar() throws SQLException {
        List<Producto> productos = new ArrayList<>();
        String sql = "SELECT codigo, nombre, precio, stock FROM producto ORDER BY codigo";

        try (Connection conexion = new Conexion().getConexion();
             PreparedStatement sentencia = conexion.prepareStatement(sql);
             ResultSet resultado = sentencia.executeQuery()) {

            while (resultado.next()) {
                long codigo = resultado.getLong("codigo");
                String nombre = resultado.getString("nombre");
                double precio = resultado.getDouble("precio");
                int stock = resultado.getInt("stock");
                productos.add(new Producto(codigo, nombre, precio, stock));
            }
        }
        return productos;
    }
}
Dos ideas importantes: implements CrudDAO<Producto> muestra el contrato que cumple la clase, y el try-with-resources se encarga de cerrar conexión, sentencia y resultados.
exceptions

CampoVacioException y NumeroNegativoException

Expresan errores del negocio, no errores de JDBC.

JAVA · exceptions
package uy.edu.utu.programacion.exceptions;

public class CampoVacioException extends Exception {
    public CampoVacioException(String mensaje) {
        super(mensaje);
    }
}
JAVA · exceptions
package uy.edu.utu.programacion.exceptions;

public class NumeroNegativoException extends Exception {
    public NumeroNegativoException(String mensaje) {
        super(mensaje);
    }
}
service

ProductoService.java

Aplica las reglas del negocio y aprovecha el polimorfismo al trabajar con la interfaz CrudDAO<Producto>.

JAVA · service
package uy.edu.utu.programacion.service;

import java.sql.SQLException;
import java.util.List;

import uy.edu.utu.programacion.dao.CrudDAO;
import uy.edu.utu.programacion.model.Producto;
import uy.edu.utu.programacion.exceptions.CampoVacioException;
import uy.edu.utu.programacion.exceptions.NumeroNegativoException;

public class ProductoService {

    private CrudDAO<Producto> productoDAO;

    public ProductoService(CrudDAO<Producto> productoDAO) {
        this.productoDAO = productoDAO;
    }

    public void agregar(Producto producto)
            throws CampoVacioException, NumeroNegativoException, SQLException {
        validar(producto);
        productoDAO.guardar(producto);
    }

    public void modificar(Producto producto)
            throws CampoVacioException, NumeroNegativoException, SQLException {
        validar(producto);
        productoDAO.modificar(producto);
    }

    public void eliminar(long codigo) throws SQLException {
        productoDAO.eliminar(codigo);
    }

    public List<Producto> listar() throws SQLException {
        return productoDAO.listar();
    }

    private void validar(Producto producto)
            throws CampoVacioException, NumeroNegativoException {

        if (producto.getNombre() == null || producto.getNombre().isBlank()) {
            throw new CampoVacioException("El nombre del producto no puede estar vacío");
        }
        if (!Double.isFinite(producto.getPrecio()) || producto.getPrecio() < 0) {
            throw new NumeroNegativoException("El precio debe ser un número válido y no negativo");
        }
        if (producto.getStock() < 0) {
            throw new NumeroNegativoException("El stock no puede ser negativo");
        }
        if (producto.getCodigo() <= 0) {
            throw new NumeroNegativoException("El código debe ser mayor que cero");
        }
    }
}

La clave didáctica: el servicio declara su atributo como CrudDAO<Producto> y no como ProductoDAO. Así conoce el contrato, no la implementación concreta.

view

Producto_Vista.java

Construye la interfaz y ofrece métodos de apoyo para que el controlador capture datos, limpie campos y actualice la tabla.

JAVA · view
package uy.edu.utu.programacion.view;

// imports abreviados para facilitar la lectura
import java.awt.*;
import java.awt.event.ActionListener;
import java.util.List;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;

import uy.edu.utu.programacion.model.Producto;

public class Producto_Vista extends JFrame {

    private JTextField txtCodigo, txtNombre, txtPrecio, txtStock;
    private JButton btnAgregar, btnModificar, btnEliminar, btnCancelar;
    private JTable tabla;
    private DefaultTableModel modeloTabla;

    public Producto_Vista() {
        crearVentana();
    }

    private void crearVentana() {
        setTitle("Tienda de Cómics");
        setSize(750, 500);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel panelPrincipal = new JPanel(new BorderLayout(10, 10));
        JPanel panelFormulario = new JPanel(new BorderLayout(5, 5));
        panelFormulario.add(new JLabel("Administración de productos"), BorderLayout.NORTH);

        JPanel panelCampos = new JPanel(new GridLayout(4, 2, 5, 5));
        txtCodigo = new JTextField();
        txtNombre = new JTextField();
        txtPrecio = new JTextField();
        txtStock = new JTextField();

        panelCampos.add(new JLabel("Código:")); panelCampos.add(txtCodigo);
        panelCampos.add(new JLabel("Nombre:")); panelCampos.add(txtNombre);
        panelCampos.add(new JLabel("Precio:")); panelCampos.add(txtPrecio);
        panelCampos.add(new JLabel("Stock:")); panelCampos.add(txtStock);

        panelFormulario.add(panelCampos, BorderLayout.CENTER);

        JPanel panelBotones = new JPanel(new FlowLayout());
        btnAgregar = new JButton("Agregar");
        btnModificar = new JButton("Modificar");
        btnEliminar = new JButton("Eliminar");
        btnCancelar = new JButton("Cancelar");
        panelBotones.add(btnAgregar); panelBotones.add(btnModificar);
        panelBotones.add(btnEliminar); panelBotones.add(btnCancelar);
        panelFormulario.add(panelBotones, BorderLayout.SOUTH);
        panelPrincipal.add(panelFormulario, BorderLayout.NORTH);

        modeloTabla = new DefaultTableModel(new Object[]{"Código", "Nombre", "Precio", "Stock"}, 0) {
            @Override
            public boolean isCellEditable(int fila, int columna) { return false; }
        };

        tabla = new JTable(modeloTabla);
        panelPrincipal.add(new JScrollPane(tabla), BorderLayout.CENTER);
        setContentPane(panelPrincipal);
    }

    // getters, listeners, limpiarCampos(), actualizarTabla() y cargarFilaSeleccionada()
}

Para no volver interminable la lectura, aquí mostramos la parte estructural de la clase. Los métodos auxiliares de la vista se conservan en el proyecto y cumplen la misma lógica que trabajamos en los SCORM de Vista.

controller

ProductoController.java

Conecta las acciones de la vista con el servicio y gestiona la conversión de texto a números.

JAVA · controller
package uy.edu.utu.programacion.controller;

import java.sql.SQLException;
import javax.swing.JOptionPane;

import uy.edu.utu.programacion.model.Producto;
import uy.edu.utu.programacion.service.ProductoService;
import uy.edu.utu.programacion.view.Producto_Vista;
import uy.edu.utu.programacion.exceptions.CampoVacioException;
import uy.edu.utu.programacion.exceptions.NumeroNegativoException;

public class ProductoController {

    private Producto_Vista vista;
    private ProductoService servicio;

    public ProductoController(Producto_Vista vista, ProductoService servicio) {
        this.vista = vista;
        this.servicio = servicio;
        agregar(); modificar(); eliminar(); cancelar(); seleccionarProducto(); actualizarTabla();
    }

    private Producto leerProducto() {
        long codigo = Long.parseLong(vista.getTxtCodigo().trim());
        String nombre = vista.getTxtNombre();
        double precio = Double.parseDouble(vista.getTxtPrecio().trim());
        int stock = Integer.parseInt(vista.getTxtStock().trim());
        return new Producto(codigo, nombre, precio, stock);
    }

    // cada listener captura datos, llama al servicio y actualiza la tabla
    public void agregar() {
        vista.listenerAgregar(e -> {
            try {
                Producto producto = leerProducto();
                servicio.agregar(producto);
                actualizarTabla();
                vista.limpiarCampos();
                JOptionPane.showMessageDialog(vista, "Producto agregado correctamente");
            } catch (NumberFormatException ex) {
                mostrarError("Código, precio o stock incorrectos");
            } catch (CampoVacioException | NumeroNegativoException ex) {
                mostrarError(ex.getMessage());
            } catch (SQLException ex) {
                mostrarError("Error de base de datos: " + ex.getMessage());
            }
        });
    }

    // también existen modificar(), eliminar(), cancelar(), seleccionarProducto() y actualizarTabla()
    private void mostrarError(String mensaje) {
        JOptionPane.showMessageDialog(vista, mensaje, "Error", JOptionPane.ERROR_MESSAGE);
    }
}

El controlador no escribe SQL y no valida reglas de negocio profundas: coordina y distribuye responsabilidades.

raíz del proyecto

Main.java

Construye los objetos principales y establece las relaciones entre ellos.

JAVA · main
package uy.edu.utu.programacion;

import javax.swing.SwingUtilities;
import com.formdev.flatlaf.FlatLightLaf;

import uy.edu.utu.programacion.dao.CrudDAO;
import uy.edu.utu.programacion.dao.ProductoDAO;
import uy.edu.utu.programacion.model.Producto;
import uy.edu.utu.programacion.service.ProductoService;
import uy.edu.utu.programacion.controller.ProductoController;
import uy.edu.utu.programacion.view.Producto_Vista;

public class Main {
    public static void main(String[] args) {
        FlatLightLaf.setup();

        SwingUtilities.invokeLater(() -> {
            Producto_Vista vista = new Producto_Vista();
            CrudDAO<Producto> dao = new ProductoDAO();
            ProductoService servicio = new ProductoService(dao);
            new ProductoController(vista, servicio);
            vista.setVisible(true);
        });
    }
}
Acá aparece el polimorfismo aplicado: la variable dao se declara con la interfaz CrudDAO<Producto>, aunque el objeto concreto creado es new ProductoDAO().
mysql

Script básico de base de datos

Tabla mínima necesaria para que el DAO funcione.

SQL
CREATE DATABASE IF NOT EXISTS tiendacomics;

USE tiendacomics;

CREATE TABLE IF NOT EXISTS producto (
    codigo BIGINT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL
);

En este curso mantenemos double en el modelo porque acompaña el trabajo previo del grupo. En contextos comerciales reales conviene usar BigDecimal para representar importes monetarios.

4. Qué resuelve esta versión y qué mejoraría después

Más adelante, podríamos mejorar tres cosas: informar cuando un UPDATE o DELETE no afectó filas, crear excepciones propias de persistencia y trabajar con transacciones que agrupen varias operaciones.

← Índice del curso