viernes, 21 de agosto de 2009

Diseño de interfaces gráficas en Java, Parte 5: Completando el Modelo


Prepárense para construir el Modelo

De regreso, bien, en este punto ya debe estar corriendo el servidor de base de datos PostgreSQL y la base de datos con su respectiva tabla debe estar lista y ésta última debe contar con algunos registros para la prueba. También los programas en Java de nuestro proyecto deben contar con la referencia al archivo JAR que descargamos e instalamos en el artículo anterior. Ahora solo queda concluir con la construcción del Modelo programando las capas de negocio y de acceso a datos de la aplicación.

Una vez más, retomamos el mismo proyecto que hemos ido construyendo en esta serie de post sobre interfaces gráficas en Java.

Primero, programaremos una clase para crear objetos de datos durante la ejecución del programa, es decir objetos cuyo único propósito sea transportar información a través de las capas de la aplicación. En este caso la clase se llamará InfoUsuario, y la utilizaremos para que sus objetos almacenen los datos extraídos de la tabla usuario de la base de datos:

InfoUsuario.java
/**
* @Desc Clase utilizada como una estructura de datos para alamacenar la información de los registros que
* consultemos de la base de datos
* @author Beto González
*/
public class InfoUsuario {
    public String nombre;
    public String correoElectronico;
    public String domicilio;
    public String telefono;
    public int edad;
}


Después crearemos una clase para manejar el acceso a la base de datos desde nuestra aplicación, incluyendo las consultas a la tabla requerida. El código fuente es el siguiente:

AccesoADatos.java
import java.sql.*;
import java.util.*;
/**
* @Desc Clase utilizada para acceder a la base de datos y realizar consultas sobre sus tablas
* @author Beto González
*/
public class AccesoADatos {
    private String usuario;
    private String contraseña;
    private String url;
    private Connection conexion;
    private Vector<InfoUsuario> resultado; //Vector en el que se almacenarán la lista de registros que resulten en una cosulta, con el <InfoUsuario> se especifica que cada uno de sus elementos será un objeto de la clase InfoUsuario
    private String mensaje;
    AccesoADatos(String url, String usuario, String contraseña)     {
        this.usuario = usuario;
        this.contraseña = contraseña;
        this.url = url;
        this.resultado = new Vector<InfoUsuario>();
        this.mensaje = "";
    }
    /**
     * @Desc
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean conecta() {
        boolean estado = true;
        Properties props = new Properties();
        props.setProperty("user",usuario);
        props.setProperty("password",contraseña);
        try{
            conexion = DriverManager.getConnection(url, props);
        }catch(SQLException e){
            estado = false;
            mensaje = e.toString();
        }
        return estado;
    }
    /**
     * @Desc Método que ejecuta una consulta SQL sobre la tabla usuario
     * @param condicion Condición para la consulta SQL
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean consultaUsuario(String condicion) {
        boolean estado = true;
        if(!condicion.isEmpty()) {
            try{
                PreparedStatement st = conexion.prepareStatement("SELECT * FROM usuario WHERE " + condicion);
                ResultSet rs = st.executeQuery();
                InfoUsuario iUsuario = new InfoUsuario(); //Objeto para almacenar la información de cada registro de la tabla
                while (rs.next()) {
                    iUsuario.correoElectronico = rs.getString("correoElectronico");
                    iUsuario.nombre = rs.getString("nombre");
                    iUsuario.domicilio = rs.getString("domicilio");
                    iUsuario.telefono = rs.getString("telefono");
                    iUsuario.edad = rs.getInt("edad");
                    resultado.add(iUsuario);
                }
                rs.close();
                st.close();
            }catch(SQLException e){
                estado = false;
                mensaje = e.toString();
            }
        }
        else{
            estado = false;
            mensaje = "Consulta vacía";
        }
        return estado;
    }
    /**
     * @Desc Método que realiza la búsqueda de un usuario a partir de un correo electrónico
     * @param correo Correo electrónico buscado
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean buscaCorreo(String correo) {
        boolean estado = consultaUsuario("\"correoElectronico\" = '" + correo + "'");
        return estado;
    }
    /**
     * @Desc Método que realiza la búsqueda de un usuario a partir de un nombre
     * @param correo Correo electrónico buscado
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean buscaNombre(String nombre) {
        boolean estado = consultaUsuario("nombre LIKE '%" + nombre + "%'");
        return estado;
    }
    /**
     * @Desc Devuelve un elemento específicado que se encuentre dentro del Vector de resultados
     * @param indice Indice del elemento dentro del vector
     * @return
     */
    public InfoUsuario regresaInfoUsuario(int indice) {
        if(resultado.size() > 0)
            return resultado.get(indice);
        else
            return null;
    }
    /**
     * @Desc Devuelve el último mensaje de erro generado por el controlador de la base de datos
     * @return Mensaje devuelto por el controlador de la base de datos
     */
    public String regresaMensaje() {
        return mensaje;
    }
    /**
     * @Desc Devuelve el número de elementos del Vector de resultados
     * @return Número de elementos
     */
    public int regresaNumDeRegistros() {
        return resultado.size();
    }
    /**
     * @Desc Método que cierra la conexión con el sistema manejador de base de datos
     */
    public void cierraConexion() {
        try{
            conexion.close();
        }catch(SQLException e){}
    }
}


