Archivo de la etiqueta: Modelo-Vista-Controlador

Ejemplo de MVC y diseño por capas: la Vista

En la entrada anterior, vimos una implementación de nuestra capa Controlador utilizando un sencillo esquema de paso de mensajes hacia la Vista y delegación de tareas hacia el Modelo. En esta nueva entrada veremos un ejemplo de implementación de la Vista utilizando Swing.

Antes de comenzar, es bueno recordar que cuando modelamos esta capa vimos que debe ofrecer un contrato para ocuparse de varias tareas: interactuar y obtener información desde el usuario, procesar acciones y ser notificada acerca de cómo ha ido dicho procesamiento, a fin de poder actualizarse en consecuencia.

Quizás parezca un poco complejo al principio, pero de hecho nuestro diseño no es accidental. Así como vimos la utilidad de definir nuestro Modelo siguiendo el patrón DAO, en este caso diseñamos la Vista de acuerdo a una característica muy común en las interfaces de usuario: son basadas en Eventos.

La mayoría de los frameworks para interfaces de usuario (Swing o JavaFX en aplicaciones de escritorio; Java ServerFaces o Vaadin en aplicaciones Web) han sido diseñadas bajo una arquitectura Event-driven o arquitectura conducida por eventos. Por ejemplo, cuando el usuario interactúa con algún componente gráfico (un botón, una lista desplegable, etc.) se genera y dispara un evento con la información básica para determinar qué es lo que ha ocurrido. A partir de este evento, nosotros como desarrolladores debemos decidir cómo procesarlo y debemos mostrarle al usuario un feedback de qué tarea se está llevando a cabo o cómo ha ido dicho procesamiento. Veremos que en este esquema, nuestro diseño encaja perfectamente.

Nuestra vista en java: package example.mvc.view

Ha llegado el momento de escribir nuestra Vista en Java, para lo cual debemos recordar que en nuestro diseño sólo tenemos una interfaz IFabricantesView, la cual define todo el comportamiento necesario para implementar esta capa. Además de su implementación concreta, en este package incluiremos una clase reutilizable destinada a capturar los datos de la entidad Fabricante, ya sea para crear uno nuevo o para modificar los datos de uno existente, y también incluiremos nuestra clase GenericDomainTableModel la cual se desarrolló y ejemplificó en entradas anteriores, y que servirá de TableModel para presentar nuestros objetos Fabricante en un componente JTable.

Package example.mvc.view

GenericDomainTableModel

Lo dicho en la sección anterior, esta clase abstracta nos sirve de TableModel base para poder utilizar junto con el componente JTable. El desarrollo completo de esta clase se encuentra disponible en la serie Creando un TableModel reutilizable en Swing. Para quienes sólo quieran el código fuente, se encuentra al final de la parte II de dicha serie.

DatosFabricante

Esta clase de utilidad está pensada para delegar la responsabilidad de capturar los datos de una entidad Fabricante (sólo nombre y fecha de fundación, recordemos que el identificador único es auto-generado e inmodificable) de un modo transparente, y que nos evite la duplicación de código  de acuerdo con el principio DRY (Don’t Repeat Yourself). Tenemos entonces una clase que genera un panel con un text field para capturar el nombre del fabricante y un spinner para ingresar su fecha de fundación.

package example.mvc.view;

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.Date;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerDateModel;
import javax.swing.text.DateFormatter;

/**
 * @author Delcio Amarillo
 */
public class DatosFabricante {
    
    private JTextField nombreTextField;
    private JSpinner fechaFundacionSpinner;
    private JPanel panel;
    
    public DatosFabricante(String nombre, Date fechaFundacion) {
        initComponents(nombre, fechaFundacion);
    }
    
    private void initComponents(String nombre, Date fechaFundacion) {
        nombreTextField = new JTextField(nombre, 30);
        
        Date fecha = fechaFundacion != null ? new Date(fechaFundacion.getTime())
                                            : new Date();
        
        fechaFundacionSpinner = new JSpinner(new SpinnerDateModel());
        JSpinner.DateEditor editor = new JSpinner.DateEditor(fechaFundacionSpinner, "dd/MM/yyyy");

        DateFormatter formatter = (DateFormatter)editor.getTextField().getFormatter();
        formatter.setAllowsInvalid(false);
        formatter.setOverwriteMode(true);
        
        fechaFundacionSpinner.setEditor(editor);
        fechaFundacionSpinner.setValue(fecha);

        panel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(4, 4, 4, 4);
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.gridx = 0;
        gbc.gridy = 0;

        panel.add(new JLabel("Nombre:"), gbc);

        gbc.gridx = 1;
        gbc.weightx = 1;
        panel.add(nombreTextField, gbc);

        gbc.gridx = 0;
        gbc.weightx = 0;
        gbc.gridy = 1;
        panel.add(new JLabel("Fecha de fundación:"), gbc);

        gbc.gridx = 1;
        gbc.weightx = 1;
        panel.add(fechaFundacionSpinner, gbc);
    }
    
    public Date getFechaFundacion() {
        Date fechaFundacion = (Date)fechaFundacionSpinner.getValue();
        return fechaFundacion != null ? new Date(fechaFundacion.getTime()) 
                                      : fechaFundacion;
    }
    
    public String getNombreFabricante() {
        return nombreTextField.getText();
    }
    
    public JComponent getUIComponent() {
        return panel;
    }
}

IFabricantesView

He aquí el código de nuestra interfaz de la Vista. Nótese que por contrato es necesario mantener una referencia al Controlador al cual se le delegarán las tareas al momento de procesar las acciones. Veremos esto en la implementación de la interfaz.

package example.mvc.view;

import example.mvc.controller.IFabricantesController;
import example.mvc.model.Fabricante;
import java.util.List;

/**
 * @author Delcio Amarillo
 */
public interface IFabricantesView {
    
    public void doAgregarFabricante();
    
    public Fabricante getNuevoFabricante();
    
    public void notificarFabricanteAgregado(Boolean resultado);
    
    public void doModificarFabricante();
    
    public Fabricante getFabricanteAModificar();
    
    public void notificarFabricanteModificado(Boolean resultado);
    
    public void doEliminarFabricantes();
    
    public List<Fabricante> getFabricantesAEliminar();
    
    public void notificarFabricantesEliminados(Boolean resultado);
    
    public void doConsultarFabricantes();
    
    public void setListaFabricantes(List<Fabricante> fabricantes);
    
    public void setFabricantesController(IFabricantesController controller);
}

FabricantesViewSwing

Finalmente, llegó el momento de implementar nuestra interfaz. Como es de imaginar, el código de nuestra clase concreta es bastante extenso (aproximadamente 390 líneas de código), razón por la cual iremos explicando las distintas partes por separado, para finalmente poner todo el código junto en la siguiente sección.

Comencemos presentando los atributos de nuestra clase, y cómo se inicializan los componentes Swing de la misma. Luego iremos presentando la implementación de los métodos de nuestra interfaz agrupándolos por caso de uso.

public class FabricantesViewSwing implements IFabricantesView {
    
    private IFabricantesController controller;
    private Fabricante nuevoFabricante, fabricanteAModificar;
    private final List<Fabricante> fabricantesAEliminar;
    
    private JPanel content;
    private JTable table;
    private Action agregarAction, modificarAction, eliminarAction, consultarAction;
    private GenericDomainTableModel<Fabricante> tableModel;
    
    
    public FabricantesViewSwing(IFabricantesController controller) {
        this.controller = controller;
        this.fabricantesAEliminar = new ArrayList<>();
    }
    
    @Override
    public void setFabricantesController(IFabricantesController controller) {
        this.controller = controller;
    }
    
