El patrón MVC y SWING

Uno de los patrones de diseño que me parece más difícil de entender en “la vida de SWING real” es el patrón MVC. He pasado por algunas de las publicaciones en este sitio que tratan sobre el patrón, pero todavía no creo tener una comprensión clara de cómo aprovechar el patrón en mi aplicación (Java SWING).

Digamos que tengo un JFrame que contiene una tabla, un par de campos de texto y algunos botones. Probablemente usaría un TableModel para “puentear” la JTable con un modelo de datos subyacente. Sin embargo, todas las funciones responsables de borrar campos, validar campos, bloquear campos junto con acciones de botones generalmente irían directamente al JFrame. Sin embargo, ¿eso no combina el Controlador y la Vista del patrón?

Por lo que puedo ver, logro obtener el patrón MVC “correctamente” implementado cuando miro la JTable (y el modelo), pero las cosas se ponen turbias cuando veo todo el JFrame como un todo.

Realmente me gustaría escuchar cómo otros hacen con respecto a esto. ¿Cómo se hace cuando necesita mostrar una tabla, un par de campos y algunos botones a un usuario (usando el patrón MVC)?

Un libro que recomendaría para MVC en swing sería “Head First Design Patterns” de Freeman y Freeman. Tienen una explicación muy completa de MVC.

Breve resumen

  1. Tú eres el usuario: interactúas con la vista. La vista es su ventana al modelo. Cuando haces algo con la vista (como hacer clic en el botón Reproducir), la vista le dice al controlador lo que hiciste. El trabajo del controlador es manejar eso.

  2. El controlador le pide al modelo que cambie su estado. El controlador toma tus acciones y las interpreta. Si hace clic en un botón, es tarea del controlador averiguar qué significa eso y cómo se debe manipular el modelo en función de esa acción.

  3. El controlador también puede pedirle a la vista que cambie. Cuando el controlador recibe una acción de la vista, puede necesitar decirle a la vista que cambie como resultado. Por ejemplo, el controlador podría habilitar o deshabilitar ciertos botones o elementos de menú en la interfaz.

  4. El modelo notifica a la vista cuando su estado ha cambiado. Cuando algo cambia en el modelo, basado en alguna acción que tomó (como hacer clic en un botón) u otro cambio interno (como la siguiente canción en la lista de reproducción ha comenzado), el modelo notifica a la vista que su estado ha cambiado.

  5. La vista le pregunta al modelo por el estado. La vista obtiene el estado que muestra directamente desde el modelo. Por ejemplo, cuando el modelo notifica a la vista que una nueva canción ha comenzado a reproducirse, la vista solicita el nombre de la canción del modelo y la muestra. La vista también podría solicitar al estado el modelo como resultado de que el controlador solicite algún cambio en la vista.

enter image description here Fuente (En caso de que se esté preguntando qué es un “controlador cremoso”, piense en una galleta Oreo, con el controlador como el centro cremoso, la vista como la galleta superior y el modelo como la galleta inferior).

¡Um, en caso de que estés interesado, puedes descargar una canción bastante entretenida sobre el patrón MVC desde aquí !

Un problema que puede enfrentar con la progtwigción de Swing implica amalgamar el subproceso SwingWorker y EventDispatch con el patrón MVC. Dependiendo de su progtwig, su vista o controlador podría tener que extender el SwingWorker y anular el método doInBackground() donde se coloca la lógica de recursos intensivos. Esto se puede fusionar fácilmente con el patrón MVC típico, y es típico de las aplicaciones Swing.

EDIT # 1 :

Además, es importante considerar MVC como una especie de compuesto de varios patrones. Por ejemplo, su modelo podría implementarse usando el patrón Observer (que requiere que la Vista se registre como un observador para el modelo) mientras su controlador puede usar el patrón de Estrategia.

EDIT # 2 :