Esta clase tiene como propósito abstraer al resto de la aplicación de los detalles del acceso a la base de datos, en este caso a través del SMBD PostgreSQL. Como vemos, se incluyen métodos para abrir y cerrar la conexión con el servidor, un método llamado consultaUsuario(), para ejecutar consultas sobre la tabla usuario a partir de la condición especificada, almacenando la información de los registros resultantes en la propiedad resultado, así como el método regresaInfoUsuario() que precisamente extrae la información de los registros almacenados en esta propiedad. El objeto resultado pertenece a la clase Vector, y funciona como un arreglo en el que se almacenará una lista de objetos de la clase InfoUsuario que ya definimos previamente. De forma que a cada registro resultante le corresponderá un nodo en el vector.

La tercera clase que crearemos se encargará de implementar una pequeña lógica de negocio, se llamará Buscador y en ésta se recibirá la información capturada por el usuario, con la cual se decidirá qué método de la capa de acceso a datos deberá ejecutarse para llevar a cabo la búsqueda de un usuario y retornar su información a la capa de presentación.

Buscador.java
import java.awt.Component;
import javax.swing.JOptionPane;
/**
* @Desc Clase que implementa las busquedas de información en el sistema a través de un objeto de la clase AccesoADatos
* @author Beto González
*/
public class Buscador {
    /**
     * @Desc Método que busca la información de un usuario a través de un objeto de la clase AccesoADatos
     * @param correoElectronico Correo electrónico del usuario
     * @param nombre Nombre del usuario
     * @return Un objeto de la clase InfoUsuario, que contendrá la información del usuario solicitado
     */
    public InfoUsuario buscaUsuario(String correoElectronico, String nombre) {
        InfoUsuario info = new InfoUsuario();
        AccesoADatos a = new AccesoADatos("jdbc:postgresql://localhost/pruebas","postgres","myhouse6");
        boolean estado = true;
        if(a.conecta()) {
            if(!nombre.isEmpty()) {
                if((estado = a.buscaNombre(nombre)) && a.regresaNumDeRegistros() > 0)
                    info = a.regresaInfoUsuario(0);
            }
            else if(!correoElectronico.isEmpty()) {
                if((estado = a.buscaCorreo(correoElectronico)) && a.regresaNumDeRegistros() > 0)
                    info = a.regresaInfoUsuario(0);
            }
            a.cierraConexion();
        }
        if(!estado) {
            JOptionPane.showMessageDialog((Component)null,"Error de acceso a la base de datos " + a.regresaMensaje(),"Error",JOptionPane.OK_OPTION);
        }
        if(a.regresaNumDeRegistros() == 0) {
            JOptionPane.showMessageDialog((Component)null,"No se encontraron registros con los datos capturados","Alerta",JOptionPane.WARNING_MESSAGE);
        }
        return info;
    }
}


En el método buscaUsuario() vemos que se instancía la clase AccesoADatos y sus métodos son utilizándolos según el contenido de la información recibida como parámetro, así si se recibió un nombre de usuario válido para la búsqueda se utilizará el método buscaNombre(), en caso contrario si el correo electrónico es válido, se empleará el método buscaCorreo(). Ambos métodos devolverán el objeto de datos de la clase InfoUsuario, cuyas propiedades contienen la información del usuario consultado. Finalmente se incluye un pequeño control de los mensajes de error generados, para indicar los errores de acceso a datos y las consultas que no arrojaron resultados.

La información devuelta por el método buscaUsuario() deberá llegar hasta la capa de presentación de la aplicación, por lo que dentro de los programas Controlador.java y PanelDeDatos.java realizaremos los siguientes cambios:

PanelDeDatos.java
import javax.swing.*;
import java.awt.*;
/**
* @Desc Clase utilizada para desplegar ventanas emergentes dentro del sistema que contendrán la información establecida
* en un objeto JPanel pasado por el constructor
* @author Beto González
*
*/
public class PanelDeDatos extends JPanel {
    private JLabel lblNombre;
    private JLabel lblCorreoElectronico;
    private JLabel lblDomicilio;
    private JLabel lblEdad;
    private JLabel lblTelefono;
    //Constructor de la clase
    PanelDeDatos () {
        this.lblNombre = new JLabel("Nombre del usuario: ");
        this.lblCorreoElectronico = new JLabel("Correo electronico: ");
        this.lblDomicilio = new JLabel("Domicilio: ");
        this.lblEdad = new JLabel("Edad: ");
        this.lblTelefono = new JLabel("Telefono: ");
        this.setLayout(new GridBagLayout());
    }
    /**
     * @Desc Método que agrega al panel principal cada una de las etiquetas declaradas
     */
    public void estableceContenido(InfoUsuario datos) {
        lblNombre.setText(lblNombre.getText() + datos.nombre);
        lblCorreoElectronico.setText(lblCorreoElectronico.getText() + datos.correoElectronico);
        lblDomicilio.setText(lblDomicilio.getText() + datos.domicilio);
        lblEdad.setText(lblEdad.getText() + Integer.toString(datos.edad));
        lblTelefono.setText(lblTelefono.getText() + datos.telefono);

        GridBagConstraints delimitador = new GridBagConstraints(); //Creamos un delimitador que usaremos para especificar la ubicación de los componentes dentro del panel
        delimitador.gridx = 0;
        delimitador.gridy = 0;
        delimitador.anchor = GridBagConstraints.EAST;
        this.add(lblNombre, delimitador);
        delimitador.gridy = 1;
        this.add(lblCorreoElectronico, delimitador);
        delimitador.gridy = 2;
        this.add(lblDomicilio, delimitador);
        delimitador.gridy = 3;
        this.add(lblEdad, delimitador);
        delimitador.gridy = 4;
        this.add(lblTelefono, delimitador);
    }
}


En la clase PanelDeDatos adecuamos el método estableceContenido() para que reciba un objeto de la clase InfoUsuario y actualice el contenido de sus etiquetas con los datos de éste.

Controlador.java
import javax.swing.JFrame;
import java.awt.event.*;
/**
* @Desc Clase que maneja los eventos realizados en la Vista implementando una serie de interfaces nativas del lenguaje
* @author Beto González
*
*/
public class Controlador implements ActionListener, MouseMotionListener {
    public Ventana ventana;
    Controlador(Ventana ventana) {
        this.ventana = ventana;
    }
    /**
     * @Desc método perteneciente a la interface ActionListener que es implementado para controlar las acciones sobre los botones
     * ya que se ejecutará si se hace clic sobre el botón que este atendiendo
     */
    public void actionPerformed(ActionEvent ie) {
        if(ie.getActionCommand() == "Buscar usuario") { //Validación para asegurarse que la acción fue realizada sobre el botón Buscar
            ventana.panelPrincipal.lblMensaje.setText("Has hecho clic en el bóton de búsqueda");
            //Desplegamos una ventana emergente que contendrá la información de un panel de la clase PanelDeDatos
            PanelDeDatos panel = new PanelDeDatos();
            Buscador buscador = new Buscador();
            InfoUsuario datos = buscador.buscaUsuario(ventana.panelPrincipal.txtCampo2.getText().trim(), ventana.panelPrincipal.txtCampo1.getText().trim()); //Buscamos la información del usuario según los datos capturados
            panel.estableceContenido(datos);
//Pasamos la información consultada al panel de datos
            VentanaEmergente emergente = new VentanaEmergente("Información del usuario", panel);
            emergente.setBounds(180, 180, 300, 250);
            emergente.setVisible(true);
            emergente.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            emergente.addWindowListener(new WindowAdapter()
            {
                public void WindowCloser(WindowEvent e)                 {
                    System.exit(0);
                }
            });
        }
    }
     /**
     * @Desc Método ejecutado cuando el usuario arrastra el ratón presionando el botón de acción
     */
    public void mouseDragged(MouseEvent e) {
        ventana.panelPrincipal.lblMensaje.setText("Ratón arrastrado");
    }
    /**
     * @Desc Método ejecutado cuando el usuario solo arrastra el ratón
     */
    public void mouseMoved(MouseEvent e) {
        ventana.panelPrincipal.lblMensaje.setText("Posición del puntero X:"
         + Integer.toString(e.getX()) + " Y:"
         + Integer.toString(e.getY()));
    }
}