    private void initActions() {
        agregarAction = new AbstractAction("Agregar nuevo") {
            @Override
            public void actionPerformed(ActionEvent e) {
                DatosFabricante datosFabricante = new DatosFabricante(null, null);
                JComponent component = datosFabricante.getUIComponent();
                
                int opcion = JOptionPane.showConfirmDialog ( 
                                              null
                                            , component
                                            , "Nuevo fabricante"
                                            , JOptionPane.OK_CANCEL_OPTION
                                            , JOptionPane.PLAIN_MESSAGE
                );
                
                if (opcion == JOptionPane.OK_OPTION) {
                    String nombre = datosFabricante.getNombreFabricante();
                    Date fechaFundacion = datosFabricante.getFechaFundacion();
                    nuevoFabricante = new Fabricante(nombre, fechaFundacion);
                    doAgregarFabricante();
                }
            }
        };
        
        modificarAction = new AbstractAction("Modificar") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int viewIndex = table.getSelectedRow();
                if (viewIndex > -1) {
                    int modelIndex = table.convertRowIndexToModel(viewIndex);
                    
                    fabricanteAModificar = tableModel.getDomainObject(modelIndex);
                    String nombre = fabricanteAModificar.getNombre();
                    Date fechaFundacion = fabricanteAModificar.getFechaFundacion();
                    
                    DatosFabricante datosFabricante = new DatosFabricante(nombre, fechaFundacion);
                    JComponent component = datosFabricante.getUIComponent();
                    
                    int opcion = JOptionPane.showConfirmDialog (
                                                null
                                              , component
                                              , "Modificar fabricante"
                                              , JOptionPane.OK_CANCEL_OPTION
                                              , JOptionPane.PLAIN_MESSAGE
                    );

                    if (opcion == JOptionPane.OK_OPTION) {
                        nombre = datosFabricante.getNombreFabricante();
                        fechaFundacion = datosFabricante.getFechaFundacion();
                        fabricanteAModificar.setNombre(nombre);
                        fabricanteAModificar.setFechaFundacion(fechaFundacion);
                        doModificarFabricante();
                    }
                }
            }
        };
        
        eliminarAction = new AbstractAction("Eliminar") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int[] viewIndexes = table.getSelectedRows();
                if (viewIndexes.length > 0) {
                    fabricantesAEliminar.clear();                    
                    for (int viewIndex : viewIndexes) {
                        int modelIndex = table.convertRowIndexToModel(viewIndex);
                        Fabricante fabricante = tableModel.getDomainObject(modelIndex);
                        fabricantesAEliminar.add(fabricante);
                    }                    
                    doEliminarFabricantes();
                }
            }
        };
        
        consultarAction = new AbstractAction("Actualizar tabla") {
            @Override
            public void actionPerformed(ActionEvent e) {
                doConsultarFabricantes();
            }
        };
    }
    
    private void initTableAndModel() {
        List header = Arrays.asList(new Object[] {
            "Id", "Nombre", "Fecha de fundación"
        });
        
        tableModel = new GenericDomainTableModel<Fabricante>(header) {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                switch (columnIndex) {
                    case 0: return BigInteger.class;
                    case 1: return String.class;
                    case 2: return Date.class;
                }
                throw new ArrayIndexOutOfBoundsException(columnIndex);
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                Fabricante fabricante = getDomainObject(rowIndex);
                switch (columnIndex) {
                    case 0: return fabricante.getId();
                    case 1: return fabricante.getNombre();
                    case 2: return fabricante.getFechaFundacion();
                }
                throw new ArrayIndexOutOfBoundsException(columnIndex);
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return false;
            }
            
            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex) {}
        };
        
        table = new JTable(tableModel);
        table.setAutoCreateRowSorter(true);
        doConsultarFabricantes();
    }
    
    private void initContent() {
        JScrollPane scrollPane = new JScrollPane(table);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        
        JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        buttonsPanel.add(new JButton(agregarAction));
        buttonsPanel.add(new JButton(modificarAction));
        buttonsPanel.add(new JButton(eliminarAction));
        buttonsPanel.add(new JButton(consultarAction));
        
        content = new JPanel(new BorderLayout(8, 8));
        content.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
        content.add(scrollPane, BorderLayout.CENTER);
        content.add(buttonsPanel, BorderLayout.PAGE_END);
    }
    
    private void initComponents() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                initActions();
                initTableAndModel();
                initContent();
            }
        };
        
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
    
    public JComponent getUIComponent() {
        if (content == null) {
            initComponents();
        }
        return content;
    }
    ...
}

Hasta aquí podemos ver que a nivel de Swing nuestra clase cuenta con un conjunto de acciones, una tabla y su respectivo modelo, y un panel llamado content. La inicialización de cada grupo de componentes se divide en los métodos initActions(), initTableAndModel() e initContent(), respectivamente. El método initComponents() se encarga de llamar a los tres métodos anteriores y se asegura que la creación de los componentes Swing se efectúe en el Event Dispatch Thread (EDT). Finalmente el método getUIComponent() expone el panel content hacia el exterior, de modo que podamos agregarlo a otro contenedor, ya sea otro panel o un contenedor de alto nivel (JFrame o JDialog). Por ejemplo:

IFabricantesView view = new FabricantesViewSwing(controller);
JComponent component = ((FabricantesViewSwing) view).getUIComponent();                
JFrame frame = new JFrame("ABMC Fabricantes");
frame.add(component);
...

Para que tengamos una idea, el panel debe verse más o menos así, dependiendo del Look and Feel (en mi caso Windows): un scroll pane con la tabla en el centro y un panel con los botones en la región inferior.

Panel content

Nótese también que en las acciones de agregar y modificar un Fabricante, hacemos uso de nuestra clase de utilidad DatosFabricante, mapeando los resultados en los atributos nuevoFabricante y fabricanteAModificar, respectivamente. En cambio, en la acción de eliminar los fabricantes seleccionados, tomamos los mismos del table model y los agregamos a la lista fabricantesAEliminar.

Pasemos entonces a la implementación de los métodos para cada caso de uso, enfocándonos en la interacción Usuario-Vista y Vista-Controlador.

Agregar fabricante

SD - Agregar fabricante

public class FabricantesViewSwing implements IFabricantesView {
    private IFabricantesController controller;
    private Fabricante nuevoFabricante;
    private GenericDomainTableModel<Fabricante> tableModel;
    ...
    @Override
    public void doAgregarFabricante() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                controller.agregarFabricante(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public Fabricante getNuevoFabricante() {
        return nuevoFabricante;
    }

    @Override
    public void notificarFabricanteAgregado(final Boolean resultado) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (resultado) {
                    tableModel.addRow(nuevoFabricante);
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Ha ocurrido un error al agregar un nuevo fabricante.")
                          .append("%n")
                          .append("Por favor contacte al administrador");
                    
                    String mensaje = String.format(sb.toString());
                    
                    JOptionPane.showMessageDialog (
                                    null
                                  , mensaje
                                  , "Error"
                                  , JOptionPane.WARNING_MESSAGE
                    );
                }
                nuevoFabricante = null;
            }
        };

        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
    ...
}

Aquí podemos ver que en esencia la secuencia de paso de mensajes es sencilla:

  1. El usuario clickea el botón Agregar nuevo.
  2. Mostramos un diálogo para cargar los datos del nuevo fabricante, haciendo uso de nuestra clase de utilidad DatosFabricante. Los datos se mapean en el atributo o place-holder nuevoFabricante.
  3. Llamamos al método doAgregarFabricante(), dentro del cual llamamos al método del Controlador agregarFabricante(…) y le pasamos como parámetro nuestra clase que implementa la vista.
  4. El método getNuevoFabricante() que será llamado desde el Controlador retorna el place-holder nuevoFabricante
  5. Finalmente, si todo ha ido bien, en el método notificarFabricanteAgregado(…) agregamos el nuevoFabricante a nuestro table model, y reinicializamos el place-holder.

