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

17 pensamientos en “Ejemplo de MVC y diseño por capas: la Vista

  1. Hernán

    Buenísimo!!! Lo terminaste super-rápido, pensé que se iba a hacer más largo (en tiempo, no en cantidad de líneas…). Un ejemplo hecho con excelente calidad, hay código para analizar durante un rato largo. Estoy viendo varias de las cosas que tenía pendientes combinadas en este ejemplo, habrá que dedicarle unas cuantas lecturas.
    Además, al reutilizar el ‘GenericDomainTableModel’, es posible repasar características del mismo que no habían quedado claras (en lo personal, digo).
    Gracias Delcio, saludos!

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Así es, tenía que aprovechar la racha y no dejarme estar. Una vez más, muchas gracias por seguir el ejemplo. Cualquier duda o consulta, no dudes en dejar un comentario. Saludos!

      PD: muy feliz y próspero 2015! 😀

      Me gusta

      Responder
  2. Hernán

    Hola Delcio, espero que hayas comenzado con un excelente año!
    Acá estamos, en este veranito de estudio constante aprovechando un tiempo libre para modificar ejemplos anteriores. Todavía no implementé completo este ejercicio, lo que voy haciendo es escalar otro ya terminado, porque me sirve más para entender qué ocurre con cada parte. Entonces, dejo para una próxima etapa incorporar JPA, los métodos ‘notify’, y un para de cosas más, menores, por comodidad.
    Ya conseguí que me funcione el ‘GenericDomainTableModel’ con este ejemplo, la verdad que es buenísimo, era lo que más me importaba.
    El problema que tengo es que no actualiza los datos al modificar en la vista, usé ‘tableModel.notifyDomainObjectUpdated(userForModify);’ (‘userForModifiy’ es el equivalente a ‘fabricanteAModificar’) al ejecutar un botón ‘Guardar’ de una ventana emergente y no se actualiza. Repetí acciones similares para insertar (‘tableModel.addRow(newUser);’) y para borrar, funcionan bien, actualizan la vista perfectamente, pero solamente la modificación se complica. En la base todo se realiza bien con todos los métodos, estoy usando el mismo ejemplo que te comenté para el ‘AbstractTableModel’, por suerte en un par de días conseguí adaptar casi todo, principalmente la interfaces y las clases nuevas. Igual es denso el ejemplo eh, si hubiera que desarrollarlo en clases llevaría unas cuantas horas (dependiendo del nivel de los asistentes, claro) de trabajo explicar y relacionar todo.
    Bueno, ando con ese inconveniente, si tenés alguna sugerencia será bienvenida, no es muy grande, igualmente seguiré viendo si lo soluciono.
    Saludos Delcio!

    Me gusta

    Responder
  3. Hernán

    Solucionado, agregué las ‘Actions’ modificadas y funcionó la actualización, junto con los otros métodos. La semana que viene pruebo con JPA.
    Saludos!

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Hola Hernán, gracias e igualmente!

      Perdón por no contestar antes, pero recién estoy volviendo a las actividades. Me alegro que hayas podido solucionar el inconveniente. Supongo que el problema era que el objeto que le pasabas como parámetro al método ‘tableModel.notifyDomainObjectUpdated(…)’ no se encontraba en el table model y por lo tanto no notificaba nada al JTable.

      La implementación de ese método recorre la lista de objetos en el table model y si el parámetro es exactamente el mismo objeto que el índice actual, entonces notifica a los listeners para que actualicen la fila. La comparación se efectúa con el operador ‘==’ y esto significa que ambas variables deben apuntar a la misma dirección de memoria para que la expresión sea verdadera. Se puede modificar la implementación y utilizar ‘equals(…)’ lo cual sería incluso mejor.

      Cualquier duda o comentario, no dudes en comentar. Saludos!

      Me gusta

      Responder
  4. Hernán

    Hola Delcio, no era importante, además es totalmente lógico (y necesario) descansar un poco. Más que nada era una curiosidad, porque yo decidí quitarle cosas para entender mejor cómo funciona el ejemplo. Le quité todos los métodos ‘notify’ a la vista (los tres), y en lugar de usarlos, llamé directamente al método ‘notifyDomainObjectUpdated()’ del ‘GDTM’ al terminar de ejecutar un método para actualizar (desde la vista también); me llamó la atención que no lo hacía, y solamente tenía problemas ese método, porque haciendo lo mismo con los métodos para insertar (colocando al final del método ‘tableModel.addRow(newUser);’) y borrar (‘tableModel.deleteRow(userForDelete);’) sí se actualizaba la tabla. Pero no hubo forma. Recién cuando agregué los ‘notify’, pasando datos por el controlador, para recibir el booleano empezó a actualizar la tabla en la vista al ‘actualizar’ (valga la redundancia). Invocado directamente desde la vista no caminaba.
    Ah, ayer descubrí JavaFX, está buenísimo! No lo conocía (siempre lo vi en NetBeans como opción para generar proyectos, pero no le había prestado atención). Todavía le falta madurar, hay poca documentación (comparada con SWING), y es de ‘Oracle’ (no sé si está totalmente liberado…), los ‘dialog’ recién serán posibles en marzo de este año con Java 8.4 (igualmente ya disponible), pero permite meterle CSS a las vistas! Además se puede combinar con SWING. La verdad que me pareció muy interesante, ya empecé a trabajar en un CRUD usándolo.
    Saludos Delcio!

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Lo que pasa es que los métodos ‘addRow(…)’ y ‘deleteRow(…)’ tienen un mecanismo de notificación ligeramente diferente y es difícil que fallen porque trabajan sobre operaciones sobre una lista, agregando y removiendo elementos respectivamente para luego notificar a los listeners. Es bastante simple y directo.

      En cambio el metodo ‘notifyDomainObjectUpdated(…)’ recorre la lista verificando que el elemento actual sea exactamente igual al parámetro ‘domainObject’ en el sentido que debe apuntar a la misma dirección de memoria. Si por alguna razón le pasamos un objeto cuyo estado interno sea igual PERO no apunta a la misma dirección, lo ignorará y no notificará a los listeners. Fijate en la implementación del caso de uso Modificar fabricante. Al atributo ‘fabricanteAModificar’ le asignamos un objeto que efectivamente se encuentra en el table model, lo modificamos y finalmente llamamos al método ‘notifyDomainObjectUpdated(…)’ . Por todo lo explicado anteriormente, forzosamente debe funcionar.

      Con respecto a JavaFX realmente es muy interesante aunque hay opiniones encontradas. Por un lado es una mejora desde el punto de vista de la experiencia de usuario y de la estética visual, permitiendo utilizar CSS en lugar del sistema de Look and Feel de Swing, En cambio, hay quienes sostienen que no representa el cambio que se esperaba y que llegó demasiado tarde, en el sentido que demoró demasiado tiempo en estar listo y apenas alcanza las expectativas que los desarrolladores tenían de un nuevo framework para aplicaciones de escritorio. Además como vos decís, hay muy poca documentación comparado con Swing para lo cual obviamente se necesitará más tiempo. En fin, en mi opinión vale totalmente la pena, aunque la sensación es que vamos a tener a Swing dando vueltas por un tiempo más hasta que sea totalmente reemplazado.

      Saludos!

      Me gusta

      Responder
  5. Hernán

    Claro Delcio, después releí tu comentario anterior y entendí mejor como funciona ese ‘notifyDomainObjectUpdated’, y la diferencia con respecto al método ‘addRow’ y ‘deleteRow’, que simplemente agregan y quitan sin chequear.
    Con respecto a JavaFX, anduve viendo algunas de las polémicas que andan dando vueltas por allí, y sí, llegó medio tarde, más aún cuando recién ahora son posibles las ventanas de diálogo por ejemplo. Entiendo la cuestión. Así que coincido en que hay SWING para rato todavía, en particular por el tema de la documentación.
    Saludos!

    Me gusta

    Responder
  6. Grover Ortega

    Muchas Gracias por el Post, es muy buena la orientacion que haces de MVC con el miniProyecto. Una Consulta: En el paquete Modelo hace una interfaz especifica “IFabricanteModel” para una entidad. ¿En un proyecto de mas de una entidad crearias otras interfaces especificas para cada una, o solo crearias una interfaz generica tipo .?. ¿Seria correcto hacer solo una Generica?

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Hola Grover, muchas gracias por dejar tu comentario.Tu pregunta es muy buena y de hecho también me lo he preguntado alguna vez: Persistence contract design: Single generic interface vs. Several specialized interfaces. Según mi experiencia, en algún punto tendemos a crear una sola interfaz usando generics al estilo IGenericDao<T> creyendo que nuestro código será más óptimo y fácil de mantener. Sin embargo existen algunos factores que vale la pena tener en cuenta para no hacerlo:

      1. Desde el punto de vista del diseño de la aplicación (por ejemplo en un diagrama de secuencias) es más fácil de entender una interfaz especializada por cada clase de dominio que una sola genérica.
      2. Los Generics son una herramienta propia del lenguaje y como tal es válido utilizarlos en la implementación para obtener un template o modelo y extender del mismo, no para diseñar.
      3. Tarde o temprano tendremos excepciones que van más allá de las operaciones CRUD y tendremos que extender la interfaz genérica.

      Por ejemplo, supongamos que necesitamos un método para obtener los fabricantes que siguen activos. Este requerimiento necesita un método en la firma de la interfaz que nos obliga a extender la interfaz genérica. Por lo tanto resultaría conveniente tener de entrada una interfaz específica para la entidad Fabricante. Sí estoy de acuerdo en tener una interfaz genérica pero especializarla para cada entidad de dominio.

      public interface IGenericDao<T> {
      
          public int insert(T domainObject);
      
          public int update(T domainObject);
      
          public Boolean delete(T domainObject);
      
          public List<T> getAll();
      }
      
      public interface IFabricantesDao extends IGenericDao<Fabricante> {
      
          public List<Fabricante> getFabricantesActivos();
      }
      

      Como comentario final diría que muchas veces el querer modelar todo como un solo caso general nos lleva a cometer errores de diseño al mediano y largo plazo. Recientemente, a modo anecdótico, me tocó trabajar en un proyecto legacy donde quisieron modelar un servicio de Request-Response de modo totalmente genérico con interfaces, y forzar a toda la aplicación a adaptarse a ese modelo. Obviamente una llamada o Request a una base de datos no es lo mismo que a un Web service o un módulo EJB, y obviamente tampoco lo son sus respectivas Responses, razón por la cual el código se fue llenando de dwon-cast hacia las clases cocncetas porque era necesario llamar a métodos específicos de cada implementación. En síntesis, una idea en apariencia brillante llevó a un desastre en el código.

      Me gusta

      Responder
      1. Grover Ortega

        Gracias por la respuesta. Veo que es una estrategia muy buena tener la interfaz generica y crear las interfaces especificas en casos que lo ameriten.

        Me gusta

  7. Jonathan Hernandez

    Hola Delcio, me han sido de mucha utilidad tus ejemplos, los he estudiado, e hice dos proyectos diferentes basados en el de tus ejemplos… en ambos hice uso de una base de datos con dos o mas tablas relacionadas… y a la hora de hacer la vista en ambos proyectos se me genero un aprieto… la vista necesitaba manejar datos de dos modelos para poder hacer bien su funcionamiento, pero dado a como esta estructurado el proyecto se me hace confuso.

    Para que se haga una idea uno de los proyectos esta basado en el proyecto DemoGenericDomainTableModel que usted realizo, en el se utilizo Procesador y Fabricante, en la interfaz se tenia que agregar Un Fabricante y luego agregar los procesadores. Al hacer el proyecto DemoGenericDomainTableModel usando MVC; Procesador y Fabricante ambos tienen su implementacion usando DAO y tambien sus propios controladores …. pero no dos vistas ya que necesito nada mas una vista pero dado que una vista esta asociada a un controlador… View(controlodor)… me enrede… yo lo solucione pero no se si es lo mejor o lo mas apropiado… y es que desde la clase DAO Procesador yo puedo tambien consultar los Fabricantes y haci desde el controlador Procedador yo devuelvo los Fabricantes para llenar en la vista al comboBox… mi pregunta es… esta bien lo que hice?? hay otro modo de hacerlo o algun consejo para poder trabajar DemoGenericDomainTableModel usando MVC?

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Hola Jonathan, ante todo muchas gracias por leer el blog y dejar tu comentario.

      Para comenzar a responder tu pregunta, no necesariamente una Vista tiene que tener un único Controlador asociado, es decir, la relación no necesariamente es 1 a 1. Lo mismo ocurre entre Controlador y Modelo, donde de hecho un controlador puede llamar a más de un DAO, por ejemplo. Lo que es realmente importante es la división de responsabilidades.

      Dicho esto, en el caso que planteas creo que sería más apropiado tener un único controlador y que sea este el que se encargue de la lógica de negocios necesaria para mantener la integridad de los datos llamando a las clases DAO correspondientes.

      Con respecto a las relaciones entre tablas que se traducen en una relacion de entidades de dominio, se pueden aplicar diferentes enfoques. En el caso planteado, existe una relación 1 a varios entre Fabricante y Procesador, y existen al menos dos enfoques para trabajar esta relación:

      1. El DAO de Fabricante puede mapear los Porcesadores asociados al mismo y devolverlos como una lista contenida en el objeto Fabricante.
      2. El Controlador puede llamar a ambas clases DAO para obtener un Fabricante y sus Procesadores asociados.

      Ahora, lo que no es semánticamente correcto es que el DAO de Procesadores devuelva una lista de Fabricantes, dado que la responsabilidad de un DAO debería limitarse a la entidad a la que está manejando. Si necesito una lista de Fabricantes, la misma debe ser provista por el DAO correspondiente.

      Espero haber respondido tu consulta y aguardo tus comentarios.

      Me gusta

      Responder
  8. JOSE DE PAZ

    Hola,

    Excelente aporte, es otra forma de implementar el patrón MVC.

    Tengo la siguientes preguntas:

    1. ¿Para qué se envía por parámetro del controlador al constructor de la vista?
    2. ¿Qué puedo hacer con el controlador que recibo por parámetro en la vista?

    Saludos

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Hola José, muchas gracias por leer el blog y dejar tu comentario. Respondiendo a tus preguntas:

      1. En el ejemplo el controlador se pasa como parámetro al construir la Vista porque la interfaz no define un método para setearlo, y esto obedece en parte a lo que explicaba en un comentario anterior y es que no neceseriamente existe una relación 1 a 1 entre vista y controlador. Por ende considero más apropiado pasar el o los controladores por constructor para que la vista tenga una referencia de los mismos.

      2. En todos los casos de uso planteados, la Vista se comunica con el Modelo de datos subyacente siempre a través del Controlador, nunca en forma directa. Y esto es así para abstraer el modelo de datos de su representación y para poder concentrar las reglas de negocio en la capa de controlador, aislando este aspecto tanto de la vista como del modelo mismo.

      Espero haber respondido a tus consultas y espero tu respuesta. Saludos!

      Me gusta

      Responder
  9. Daniex

    Solo una duda.
    Donde expones la clase “IFabricantesController”?
    En ningún momento la identificas, muestras o al menos su estructura que es un punto importante (Es el controlador) no se ve en ningún sitio. Saludos

    Me gusta

    Responder
    1. Delcio Amarillo Autor de la entrada

      Hola Daniex, ante todo gracias por leer el blog y dejar tu comentario. La interfaz IFabricantesController se detalla en la entrada anterior del Bloc (hay un link al principio de esta entrada). Saludos!

      Me gusta

      Responder

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s