También me gustaría responder específicamente a su pregunta. Debería mostrar sus botones de tabla, etc. en la Vista, lo que obviamente implementaría un ActionListener. En su método actionPerformed() , detecta el evento y lo envía a un método relacionado en el controlador (recuerde, la vista contiene una referencia al controlador). Entonces, cuando se hace clic en un botón, la vista detecta el evento, se envía al método del controlador, el controlador puede solicitar directamente a la vista que desactive el botón o algo. A continuación, el controlador interactuará con el modelo y lo modificará (que en su mayoría tendrá métodos getter y setter, y algunos otros para registrar y notificar a los observadores, etc.). Tan pronto como se modifique el modelo, se solicitará una actualización de los observadores registrados (esta será la vista en su caso). Por lo tanto, la vista se actualizará ahora.

No me gusta la idea de que la vista sea la que el modelo notifica cuando cambian sus datos. Yo delegaría esa funcionalidad al controlador. En ese caso, si cambia la lógica de la aplicación, no necesita interferir con el código de la vista. La tarea de la vista es solo para los componentes + diseño de las aplicaciones, nada más y nada menos. Layouting en swing ya es una tarea prolija, ¿por qué dejar que interfiera con la lógica de las aplicaciones?

Mi idea de MVC (con la que estoy trabajando actualmente, hasta ahora todo bien) es:

  1. La vista es la más tonta de las tres. No sabe nada sobre el controlador y el modelo. Su preocupación es solo la prótesis y el diseño de los componentes oscilantes.
  2. El modelo también es tonto, pero no tan tonto como la vista. Realiza las siguientes funcionalidades.
    • a. cuando el controlador llama a uno de sus iniciadores, enviará una notificación a sus oyentes / observadores (como dije, transferiría este rol al controlador). Prefiero SwingPropertyChangeSupport para lograr esto ya que ya está optimizado para este propósito.
    • segundo. funcionalidad de interacción con la base de datos
  3. Un controlador muy inteligente. Conoce muy bien la vista y el modelo. El controlador tiene dos funcionalidades:
    • a. Define la acción que la vista ejecutará cuando el usuario interactúe con ella.
    • segundo. Escucha el modelo. Al igual que lo que dije, cuando se llama al colocador del modelo, el modelo enviará una notificación al controlador. El trabajo del controlador es interpretar esta notificación. Puede necesitar reflejar el cambio en la vista.

Muestra de código

La vista :

Como dije, crear la vista ya es detallado, así que solo crea tu propia implementación 🙂

 interface View{ JTextField getTxtFirstName(); JTextField getTxtLastName(); JTextField getTxtAddress(); } 

Es ideal para interactuar con los tres para propósitos de prueba. Solo proporcioné mi implementación de Modelo y Controlador.