Lo único fuera de lo común es el uso de SwingWorker y SwingUtilities.invokeLater(…). Ambos son mecanismos para manejar la concurrencia en Swing. Por ejemplo, utilizamos SwingWorker para ejecutar la llamada al controlador en  un nuevo hilo. De este modo, nuestra interfaz gráfica no quedará “congelada” esperando una respuesta. Finalmente, al agregar el nuevo fabricante al table model, debemos asegurarnos que la llamada se efectúe en el Event Dispatch Thread, por lo cual utilizamos SwingUtilities.invokeLater(…).

Modificar fabricante

SD - Modificar fabricante

public class FabricantesViewSwing implements IFabricantesView {    
    private IFabricantesController controller;
    private Fabricante fabricanteAModificar;
    private GenericDomainTableModel<Fabricante> tableModel;
    ...
    @Override
    public void doModificarFabricante() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                controller.modificarFabricante(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public Fabricante getFabricanteAModificar() {
        return fabricanteAModificar;
    }

    @Override
    public void notificarFabricanteModificado(final Boolean resultado) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (resultado) {
                    tableModel.notifyDomainObjectUpdated(fabricanteAModificar);
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Ha ocurrido un error al modificar el fabricante seleccionado.")
                          .append("%n")
                          .append("Por favor contacte al administrador");
                    
                    String mensaje = String.format(sb.toString());
                    
                    JOptionPane.showMessageDialog (
                                    null
                                  , mensaje
                                  , "Error"
                                  , JOptionPane.WARNING_MESSAGE
                    );
                }
                fabricanteAModificar = null;
            }
        };
            
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
    ...
}

En este caso de uso, el escenario es muy similar al anterior:

  1. El usuario clickea el botón Modificar.
  2. Tomamos el fabricante seleccionado del table model y se lo asignamos al atributo fabricanteAModificar.
  3. Mostramos un diálogo para modificar los datos del fabricante, haciendo uso de nuestra clase de utilidad DatosFabricante. Los datos se mapean en el atributo o place-holder fabricanteAModificar.
  4. Llamamos al método doModificarFabricante(), dentro del cual llamamos al método del Controlador modificarFabricante(…) y le pasamos como parámetro nuestra clase que implementa la vista.
  5. El método getFabricanteAModificar() que será llamado desde el Controlador retorna el place-holder fabricanteAModificar.
  6. Finalmente, si todo ha ido bien, en el método notificarFabricanteModificado(…) notificamos al table model que el fabricante fabricanteAModificar ha cambiado su estado interno, y posteriormente reinicializamos el place-holder.

Eliminar fabricantes

SD - Eliminar fabricantes

public class FabricantesViewSwing implements IFabricantesView {    
    private IFabricantesController controller;
    private final List<Fabricante> fabricantesAEliminar;
    private GenericDomainTableModel<Fabricante> tableModel;
    ...
    @Override
    public void doEliminarFabricantes() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                controller.eliminarFabricantes(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public List<Fabricante> getFabricantesAEliminar() {
        return fabricantesAEliminar;
    }

    @Override
    public void notificarFabricantesEliminados(final Boolean resultado) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (resultado) {
                    
                    for (Fabricante fabricante : fabricantesAEliminar) {
                        tableModel.deleteRow(fabricante);
                    }
                    
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Ha ocurrido un error al eliminar los fabricantes seleccionados.")
                          .append("%n")
                          .append("Por favor contacte al administrador");
                    
                    String mensaje = String.format(sb.toString());
                    
                    JOptionPane.showMessageDialog (
                                    null
                                  , mensaje
                                  , "Error"
                                  , JOptionPane.WARNING_MESSAGE
                    );
                }

                fabricantesAEliminar.clear();
            }
        };
        
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
    ...
}

 En este caso de uso, el el paso de mensajes se da de la siguiente manera:

  1. El usuario clickea el botón Eliminar.
  2. Tomamos los fabricantes seleccionados del table model y los agregamos en la lista  fabricantesAEliminar.
  3. Llamamos al método doEliminarFabricantes(), dentro del cual llamamos al método del Controlador eliminarFabricantes(…) y le pasamos como parámetro nuestra clase que implementa la vista.
  4. El método getFabricantesAEliminar() que será llamado desde el Controlador retorna la lista fabricantesAEliminar.
  5. Finalmente, si todo ha ido bien, en el método notificarFabricantesEliminados(…) notificamos al table model que cada uno de los elementos en la lista fabricantesAEliminar debe ser removido, para luego vaciar la lista.

Nótese que en este caso no solicitamos la confirmación del usuario antes de eliminar los fabricantes seleccionados. Si se requiere una confirmación adicional, podemos agregarla fácilmente entre el punto 1 y 2 para continuar con el flujo de ejecución si el usuario accede a la eliminación.

Consultar fabricantes

SD - Consultar fabricantes

public class FabricantesViewSwing implements IFabricantesView {    
    private IFabricantesController controller;
    private GenericDomainTableModel<Fabricante> tableModel;
    ...
    @Override
    public void doConsultarFabricantes() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void,Void> () {
            @Override
            protected Void doInBackground() throws Exception {
                controller.getListaFabricantes(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public void setListaFabricantes(final List<Fabricante> fabricantes) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                tableModel.clearTableModelData();
                tableModel.addRows(fabricantes);
            }
        };
        
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
}

Finalmente, la secuencia en el paso de mensajes en este caso de uso es así:

  1. El usuario presiona el botón Actualizar tabla.
  2. Llamamos al método doConsultarFabricantes(), dentro del cual llamamos al método del Controlador getListaFabricantes(…) y le pasamos como parámetro nuestra clase que implementa la vista.
  3. Desde el controlador llamamos al método de la Vista setListaFabricantes(…).
  4. Si todo ha ido bien, reiniciamos nuestro table model y agregamos la lista de fabricantes obtenida desde el controlador.

En este punto puede resultar extraño el paso de mensajes en los puntos 2 y 3. Es decir, ¿por qué la Vista pide la una lista al Controlador, se suscribe a sí misma pasándose como parámetro, y luego el Controlador setea la lista en la Vista? ¿Por qué no simplemente obtenemos una lista desde el Controlador y actualizamos el table model directamente? Pues bien, esta decisión de diseño obedece a un punto clave que fue mencionado al principio de esta entrada:

Event driven UI: dado que el Controlador no sabe cómo se implementa la Vista, ni esta última conoce cuánto tiempo puede llevarle al Controlador obtener una lista con las entidades fabricante, optamos por un esquema en el cual la Vista solicita la lista al controlador y se pasa a sí misma como parámetro a la espera que el Controlador le responda. De este modo, cuando el Controlador tenga por fin la lista con los fabricantes, se lo comunica a la Vista para que se actualice en consecuencia.

FabricantesViewSwing (código completo)

A continuación, el código completo de nuestra clase.

package example.mvc.view;

import example.mvc.controller.IFabricantesController;
import example.mvc.model.Fabricante;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

/**
 * @author Delcio Amarillo
 */
public class FabricantesViewSwing implements IFabricantesView {
    
    private IFabricantesController controller;
    private Fabricante nuevoFabricante, fabricanteAModificar;
    private final List<Fabricante> fabricantesAEliminar;
    
    private JPanel content;
    private JTable table;
    private Action agregarAction, modificarAction, eliminarAction, consultarAction;
    private GenericDomainTableModel<Fabricante> tableModel;
    
    public FabricantesViewSwing(IFabricantesController controller) {
        this.controller = controller;
        this.fabricantesAEliminar = new ArrayList<>();
    }
    
    @Override
    public void setFabricantesController(IFabricantesController controller) {
        this.controller = controller;
    }
    