En la clase Controlador modificamos el método actionPerformed() para que se incluya un objeto de la clase Buscador que proporcione la información del usuario al objeto panel.

Ahora solo queda probar todos estos cambios, el resultado al ejecutar la aplicación deberá verse al capturar un nombre un un correo en el formulario y presionar el botón "Buscar", tal y como se muestra en la siguiente imagen:


En este caso, buscamos un usuario con nombre María. Debido a que en la base de datos existe una registro con nombre 'María Rodríguez' la aplicación nos arrojó un resultado satisfactorio, gracias a que en el nivel de acceso a datos la búsqueda se realiza utilizando un LIKE '%%', aunque claro que el usuario será ajeno a todos esos detalles.

En caso de buscar algún nombre inexistente, el sistema nos debe desplegar un mensaje como el siguiente:


También pueden probar la búsqueda por correo electrónico, en donde la coincidencia con la información almacenada sí deberá ser exacta.

Claro que a este programa se le pueden hacer múltiples mejoras, como por ejemplo en la búsqueda por nombre desplegar una lista con la información de todos los usuarios, en lugar del primer usuario consultado, sin embargo el ejemplo cumple con el propósito de mostrar una forma para separar nuestras aplicaciones en una Vista, un Controlador y un Modelo, así como trabajar con capas verticalmente, yendo desde la base de datos hasta la presentación. Un aspecto que me agrada mucho de trabajar con objetos y que aquí se ve reflejado, es el encapsulamiento que proporcionan, es decir que los detalles de las tareas realizadas por una capa de más bajo nivel, quedan fuera del conocimiento de las capas superiores, a estas solo les interesarán los datos o mensajes que las capas de abajo arrojen al realizar sus tareas.

Con este último post es que finalizamos con el pequeño proyecto que fuimos construyendo a partir de la parte 2 de la programación de interfaces gráficas en Java. Ya vimos algunos detalles interesantes sobre la presentación de los componentes y el manejo de eventos, complementándolos con el acceso a una base de datos. En los siguientes post veremos algunos otros componentes de la interfaz gráfica, y cómo utilizarlos para crear una nueva aplicación para la manipulación de imágenes, que será el proyecto con el que concluiremos con esta serie de artículos sobre Java.

Hasta la próxima.

2 comentarios:

WiL dijo...

Que buenas entradas, me ha aclarado muchas cosas, y me van a servir mucho para un trabajo que estoy realizando con manejo de usuarios en postgres tambien, ademas el ejemplo esta muy claro y completo, me gusto :D, aunque tengo una duda.

En otras parte usan el patron observer para notificar a la vista que el modelo ha cambiado de estado e incluso algunos usan el controlador para el paso de mensajes desde el modelo a la vista, ¿Que criterio se usa para elegir esta o la otra implementación?, y si puedes darme un par de ventajas y desventajas te lo agradeceria aún mas

Felicitaciones por tu blog

Anónimo dijo...

Hola, muchas gracias por tus comentarios, me alegra mucho que leas este blog :D En fechas recientes lo he tenido descuidado, pero aún tengo la esperanza de poder subir más temas, relacionadas con nuevas cosas que he venido aprendiendo.

Sobre tu pregunta, la gran ventaja que noto en el patrón Observer (http://mgaravaglia.com.ar/blog/index.php/2008/03/30/patterns-observer/), es que te puedes evitar la rigidez de la comunicación vertical entre objetos; si te fijas en este ejemplo, para mandar un mensaje de un error o resultado desde la clase AccesoADatos, hasta la interfaz gráfica, tiene que hacer un par de escalas. En mucho casos es más práctico saltarte este camino largo y "tomar un atajo", por así decirlo, ya que gracias a esto el código se simplifica y el mantenimiento se hace más fácil.

En otras situaciones, debido a los requerimientos particulares del sistema, puede resultar necesario respetar el esquema vertical, por ejemplo si deseamos que en la capa de negocio procese los mensajes o datos de la capa de acceso a datos antes de pasarlo a la interfaz gráfica.

Ese patrón tiene sus ventajas sin duda, al final todo dependerá del diseño particular que tus requerimientos demanden.

Publicar un comentario