El modelo :

 public class MyImplementationOfModel implements Model{ ... private SwingPropertyChangeSupport propChangeFirer; private String address; private String firstName; private String lastName; public MyImplementationOfModel() { propChangeFirer = new SwingPropertyChangeSupport(this); } public void addListener(PropertyChangeListener prop) { propChangeFirer.addPropertyChangeListener(prop); } public void setAddress(String address){ String oldVal = this.address; this.address = address; //after executing this, the controller will be notified that the new address has been set. Its then the controller's //task to decide what to do when the address in the model has changed. Ideally, the controller will update the view about this propChangeFirer.firePropertyChange("address", oldVal, address); } ... //some other setters for other properties & code for database interaction ... } 

El controlador :

 public class MyImplementationOfController implements PropertyChangeListener, Controller{ private View view; private Model model; public MyImplementationOfController(View view, Model model){ this.view = view; this.model = model; //register the controller as the listener of the model this.model.addListener(this); setUpViewEvents(); } //code for setting the actions to be performed when the user interacts to the view. private void setUpViewEvents(){ view.getBtnClear().setAction(new AbstractAction("Clear") { @Override public void actionPerformed(ActionEvent arg0) { model.setFirstName(""); model.setLastName(""); model.setAddress(""); } }); view.getBtnSave().setAction(new AbstractAction("Save") { @Override public void actionPerformed(ActionEvent arg0) { ... //validate etc. ... model.setFirstName(view.getTxtFName().getText()); model.setLastName(view.getTxtLName().getText()); model.setAddress(view.getTxtAddress().getText()); model.save(); } }); } public void propertyChange(PropertyChangeEvent evt){ String propName = evt.getPropertyName(); Object newVal = evt.getNewValue(); if("address".equalsIgnoreCase(propName)){ view.getTxtAddress().setText((String)newVal); } //else if property (name) that fired the change event is first name property //else if property (name) that fired the change event is last name property } } 

The Main, donde está configurado el MVC:

 public class Main{ public static void main(String[] args){ View view = new YourImplementationOfView(); Model model = new MyImplementationOfModel(); ... //create jframe //frame.add(view.getUI()); ... //make sure the view and model is fully initialized before letting the controller control them. Controller controller = new MyImplementationOfController(view, model); ... //frame.setVisible(true); ... } } 

El patrón MVC es un modelo de cómo se puede estructurar una interfaz de usuario. Por lo tanto, define los 3 elementos Model, View, Controller:

  • El modelo A modelo es una abstracción de algo que se presenta al usuario. En swing tienes una diferenciación de modelos de GUI y modelos de datos. Los modelos de GUI resumen el estado de un componente ui como ButtonModel . Los modelos de datos resumen datos estructurados que la interfaz de usuario presenta al usuario como TableModel .
  • Ver La vista es un componente de la interfaz de usuario que se encarga de presentar los datos al usuario. Por lo tanto, es responsable de todos los problemas de la interfaz de usuario como el diseño, el dibujo, etc. Ej. JTable .
  • Controlador Un controlador encapsula el código de la aplicación que se ejecuta para una interacción del usuario (movimiento del mouse, clic del mouse, pulsación de tecla, etc.). Los controladores pueden necesitar información para su ejecución y producen resultados. Leen sus comentarios de los modelos y actualizan los modelos como resultado de la ejecución. También pueden reestructurar la interfaz de usuario (por ejemplo, reemplazar componentes ui o mostrar una vista nueva completa). Sin embargo, no deben conocer los componentes de la interfaz de usuario, ya que puede encapsular la reestructuración en una interfaz separada que el controlador solo invoca. En swing, un controlador normalmente se implementa mediante un ActionListener o acción .

Ejemplo

  • Rojo = modelo
  • Verde = vista
  • Azul = controlador

enter image description here

Cuando se hace clic en el botón, invoca el ActionListener . ActionListener solo depende de otros modelos. Utiliza algunos modelos como entrada y otros como resultado o salida. Es como argumentos de método y valores de retorno. Los modelos notifican a la interfaz de usuario cuando se actualizan. Por lo tanto, no es necesario que la lógica del controlador conozca el componente ui. Los objetos modelo no conocen la interfaz de usuario. La notificación se realiza mediante un patrón de observador. Por lo tanto, los objetos modelo solo saben que hay alguien que quiere recibir notificaciones si el modelo cambia.

En java swing hay algunos componentes que implementan un modelo y un controlador también. Por ejemplo, javax.swing.Action . Implementa un modelo ui (propiedades: habilitación, icono pequeño, nombre, etc.) y es un controlador porque amplía ActionListener .

Una explicación detallada, aplicación de ejemplo y código fuente : https://www.link-intersystems.com/blog/2013/07/20/the-mvc-pattern-implemented-with-java-swing/ .

Conceptos básicos de MVC en menos de 240 líneas:

 public class Main { public static void main(String[] args) { JFrame mainFrame = new JFrame("MVC example"); mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); mainFrame.setSize(640, 300); mainFrame.setLocationRelativeTo(null); PersonService personService = new PersonServiceMock(); DefaultListModel searchResultListModel = new DefaultListModel(); DefaultListSelectionModel searchResultSelectionModel = new DefaultListSelectionModel(); searchResultSelectionModel .setSelectionMode(ListSelectionModel.SINGLE_SELECTION); Document searchInput = new PlainDocument(); PersonDetailsAction personDetailsAction = new PersonDetailsAction( searchResultSelectionModel, searchResultListModel); personDetailsAction.putValue(Action.NAME, "Person Details"); Action searchPersonAction = new SearchPersonAction(searchInput, searchResultListModel, personService); searchPersonAction.putValue(Action.NAME, "Search"); Container contentPane = mainFrame.getContentPane(); JPanel searchInputPanel = new JPanel(); searchInputPanel.setLayout(new BorderLayout()); JTextField searchField = new JTextField(searchInput, null, 0); searchInputPanel.add(searchField, BorderLayout.CENTER); searchField.addActionListener(searchPersonAction); JButton searchButton = new JButton(searchPersonAction); searchInputPanel.add(searchButton, BorderLayout.EAST); JList searchResultList = new JList(); searchResultList.setModel(searchResultListModel); searchResultList.setSelectionModel(searchResultSelectionModel); JPanel searchResultPanel = new JPanel(); searchResultPanel.setLayout(new BorderLayout()); JScrollPane scrollableSearchResult = new JScrollPane(searchResultList); searchResultPanel.add(scrollableSearchResult, BorderLayout.CENTER); JPanel selectionOptionsPanel = new JPanel(); JButton showPersonDetailsButton = new JButton(personDetailsAction); selectionOptionsPanel.add(showPersonDetailsButton); contentPane.add(searchInputPanel, BorderLayout.NORTH); contentPane.add(searchResultPanel, BorderLayout.CENTER); contentPane.add(selectionOptionsPanel, BorderLayout.SOUTH); mainFrame.setVisible(true); } } class PersonDetailsAction extends AbstractAction { private static final long serialVersionUID = -8816163868526676625L; private ListSelectionModel personSelectionModel; private DefaultListModel personListModel; public PersonDetailsAction(ListSelectionModel personSelectionModel, DefaultListModel personListModel) { boolean unsupportedSelectionMode = personSelectionModel .getSelectionMode() != ListSelectionModel.SINGLE_SELECTION; if (unsupportedSelectionMode) { throw new IllegalArgumentException( "PersonDetailAction can only handle single list selections. " + "Please set the list selection mode to ListSelectionModel.SINGLE_SELECTION"); } this.personSelectionModel = personSelectionModel; this.personListModel = personListModel; personSelectionModel .addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { ListSelectionModel listSelectionModel = (ListSelectionModel) e .getSource(); updateEnablement(listSelectionModel); } }); updateEnablement(personSelectionModel); } public void actionPerformed(ActionEvent e) { int selectionIndex = personSelectionModel.getMinSelectionIndex(); PersonElementModel personElementModel = (PersonElementModel) personListModel .get(selectionIndex); Person person = personElementModel.getPerson(); String personDetials = createPersonDetails(person); JOptionPane.showMessageDialog(null, personDetials); } private String createPersonDetails(Person person) { return person.getId() + ": " + person.getFirstName() + " " + person.getLastName(); } private void updateEnablement(ListSelectionModel listSelectionModel) { boolean emptySelection = listSelectionModel.isSelectionEmpty(); setEnabled(!emptySelection); } } class SearchPersonAction extends AbstractAction { private static final long serialVersionUID = 4083406832930707444L; private Document searchInput; private DefaultListModel searchResult; private PersonService personService; public SearchPersonAction(Document searchInput, DefaultListModel searchResult, PersonService personService) { this.searchInput = searchInput; this.searchResult = searchResult; this.personService = personService; } public void actionPerformed(ActionEvent e) { String searchString = getSearchString(); List matchedPersons = personService.searchPersons(searchString); searchResult.clear(); for (Person person : matchedPersons) { Object elementModel = new PersonElementModel(person); searchResult.addElement(elementModel); } } private String getSearchString() { try { return searchInput.getText(0, searchInput.getLength()); } catch (BadLocationException e) { return null; } } } class PersonElementModel { private Person person; public PersonElementModel(Person person) { this.person = person; } public Person getPerson() { return person; } @Override public String toString() { return person.getFirstName() + ", " + person.getLastName(); } } interface PersonService { List searchPersons(String searchString); } class Person { private int id; private String firstName; private String lastName; public Person(int id, String firstName, String lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } public int getId() { return id; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } } class PersonServiceMock implements PersonService { private List personDB; public PersonServiceMock() { personDB = new ArrayList(); personDB.add(new Person(1, "Graham", "Parrish")); personDB.add(new Person(2, "Daniel", "Hendrix")); personDB.add(new Person(3, "Rachel", "Holman")); personDB.add(new Person(4, "Sarah", "Todd")); personDB.add(new Person(5, "Talon", "Wolf")); personDB.add(new Person(6, "Josephine", "Dunn")); personDB.add(new Person(7, "Benjamin", "Hebert")); personDB.add(new Person(8, "Lacota", "Browning ")); personDB.add(new Person(9, "Sydney", "Ayers")); personDB.add(new Person(10, "Dustin", "Stephens")); personDB.add(new Person(11, "Cara", "Moss")); personDB.add(new Person(12, "Teegan", "Dillard")); personDB.add(new Person(13, "Dai", "Yates")); personDB.add(new Person(14, "Nora", "Garza")); } public List searchPersons(String searchString) { List matches = new ArrayList(); if (searchString == null) { return matches; } for (Person person : personDB) { if (person.getFirstName().contains(searchString) || person.getLastName().contains(searchString)) { matches.add(person); } } return matches; } } 

Puede crear un modelo en una clase de Java simple separada y un controlador en otro.

Entonces puedes tener componentes Swing encima de eso. JTable sería una de las vistas (y el modelo de tabla sería de facto parte de la vista; solo se traduciría del “modelo compartido” a JTable ).

Cada vez que se edita la tabla, su modelo de tabla le dice al “controlador principal” que actualice algo. Sin embargo, el controlador no debe saber nada sobre la mesa. Por lo tanto, la llamada debería verse más como: updateCustomer(customer, newValue) , no updateCustomer(row, column, newValue) .

Agregue una interfaz de escucha (observador) para el modelo compartido. Algunos componentes (por ejemplo, su mesa) podrían implementarlo directamente. Otro observador podría ser el controlador que coordina la disponibilidad del botón, etc.


Esa es una forma de hacerlo, pero por supuesto puede simplificarlo o ampliarlo si es excesivo para su caso de uso.

Puede fusionar el controlador con el modelo y tener las mismas actualizaciones de proceso de clase y mantener la disponibilidad de los componentes. Incluso puede hacer que el “modelo compartido” sea un modelo de tabla (aunque si no solo lo utiliza la tabla, recomendaría al menos proporcionar una API más amigable que no tenga fugas en las tablas)

Por otro lado, puede tener interfaces complejas para actualizaciones ( CustomerUpdateListener , OrderItemListener , OrderCancellationListener ) y un controlador dedicado (o mediador) solo para la coordinación de diferentes vistas.

Depende de qué tan complicado sea tu problema.

Para una separación adecuada, normalmente tendrías una clase de controlador a la que la clase Frame delegaría. Hay varias maneras de establecer las relaciones entre las clases: puede implementar un controlador y ampliarlo con su clase de vista principal, o utilizar una clase de controlador independiente que el Marco llama cuando ocurren los eventos. La vista normalmente recibiría eventos del controlador implementando una interfaz de escucha.

A veces, una o más partes del patrón MVC son triviales, o tan “delgadas” que agrega complejidad innecesaria para separarlas. Si su controlador está lleno de llamadas de una línea, tenerlo en una clase separada puede terminar ofuscando el comportamiento subyacente. Por ejemplo, si todos los eventos que está manejando están relacionados con un modelo de tabla y son operaciones simples de agregar y eliminar, puede optar por implementar todas las funciones de manipulación de tabla dentro de ese modelo (así como las devoluciones de llamada necesarias para mostrarlo en el JTable). No es cierto MVC, pero evita agregar complejidad donde no se necesita.

Independientemente de cómo lo implemente, recuerde utilizar JavaDoc en sus clases, métodos y paquetes para que los componentes y sus relaciones se describan correctamente.

He encontrado algunos artículos interesantes sobre la implementación de Patrones MVC, que podrían resolver su problema.

Si desarrolla un progtwig con una GUI , el patrón mvc casi está borroso.

Diseñar el modelo, la vista y el código del controlador es difícil, y normalmente no es solo una tarea de refactorización.

Sabes que lo tienes cuando tu código es reutilizable. Si ha implementado correctamente MVC, debería ser fácil implementar una TUI o una CLI o una RWD o un primer diseño móvil con la misma funcionalidad. Es fácil verlo hecho que hacerlo en realidad, más aún en un código existente.

De hecho, las interacciones entre el modelo, la vista y el controlador ocurren usando otros patrones de aislamiento (como observador o escucha)

Supongo que esta publicación lo explica en detalle, desde el patrón directo sin MVC (como lo harás en una Q & D ) hasta la implementación final reutilizable:

http://www.austintek.com/mvc/