    private void initActions() {
        agregarAction = new AbstractAction("Agregar nuevo") {
            @Override
            public void actionPerformed(ActionEvent e) {
                DatosFabricante datosFabricante = new DatosFabricante(null, null);
                JComponent component = datosFabricante.getUIComponent();
                
                int opcion = JOptionPane.showConfirmDialog ( 
                                              null
                                            , component
                                            , "Nuevo fabricante"
                                            , JOptionPane.OK_CANCEL_OPTION
                                            , JOptionPane.PLAIN_MESSAGE
                );
                
                if (opcion == JOptionPane.OK_OPTION) {
                    String nombre = datosFabricante.getNombreFabricante();
                    Date fechaFundacion = datosFabricante.getFechaFundacion();
                    nuevoFabricante = new Fabricante(nombre, fechaFundacion);
                    doAgregarFabricante();
                }
            }
        };
        
        modificarAction = new AbstractAction("Modificar") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int viewIndex = table.getSelectedRow();
                if (viewIndex > -1) {
                    int modelIndex = table.convertRowIndexToModel(viewIndex);
                    
                    fabricanteAModificar = tableModel.getDomainObject(modelIndex);
                    String nombre = fabricanteAModificar.getNombre();
                    Date fechaFundacion = fabricanteAModificar.getFechaFundacion();
                    
                    DatosFabricante datosFabricante = new DatosFabricante(nombre, fechaFundacion);
                    JComponent component = datosFabricante.getUIComponent();
                    
                    int opcion = JOptionPane.showConfirmDialog (
                                                null
                                              , component
                                              , "Modificar fabricante"
                                              , JOptionPane.OK_CANCEL_OPTION
                                              , JOptionPane.PLAIN_MESSAGE
                    );

                    if (opcion == JOptionPane.OK_OPTION) {
                        nombre = datosFabricante.getNombreFabricante();
                        fechaFundacion = datosFabricante.getFechaFundacion();
                        fabricanteAModificar.setNombre(nombre);
                        fabricanteAModificar.setFechaFundacion(fechaFundacion);
                        doModificarFabricante();
                    }
                }
            }
        };
        
        eliminarAction = new AbstractAction("Eliminar") {
            @Override
            public void actionPerformed(ActionEvent e) {
                int[] viewIndexes = table.getSelectedRows();
                if (viewIndexes.length > 0) {
                    fabricantesAEliminar.clear();
                    for (int viewIndex : viewIndexes) {
                        int modelIndex = table.convertRowIndexToModel(viewIndex);
                        Fabricante fabricante = tableModel.getDomainObject(modelIndex);
                        fabricantesAEliminar.add(fabricante);
                    }                    
                    doEliminarFabricantes();
                }
            }
        };
        
        consultarAction = new AbstractAction("Actualizar tabla") {
            @Override
            public void actionPerformed(ActionEvent e) {
                doConsultarFabricantes();
            }
        };
    }
    
    private void initTableAndModel() {
        List header = Arrays.asList(new Object[] {
            "Id", "Nombre", "Fecha de fundación"
        });
        
        tableModel = new GenericDomainTableModel<Fabricante>(header) {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                switch (columnIndex) {
                    case 0: return BigInteger.class;
                    case 1: return String.class;
                    case 2: return Date.class;
                }
                throw new ArrayIndexOutOfBoundsException(columnIndex);
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                Fabricante fabricante = getDomainObject(rowIndex);
                switch (columnIndex) {
                    case 0: return fabricante.getId();
                    case 1: return fabricante.getNombre();
                    case 2: return fabricante.getFechaFundacion();
                }
                throw new ArrayIndexOutOfBoundsException(columnIndex);
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return false;
            }
            
            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex) {}
        };
        
        table = new JTable(tableModel);
        table.setAutoCreateRowSorter(true);
        doConsultarFabricantes();
    }
    
    private void initContent() {
        JScrollPane scrollPane = new JScrollPane(table);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        
        JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        buttonsPanel.add(new JButton(agregarAction));
        buttonsPanel.add(new JButton(modificarAction));
        buttonsPanel.add(new JButton(eliminarAction));
        buttonsPanel.add(new JButton(consultarAction));
        
        content = new JPanel(new BorderLayout(8, 8));
        content.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
        content.add(scrollPane, BorderLayout.CENTER);
        content.add(buttonsPanel, BorderLayout.PAGE_END);
    }
    
    private void initComponents() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                initActions();
                initTableAndModel();
                initContent();
            }
        };
        
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
    
    public JComponent getUIComponent() {
        if (content == null) {
            initComponents();
        }
        return content;
    }

    @Override
    public void doAgregarFabricante() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                controller.agregarFabricante(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public Fabricante getNuevoFabricante() {
        return nuevoFabricante;
    }

    @Override
    public void notificarFabricanteAgregado(final Boolean resultado) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (resultado) {
                    tableModel.addRow(nuevoFabricante);
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Ha ocurrido un error al agregar un nuevo fabricante.")
                          .append("%n")
                          .append("Por favor contacte al administrador");
                    
                    String mensaje = String.format(sb.toString());
                    
                    JOptionPane.showMessageDialog (
                                    null
                                  , mensaje
                                  , "Error"
                                  , JOptionPane.WARNING_MESSAGE
                    );
                }
                nuevoFabricante = null;
            }
        };

        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }

    @Override
    public void doModificarFabricante() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                controller.modificarFabricante(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public Fabricante getFabricanteAModificar() {
        return fabricanteAModificar;
    }

    @Override
    public void notificarFabricanteModificado(final Boolean resultado) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (resultado) {
                    tableModel.notifyDomainObjectUpdated(fabricanteAModificar);
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Ha ocurrido un error al modificar el fabricante seleccionado.")
                          .append("%n")
                          .append("Por favor contacte al administrador");
                    
                    String mensaje = String.format(sb.toString());
                    
                    JOptionPane.showMessageDialog (
                                    null
                                  , mensaje
                                  , "Error"
                                  , JOptionPane.WARNING_MESSAGE
                    );
                }
                fabricanteAModificar = null;
            }
        };
            
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }

    @Override
    public void doEliminarFabricantes() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                controller.eliminarFabricantes(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public List<Fabricante> getFabricantesAEliminar() {
        return fabricantesAEliminar;
    }

    @Override
    public void notificarFabricantesEliminados(final Boolean resultado) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (resultado) {
                    
                    for (Fabricante fabricante : fabricantesAEliminar) {
                        tableModel.deleteRow(fabricante);
                    }
                    
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Ha ocurrido un error al eliminar los fabricantes seleccionados.")
                          .append("%n")
                          .append("Por favor contacte al administrador");
                    
                    String mensaje = String.format(sb.toString());
                    
                    JOptionPane.showMessageDialog (
                                    null
                                  , mensaje
                                  , "Error"
                                  , JOptionPane.WARNING_MESSAGE
                    );
                }

                fabricantesAEliminar.clear();
            }
        };
        
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }

    @Override
    public void doConsultarFabricantes() {
        SwingWorker<Void, Void> worker = new SwingWorker<Void,Void> () {
            @Override
            protected Void doInBackground() throws Exception {
                controller.getListaFabricantes(FabricantesViewSwing.this);
                return null;
            }
        };
        worker.execute();
    }
    
    @Override
    public void setListaFabricantes(final List<Fabricante> fabricantes) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                tableModel.clearTableModelData();
                tableModel.addRows(fabricantes);
            }
        };
        
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(runnable);
        } else {
            runnable.run();
        }
    }
}

Uniendo las piezas…

Ahora que tenemos una implementación para cada una de las capas, ha llegado el momento de ver cómo encajan las piezas del rompecabezas y cómo podemos lanzar nuestra aplicación. Para ello he creado un package llamado example.mvc.main el cual contiene una sola clase con un método main llamada ModelViewController, la cual se encargará de ejecutar nuestra aplicación.

ModelViewController

package example.mvc.main;

import example.mvc.controller.FabricantesControllerImp;
import example.mvc.controller.IFabricantesController;
import example.mvc.model.EntityManagerProvider;
import example.mvc.model.FabricantesModelJpa;
import example.mvc.model.IFabricantesModel;
import example.mvc.view.FabricantesViewSwing;
import example.mvc.view.IFabricantesView;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

/**
 * @author Delcio Amarillo.
 */
public class ModelViewController {
    
    public static void main(String[] args) {
        
        EntityManager entityManager = EntityManagerProvider.getProvider().getEntityManager();
        IFabricantesModel fabricantesModel = new FabricantesModelJpa(entityManager);
        final IFabricantesController controller = new FabricantesControllerImp(fabricantesModel);
        
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    Logger.getLogger(ModelViewController.class.getName()).log(Level.SEVERE, null, ex);
                }
                
                IFabricantesView view = new FabricantesViewSwing(controller);
                JComponent component = ((FabricantesViewSwing)view).getUIComponent();
                
                JFrame frame = new JFrame("ABMC Fabricantes");
                frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
                
                frame.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosing(final WindowEvent e) {
                        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
                            @Override
                            protected Void doInBackground() throws Exception {
                                EntityManagerProvider.getProvider().closeResources();
                                return null;
                            }

                            @Override
                            protected void done() {
                                e.getWindow().dispose();
                            }
                        };
                        worker.execute();
                    }
                });
                
                frame.add(component);
                frame.pack();
                frame.setLocationByPlatform(true);
                frame.setVisible(true);
            }
        });
    }
}

Capturas de pantalla

A continuación algunas capturas de pantalla de la aplicación ejecutándose.

Nuevo fabricanteModificar fabricanteFabricantes seleccionadosFabricantes eliminados

Conclusión

A lo largo de las últimas 4 entradas hemos visto un ejemplo de diseño por capas siguiendo el patrón MVC, además de un ejemplo de implementación para cada capa. Hemos visto además la ventaja de utilizar distintos patrones de diseño para cada capa, por ejemplo el patrón DAO y Singleton en la capa de Modelo, y cómo sacar provecho del concepto Event-driven UI de las interfaces de usuario para diseñar la Vista de nuestra aplicación.

Finalmente, como último comentario, podríamos decir que el nivel de desagregación de cada capa puede variar. Si tomamos la Vista como ejemplo, la misma podría desagregarse en 4 interfaces, una para cada caso de uso planteado. De este modo podríamos implementar cada caso de uso de manera diferente, a nivel de Vista. En todo caso, el análisis es clave para determinar si vale la pena. En líneas generales, como base para la toma de decisiones siempre debemos preguntarnos qué tan sencillo es cambiar de implementación de alguna de las capas, qué tan profundo es el impacto para el resto de las capas y qué nivel de acoplamiento es tolerable.

Código fuente

Pueden encontrar el código del ejemplo DemoModelViewController en JavaDeepCafe – Google Drive. El mismo tiene la estructura de un proyecto Maven, con un archivo pom.xml y una carpeta src donde se encuentra el código fuente.

Anuncios

Ejemplo de MVC y diseño por capas: el Controlador

En la entrada anterior vimos una implementación de nuestra capa de Modelo utilizando MySQL como RDBMS y JPA para el mapeo objeto-relacional. En esta nueva entrada veremos una implementación de nuestra capa Controlador utilizando un simple esquema de paso de mensajes.

Antes de comenzar, es bueno recordar que en esta capa debemos implementar la lógica de negocios que debe satisfacer nuestra aplicación. Por ejemplo, supongamos que cuando agregamos un nuevo Fabricante debemos crear una entrada de log en alguna tabla para saber qué usuario efectuó dicha operación y en qué momento lo hizo (marca de tiempo o time-stamp). O supongamos que debemos notificarle a algún usuario particular por e-mail cuando dicha operación se ha efectuado. Pues bien, este tipo de requerimientos deben satisfacerse en la capa Controlador.

No obstante, nuestro ejemplo de implementación es muy sencillo y sólo actúa como intermediario o man-in-the-middle entre el Modelo y la Vista, teniendo el recaudo de ofrecer una interfaz a la Vista que encapsula las llamadas a los métodos del Modelo. De este modo, si fuera necesario implementar los requerimientos mencionados en el párrafo anterior, sólo deberíamos modificar la implementación del Controlador.

Nuestro controlador en Java: package example.mvc.controller

Llegado el momento de escribir nuestra capa Controlador en Java, recordemos que en nuestro diseño sólo tenemos la interfaz IFabricantesController. Por lo tanto definiremos un package llamado example.mvc.controller el cual contendrá nuestra interfz y su correspondiente implementación. En esta capa no será necesario agregar ningún artefacto más allá de la mencionada implementación de nuestra interfaz.

Package example.mvc.controller

IFabricantesController

He aquí el código de nuestra interfaz Controlador. Nótese que en la sección de imports hay una referencia a la interfaz de la Vista, la cual aun no tenemos disponible pero que será incluida en la siguiente entrada. Esto obedece a que en nuestro diseño, la comunicación entre Controlador y Vista se efectúa de manera directa y por lo tanto es necesario que ambas capas mantengan una referencia entre sí. En la entrada anterior vimos que en el caso del Controlador y el Modelo la comunicación es diferente y el Modelo no necesita mantener una referencia del Controlador, por lo cual está completamente desacoplado de su capa inmediata superior.

package example.mvc.controller;

import example.mvc.model.IFabricantesModel;
import example.mvc.view.IFabricantesView;

/**
 * @author Delcio Amarillo
 */
public interface IFabricantesController {
    
    public void agregarFabricante(IFabricantesView view);
    
    public void modificarFabricante(IFabricantesView view);
    
    public void eliminarFabricantes(IFabricantesView view);
    
    public void getListaFabricantes(IFabricantesView view);
    
    public void setFabricantesModel(IFabricantesModel model);
}

FabricantesControllerImp

Antes de ver el código de nuestra clase FabricantesControllerImp, que implementa la interfaz IFabricantesController, resulta oportuno recordar cómo es el paso de mensajes entre capas para entender mejor la lógica de implementación. Tomemos por ejemplo el diagrama de secuencia para agregar un nuevo Fabricante (el resto puede consultarse en la entrada Ejemplo de MVC y diseño por capas: Introducción):

SD - Agregar fabricante

Como podemos observar, la Vista solicita al Controlador que agregue un nuevo Fabricante, este a su vez delega la operación en el Modelo y finalmente notifica a la Vista el resultado de la operación. El mismo criterio de diseño se aplica al resto de los casos de uso.

Con esto en mente, veamos el código de nuestra implementación:

package example.mvc.controller;

import example.mvc.model.Fabricante;
import example.mvc.model.IFabricantesModel;
import example.mvc.view.IFabricantesView;
import java.util.List;

/**
 * @author Delcio Amarillo
 */
public class FabricantesControllerImp implements IFabricantesController {
    
    private IFabricantesModel model;
    
    public FabricantesControllerImp(IFabricantesModel model) {
        this.model = model;
    }

    @Override
    public void agregarFabricante(IFabricantesView view) {
        Fabricante fabricante = view.getNuevoFabricante();
        int codigoOperacion = model.insertar(fabricante);
        Boolean resultado = (codigoOperacion == IFabricantesModel.EXITO);
        view.notificarFabricanteAgregado(resultado);
    }

    @Override
    public void modificarFabricante(IFabricantesView view) {
        Fabricante fabricante = view.getFabricanteAModificar();
        int codigoOperacion = model.modificar(fabricante);
        Boolean resultado = (codigoOperacion == IFabricantesModel.EXITO);
        view.notificarFabricanteModificado(resultado);
    }

    @Override
    public void eliminarFabricantes(IFabricantesView view) {
        List<Fabricante> fabricantes = view.getFabricantesAEliminar();
        int codigOperacion = model.eliminar(fabricantes);
        Boolean resultado = (codigOperacion == IFabricantesModel.EXITO);
        view.notificarFabricantesEliminados(resultado);
    }
    
    @Override
    public void getListaFabricantes(IFabricantesView view) {
        List<Fabricante> fabricantes = model.getFabricantes();
        view.setListaFabricantes(fabricantes);
    }

    @Override
    public void setFabricantesModel(IFabricantesModel model) {
        this.model = model;
    }
}

Nótese que en todo momento hacemos uso de nuestro atributo model el cual no se inicializa dentro de nuestra clase, sino que debe ser inicializado en el exterior y bien pasado como parámetro al inicializar la clase FabricantesControllerImp o bien utilizando el setter correspondiente. Por ejemplo:

EntityManager entityManager = EntityManagerProvider.getProvider().getEntityManager();
IFabricantesModel model = new FabricantesModelJpa(entityManager);
IFabricantesController controller = new FabricantesControllerImp(model);

De ese modo nuestro Controlador queda totalmente desacoplado de la implementación del Modelo e incluso podemos cambiar de Modelo en tiempo de ejecución y las consecuentes llamadas al Controlador seguirán funcionando como es de esperar, haciendo que la implementación subyacente del Modelo sea totalmente transparente para la Vista:

IFabricantesController controller = new FabricantesControllerImp(modelImp1);
...
controller.setFabricantesModel(modelImp2);
...
controller.setFabricantesModel(modelImp3);

Una vez más, podemos dar fe de las ventajas del diseño utilizando interfaces.

Conclusión

En esta entrada vimos una implementación sencilla de nuestra capa Controlador, basada en un simple mecanismo de paso de mensajes con la Vista y delegación hacia el Modelo. Sin embargo, cabe destacar una vez más la robustez del diseño en el sentido que ante la necesidad de cumplir algún requerimiento de negocios sólo sería necesario modificar el código de nuestro Controlador. En la siguiente entrada veremos un ejemplo de implementación de la Vista utilizando Swing para crear nuestra interfaz de usuario.

Ejemplo de MVC y diseño por capas: el Modelo

En la entrada anterior diseñamos las capas de nuestra aplicación siguiendo el patrón MVC. En esta nueva entrada veremos un ejemplo de implementación de la capa inferior, es decir, el Modelo.

Recordemos que hasta el momento definimos una clase de dominio Fabricante y una interfaz IFabricantesModel que define el comportamiento esperado en esta capa, de modo tal que los detalles de implementación no fueran relevantes para nuestro diseño. Ahora bien, llegado el momento de implementar la capa, es necesario conocer el contexto en el cual la misma deberá funcionar:

  • Los datos de la aplicación serán persistidos en una base de datos MySQL.
  • Utilizaremos JPA (Java Persistence API) para el mapeo objeto-relacional. Por lo tanto nuestra clase Fabricante no será un simple POJO sino una entidad JPA.
  • No utilizaremos ningún contenedor para gestionar las unidades de persistencia, razón por la cual deberemos configurar la/s unidad/es de persistencia necesaria/s a través del archivo persistence.xml
  • Asimismo, tampoco utilizaremos JTA (Java Transaction API) para el manejo de transacciones, por lo que deberemos gestionarlas por nuestra cuenta.

Bien, comencemos entonces con la implementación.

Creando la base de datos, tablas y usuarios

Como primer paso, crearemos nuestra base de datos, una tabla Fabricantes para persistir esta entidad y un usuario con los permisos necesarios para conectarse a la base de datos y operar con dicha tabla.

Nota: en líneas generales, es una buena práctica en materia de seguridad el crear uno o más usuarios dedicados para nuestra aplicación, con el mínimo de permisos necesarios para operar. De este modo podemos reducir el impacto de ataques de Inyección SQL desde la misma conexión a la base de datos. Un tema muy interesante que merece su propio post y escapa a los propósitos de este ejemplo.

DROP DATABASE IF EXISTS EjemploMVC;
CREATE DATABASE EjemploMVC;
USE EjemploMVC;

DROP TABLE IF EXISTS Fabricantes;

CREATE TABLE Fabricantes (
    idfabricante BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    nombre VARCHAR(250),
    fechafundacion DATE,
    PRIMARY KEY (idfabricante),
    UNIQUE KEY uk_fabricantes_nombre (nombre)
);

CREATE USER 'mvc'@'localhost' IDENTIFIED BY 'JavaDeepCafe';
GRANT USAGE ON EjemploMVC.* TO 'mvc'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON EjemploMVC.Fabricantes TO 'mvc'@'localhost';

Configurando JPA: persistence.xml

El siguiente paso es configurar JPA en nuestra aplicación, es decir, definir nuestro archivo persistence.xml. Como mencionaba anteriormente, nuestra aplicación no se conectará a ningún contenedor para obtener un EntityManager a través de inyección de recursos, ni  haremos uso de JTA para el manejo de transacciones. Por lo tanto deberemos definir la unidad de persistencia y manejar las transacciones y el ciclo de vida del/los entity managers por nuestra cuenta.

Llamaremos a nuestra unidad de persistencia “MvcDemoPU” e indicaremos que el tipo de transacción es “RESOURCE_LOCAL”

Nota: el siguiente archivo persistence.xml está configurado para trabajar con la implementación EclipseLink JPA 2.1. Sin embargo, si se desea utilizar otro proveedor como Hibernate JPA, los cambios necesarios son mínimos.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" 
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  
  <persistence-unit name="MvcDemoPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider<!--provider>
    <class>example.mvc.model.Fabricante<!--class>
    <properties>
      <!-- Propiedades de conexión a la base de datos -->
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/EjemploMVC"/>
      <property name="javax.persistence.jdbc.password" value="JavaDeepCafe"/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="mvc"/>
      <!-- Propiedades de EclipseLink para habilitar el Log de las operaciones -->
      <!-- Es muy útil para ver por consola las sentencias SQL ejecutadas -->
      <property name="eclipselink.logging.logger" value="DefaultLogger"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.logging.level.sql" value="FINE"/>
      <property name="eclipselink.logging.parameters" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

Nuestro modelo en Java: package example.mvc.model

Ha llegado el momento de escribir nuestro modelo en Java, para lo cual definiremos un package llamado example.mvc.model y que contendrá nuestras clases e interfaces correspondientes al Modelo. Es necesario destacar que la implementación de una capa no sólo tendrá las clases e interfaces que hemos diseñado, sino que también puede contener clases y artefactos adicionales para ayudar a cumplir con el contrato especificado en el diseño. En este caso incluiremos la implementación de la interfaz IFabricantesModel y una clase que se encargará de gestionar el ciclo de vida del EntityManager que se encarga de gestionar la persistencia. Tenemos entonces nuestro package:

Package example.mvc.model

Fabricante

Comenzaremos con nuestra clase de datos, la cual como dijimos es una entidad JPA, y como tal, además de las anotaciones correspondientes, debe implementar la interfaz Serializable, para lo cual sobrescribimos los métodos equals() y haschCode() heredados de la clase Object. También resulta oportuno sobrescribir el método toString().

package example.mvc.model;

import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

/**
 * @author Delcio Amarillo
 */
@Entity
@Table(name = "Fabricantes")
public class Fabricante implements Serializable {
    
    private static final long serialVersionUID = 6787411012408357010L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private BigInteger idFabricante;
    
    @Basic
    private String nombre;
    
    @Temporal(TemporalType.DATE)
    private Date fechaFundacion;
    
    /**
     * Crea una nueva instancia de la clase {@code Fabricante}.
     * @deprecated Obligatorio para JPA, NO debe utilizarse explícitamente.
     * @see #Fabricante(java.lang.String, java.util.Date) 
     */
    public Fabricante() {
        super();
    }

    public Fabricante(String nombre, Date fechaFundacion) {
        this.nombre = nombre;
        this.fechaFundacion = fechaFundacion;
    }

    public BigInteger getId() {
        return idFabricante;
    }
    
    public void setId(BigInteger id) {
        // Esta propiedad no debe ser modificada explícitamente!
        throw new UnsupportedOperationException("Not supported!");
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Date getFechaFundacion() {
        return fechaFundacion != null ? new Date(fechaFundacion.getTime()) : null;
    }

    public void setFechaFundacion(Date fechaFundacion) {
        this.fechaFundacion = fechaFundacion != null 
                            ? new Date(fechaFundacion.getTime()) : null;
    }
    
    @Override
    public int hashCode() {
        return idFabricante != null ? idFabricante.hashCode() : 0;
    }

    @Override
    public boolean equals(Object object) {
        if (object instanceof Fabricante) {
            Fabricante other = (Fabricante) object;
            return (this.idFabricante != null || other.idFabricante == null) 
                && (this.idFabricante == null || this.idFabricante.equals(other.idFabricante));
        }
        return false;
    }

    @Override
    public String toString() {
        return "example.mvc.model.Fabricante[ id=" + idFabricante + " ]";
    }
}

Observaciones:

  • Nótese que el método setId() lanza una UnsupportedOperationException y esta decisión obcedece al hecho de que en general no es necesario (ni posible, según las reglas de negocio) actualizar un identificador único. Como JPA trabaja con la API Reflection, no necesita ningún setter para modificar el valor de los atributos de una entidad. Por lo tanto, mientras más restringido tengamos el estado interno de nuestra clase, mejor (principio de encapsulamiento).
  • Al trabajar con objetos Date en Java, debemos tomar recaudos extra porque dicha clase NO es inmutable. Por lo tanto, si devolvemos directamente el atributo fechaFundacion en lugar de una copia, podría modificarse su estado interno desde el exterior y provocar efectos no deseados en nuestra clase Fabricante. Del mismo modo, en el setter también asignamos una copia del parámetro para evitar que las referencias externas, si son modificadas, puedan afectar el estado interno de nuestra clase.

EntityManagerProvider

Como indica el estereotipo en el diagrama, esta clase implementará el patrón Singleton con la finalidad de mantener una única instancia de la clase a lo largo de todo el ciclo de vida de la ejecución de nuestra aplicación.

package example.mvc.model;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

/**
 * @author Delcio Amarillo
 */
public class EntityManagerProvider {
    
    private EntityManagerFactory entityManagerFactory;
    private EntityManager entityManager;
    private static EntityManagerProvider provider;
    
    
    private EntityManagerProvider() {
        initEntityManager();
    }

    private void initEntityManager() {
        entityManagerFactory = Persistence.createEntityManagerFactory("MvcDemoPU");
        entityManager = entityManagerFactory.createEntityManager();
    }
    
    public static synchronized EntityManagerProvider getProvider() {
        if (provider == null) {
            provider = new EntityManagerProvider();
        }
        return provider;
    }
    
    public EntityManager getEntityManager() {
        return entityManager;
    }
    
    public void closeResources() {
        if (entityManager != null && entityManager.isOpen()) {
            entityManager.close();
        }
        
        if (entityManagerFactory != null && entityManagerFactory.isOpen()) {
            entityManagerFactory.close();
        }
    }
}

Es necesario mencionar que esta implementación del patrón Singleton funciona si el ciclo de vida de ejecución tiene lugar en la misma JVM, como es el caso de una aplicación de escritorio. En el caso de aplicaciones Web, el encargado de iniciar una nueva JVM es el servidor o contenedor Web y la misma es única para múltiples Servlets. Por lo tanto en ese entorno es necesario implementar el patrón ThreadLocal o bien utilizar Session Beans anotados con @Singleton.

IFabricantesModel

He aquí el código de nuestra interfaz de modelo:

package example.mvc.model;

import java.math.BigInteger;
import java.util.List;

/**
 * @author Delcio Amarillo
 */
public interface IFabricantesModel {
    
    public static final int EXITO = 0;
    
    public static final int FALLO = -1;
    
    public Fabricante getFabricantePorId(BigInteger id);
    
    public List<Fabricante> getFabricantes();
    
    public int insertar(Fabricante fabricante);
    
    public int modificar(Fabricante fabricante);
    
    public int eliminar(Fabricante fabricante);
    
    public int eliminar(List<Fabricante> fabricantes);    
}

Nótese que se agregaron dos constantes que podrán ser utilizadas por la capa superior para evaluar el resultado de la operación en esta capa. Como vimos en la entrada anterior, el Modelo se comunicará con el Controlador a través del resultado de sus operaciones, no por notificación directa.

FabricantesModelJpa

Finalmente, llegó la hora de ver el código de la clase concreta que implementa nuestra interfaz IFabricantesModel.

package example.mvc.model;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.TypedQuery;

/**
 * @author Delcio Amarillo
 */
public class FabricantesModelJpa implements IFabricantesModel {
    
    private final EntityManager entityManager;
    
    public FabricantesModelJpa(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Override
    public Fabricante getFabricantePorId(BigInteger id) {
        return entityManager.find(Fabricante.class, id);
    }

    @Override
    public List<Fabricante> getFabricantes() {
        List<Fabricante> fabricantes = new ArrayList<>();
        String jpql = "SELECT f FROM Fabricante f";
        TypedQuery<Fabricante> query = entityManager.createQuery(jpql, Fabricante.class);
        fabricantes.addAll(query.getResultList());
        return fabricantes;
    }

    @Override
    public int insertar(Fabricante fabricante) {
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            entityManager.persist(fabricante);
            transaction.commit();
            return EXITO;
        } catch (EntityExistsException ex) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
            transaction.rollback();
            return FALLO;
        }
    }

    @Override
    public int modificar(Fabricante fabricante) {
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            entityManager.merge(fabricante);
            transaction.commit();
            return EXITO;
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
            transaction.rollback();
            return FALLO;
        }
    }

    @Override
    public int eliminar(Fabricante fabricante) {
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            Fabricante fabricanteAEliminar = entityManager.getReference(Fabricante.class, fabricante.getId());
            entityManager.remove(fabricanteAEliminar);
            transaction.commit();
            return EXITO;
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
            transaction.rollback();
            return FALLO;
        }
    }

    @Override
    public int eliminar(List<Fabricante> fabricantes) {
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            for (Fabricante fabricante : fabricantes) {
                Fabricante fabricanteAEliminar = entityManager.getReference(Fabricante.class, fabricante.getId());
                entityManager.remove(fabricanteAEliminar);
            }
            transaction.commit();
            return EXITO;
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
            transaction.rollback();
            return FALLO;
        }
    }
}

 Resulta útil remarcar en este punto la ventaja de haber definido nuestra interfaz de modelo siguiendo el patrón DAO: la clase EntityManager tiene su correlato casi directo para cada uno de nuestros métodos, con lo cual resulta muy sencillo implementar esta capa con JPA.

Nótese también que esta implementación toma como parámetro un EntityManager al instanciar la clase, para lo cual es conveniente la clase EntityManagerProvider vista anteriormente. Por ejemplo:

EntityManager entityManager = EntityManagerProvider.getProvider().getEntityManager();
IFabricantesModel model = new FabricantesModelJpa(entityManager);

Conclusión

En esta entrada vimos un ejemplo de implementación de nuestra capa de Modelo basada en algunos requerimientos no funcionales. Si en lugar de JPA se deseara utilizar JDBC plano, entonces sólo deberíamos quitar las anotaciones de nuestra clase Fabricante y crear una nueva implementación de la interfaz IFabricantesModel que haga uso de dicha API.

En la siguiente entrada, veremos un ejemplo de implementación de la capa Controlador.

Ejemplo de MVC y diseño por capas: Introducción

Cuando estamos aprendiendo a desarrollar aplicaciones que conllevan una cierta complejidad, en algún momento e indudablemente oímos hablar de diseño o arquitectura en capas (o niveles)  y del patrón Modelo-Vista-Controlador (de aquí en adelante MVC). Dado que existe gran cantidad de material sobre este tema, rápidamente comprendemos el concepto y nos convencemos de todas sus ventajas Más aun, hasta llegamos a convencernos que no existe mejor forma de diseñar y desarrollar una aplicación.

Sin embargo, al momento de aplicar los conceptos en la práctica, vemos que la situación no es tan sencilla como parecía en un prinicpio. Dado que es altamente improbable encontrar un ejemplo que se ajuste perfectamente a nuestras necesidades, caemos en un mar de incertidumbre sobre qué debemos hacer primero, cómo diseñamos las capas, a qué capa corresponde qué clase, y un sinfín de etcéteras. Máxime cuando sabemos o conocemos la tecnología sobre la cual implementaremos nuestra aplicación: los aspectos relacionados con la implementación nos obnubilan y no nos dejan ver el concepto de diseño con claridad.

Pues bien, estos son los motivos por lo cuales he decidido escribir este artículo, en el cual presentaré un ejemplo completo de cierta complejidad a lo largo de 4 entradas, siendo esta primera la que describirá el diseño en capas y las siguientes 3 corresponderán a una implementación concreta de cada una de las capas. Nótese que dije una implementación concreta, porque en realidad cada capa puede tener múltiples implementaciones y cualquier combinación debería poder ensamblarse sin problemas.

Como primera medida, vamos a definir qué es lo que haremos. Supongamos que nuestro requerimiento es el siguiente:

Permitir a los usuarios consultar, persistir , modificar y eliminar información acerca de una entidad Fabricante, la cual tiene un identificador único, un nombre y una fecha de fundación.

Antes de comenzar con el diseño de las capas, y a modo de comentario personal, a veces resulta útil pensar en cada capa como un contrato o servicio que se le debe brindar a las capas  contiguas. Es decir, conviene pensar qué es lo que realmente debe ofrecerle una capa a las demás. También debemos pensar cómo se comunicarán las capas entre sí, por ejemplo, manteniendo una referencia entre ellas o bien analizando el resultado del paso de mensajes de una capa a otra. En este contexto, la herramienta por excelencia para el modelado en cualquier lenguaje orientado a objetos que se precie son las interfaces.

Modelo

Comenzaremos, como es de esperar, por la base de nuestra arquitectura, es decir, el Modelo. Viendo detenidamente los requerimientos a satisfacer, podemos identificar dos cosas: necesitaremos una clase de dominio que represente a la entidad Fabricante, y otra clase que nos permita consultar, persistir, modificar y eliminar objetos de la clase Fabricante, operaciones que normalmente se conocen como ABMC (Alta, Baja, Modificación y Consulta) o CRUD (Create, Update, Read and Delete) en inglés.

Definimos una clase Fabricante y la incluimos dentro del Modelo, por la simple razón que sirve para modelar una entidad de datos. Sin embargo, esta clase será transversal a toda la arquitectura y podrá utilizarse en cualquiera de las tres capas, como veremos más adelante.

Fabricante

Ahora bien, para definir las operaciones ABMC que debe proveer esta capa, utilizaremos una interfaz para mantener aislado el comportamiento esperado de la implementación concreta. Nótese que la interfaz está diseñada siguiendo el patrón Data Access Object (o simplemente patrón DAO) , el cual es ampliamente utilizado al momento de trabajar con objetos persistentes en algun medio (base de datos, archivos, etc).

IFabricantesModel

Podemos pensar en la interfaz IFabricantesModel como la puerta de acceso o fachada a la persistencia de nuestros datos. Lo bueno de este enfoque es que no conocemos ni necesitamos conocer detalles de implementación. Es decir, los objetos pueden persistirse utilizando un RDBMS como MySQL o MS SQL Server, o bien en un archivo plano o bien una base de datos embebida, etc. Es necesario destacar que el Modelo no mantiene una referencia hacia la capa superior (Controlador) sino que la comunicación se efectúa devolviendo un número entero, el cuál debe interpretarse como un código de cómo ha ido la operación. Será entonces responsabilidad del Controlador definir qué curso de acción tomar en caso de fallo en la capa inferior. Por supuesto, esto una decisión de diseño y podría realizarse de otra manera, pero lo he diseñado así a los efectos de mostrar dos tipos diferentes de comunicación entre capas. El segundo método se aplica entre Vista y Controlador y se explicará más adelante.

Controlador

La siguiente capa a diseñar es el Controlador y diremos que en esta capa ocurren dos cosas: por un lado debe ejecutarse la lógica de negocios y por otro lado debe actuar como mediador o middle-man entre el Modelo y la Vista. Para esto, debe proveerle a la Vista un contrato que encapsule los métodos que serán llamados en el Modelo, además de notificarle sobre el resultado de las operaciones.

IFabricantesController

Como podemos ver, el Controlador mantendrá una referencia al Modelo y los métodos provistos a la Vista tomarán como parámetro la misma VIsta que los está invocando, a los efectos de notificarle cómo han ido los eventos. Quizás el mecanismo resulte complejo, pero quedará claro más adelante cuando veamos el paso de mensajes entre las capas.

Vista

Finalmente, la Vista deberá proveer mecanismos para: recolectar información ingresada por el usuario (por ejemplo para agregar un nuevo Fabricante),  procesar acciones o eventos disparados por el usuario, y para ser notificada acerca del resultado de dicho procesamiento.

IFabricantesView

Vemos entonces que la interfaz IFabricantesView provee una serie de métodos para obtener información desde el usuario: getNuevoFabricante(); para procesar acciones: doAgregarFabricante(); y para ser notificada acerca de cómo ha ido el procesamiento: notificarFabricanteAgregado(..). También podemos observar que la Vista mantiene una referencia al Controlador, al cual deberá llamar para procesar las acciones solicitadas por el usuario.

Nota al margen

Un debate muy común en lo que respecta a esta capa y la recolección de datos ingresados por el usuario, es si es responsabilidad de la Vista mapear los datos dentro de una clase de dominio, o si sólo debe capturar datos primitivos y pasarlos al Controlador para que éste se encargue de dicha tarea. En mi opinión, si bien el segundo enfoque es más purista en lo que respecta a división de responsabilidades llevando al máximo el argumento de “la vista sólo debe recolectar datos, no importa lo que representen”, el primer enfoque resulta mucho más robusto y escalable con el tiempo. Por ejemplo, supongamos que la Vista sólo captura un String para el nombre del fabricante y un Date para su fecha de fundación. Perfecto, hacemos el mapeo de ambos datos en el Controlador y construimos un objeto Fabricante. Ahora bien, supongamos que debemos agregar nuevos campos con el domicilio del fabricante y sus datos de contacto. Si queremos modificar la Vista, forzosamente deberemos modificar la interfaz para exigirle a la vist que capture los nuevos campos. En cambio, manteniendo el diseño actual sólo será necesario cambiar la implementación: el contrato (interfaz) permanece intacto.

Secuencias de paso de mensajes

Para ilustrar los procesos de llamadas y paso de mensajes ente las capas, veamos los siguientes diagramas de secuencia. Nótese que en ningún momento hacemos referencia a alguna implementación en particular, simplemente se ilustra cómo las interfaces interactúan entre sí.

Agregar fabricante

SD - Agregar fabricante

Modificar fabricante

SD - Modificar fabricante

Eliminar fabricantes

SD - Eliminar fabricantes

Consultar fabricantes

SD - Consultar fabricantes

Conclusión

En esta primera entrada, hemos definido la arquitectura necesaria para cumplir con los requerimientos planteados, siguiendo el patrón MVC. En la siguiente entrada veremos una implementación del Modelo utilizando MySQL como RDBMS y JPA (Java Persistence API) para la implementación de nuestro DAO.