Pattern MVC in Java

Man mano che i software diventano più complessi, la loro qualità rischia di calare. Per mantenerla alta si applicano i design pattern. Un Design Pattern è una soluzione generica che risolve un problema ricorrente; In pratica è un modo standard per scrivere determinati pezzi di codice. Quando si parla di interfacce grafiche si pensa al pattern MVC, usato molto nel web, oggi vediamo come si usa.

Model View Controller

Il pattern MVC (Model View Control) prevede la suddivisione di un'interfaccia grafica in tre parti:

  • Model, che contiene i dati e la logica dell'applicazione
  • View, che contiene rappresenta i dati
  • Controller, che in base all'input dell'utente aggiorna Model o View

Model View Controller

Separare il codice in tre parti distinte lo rende più pulito e comprensibile, ciò viene in aiuto quando dobbiamo sviluppare e debuggare applicazioni complesse.

Applichiamo il pattern MVC

Scriviamo un programma un po' più complesso dei soliti utilizzando il pattern MVC. Dovremo realizzare una app rubrica, che per ogni contatto mostrerà nome, cognome e numero di telefono. Inoltre dovrà essere possibile aggiungere, modificare e rimuovere persone. Dovrà inoltre essere possibile importare ed esportare la rubrica su file.

Il risultato finale sarà questo qui, il codice completo lo trovi su github gists.

App Rubrica

Facciamo un breve riepilogo dei requisiti software:

  • Aggiungere, modificare ed eliminare contatti
  • Ogni contatto ha nome, cognome e numero di telefono
  • Importare ed Esportare su file i contatti

Cominciamo a pensare in che modo organizzare i componenti dell'app, partiamo facendo un wireframe della UI, cioè un disegno molto semplice di come vogliamo sia la nostra applicazione graficamente.

Wireframe

Ora che abbiamo un' idea di come sarà la nostra app pensiamo a come definire le classi, partiamo dal view:

Struttura del View

  • L'app è contenuta in una finestra, quindi abbiamo una classe JFrame.
  • Abbiamo 5 JButton, tre che effettuano operazioni su un contatto e due che lavorano su file.
  • Abbiamo una JTable che mostra i contatti.
  • Premendo sul bottoni nuovo dovrà aprirsi un JDialog con delle JTextField, per inserire le informazioni del contatto. Lo stesso vale per il bottone modifica.
  • Premendo sul bottone rimuovi mi verrà chiesta la conferma prima di eliminare un contatto.
  • I bottoni importa ed esporta utilizzeranno un dialog JFileChooser per poter selezionare i file.
  • Se l'utente dovesse commettere un errore bisognerà avvisarlo con un JDialog.

Struttura del Model

  • Avremo sicuramente bisogno di una classe Contatto, con attributi nome, cognome e telefono.
  • Siccome nel view usiamo una JTable, nel model dovremo definire una TableModel (estendendo AbstractTableModel) contenente i dati della tabella, cioè i contatti.
  • Importare ed esportare la rubrica implica l'utilizzo di java.io per le operazioni su file.

Struttura del Controller

  • Il Controller dovrà implementare un ActionListener e registrarvi i 5 bottoni del View.
  • In base al bottone premuto il Controller mostrerà dei dialog e modificherà i dati della rubrica.

Diagramma delle classi

Dopo aver descritto la struttura della nostra app, realizziamo la gerarchia delle classi in UML:

UML

Il codice

Strutturiamo il codice in package, il risultato sarà il seguente:

📂
├── Controller/
│     └── Controller.java
├── Model/
│     ├── Contatto.java
│     ├── FileOps.java
│     └── Rubrica.java
├── View/
│     ├── ContattoDialog.java
│     └── Finestra.java
└── Main.java

View

Finestra.java

Finestra mostra un JFrame con i componenti dell'app.

package View;
import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
public class Finestra {
private JPanel panel1;
private JButton aggiungiButton;
private JButton modificaButton;
private JButton rimuoviButton;
private JTable tabellaRubrica;
private JButton importaButton;
private JButton esportaButton;
private JFrame frame;
public Finestra() {
frame = new JFrame();
frame.setContentPane(this.panel1);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
view raw Finestra.java hosted with ❤ by GitHub

ContattoDialog.java

ContattoDialog mostra un JDialog che permette di aggiungere o modificare un contatto.

package View;
import Controller.Controller;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class ContattoDialog extends JDialog {
private JPanel contentPane;
private JButton buttonOK;
private JButton buttonCancel;
private JTextField nomeField;
private JTextField telefonoField;
private JTextField cognomeField;
private boolean buttonOKPressed = false;
public ContattoDialog(JFrame f, String title) {
setTitle(title);
initialize();
setLocationRelativeTo(f);
setVisible(true);
}
public ContattoDialog(JFrame f, String title, String nome, String cognome, String telefono) {
setTitle(title);
nomeField.setText(nome);
cognomeField.setText(cognome);
telefonoField.setText(telefono);
initialize();
setLocationRelativeTo(f);
setVisible(true);
}

Model

Contatto.java

Contatto contiene le informazioni necessarie per gestire un contatto, implementiamo Serializable per poter salvare su file la classe. Il metodo toArray() verrà usato in Rubrica.

package Model;
import java.io.Serializable;
public class Contatto implements Serializable {
private String nome;
private String cognome;
private String telefono;
public Contatto(String nome, String cognome, String telefono) {
this.nome = nome;
this.cognome = cognome;
this.telefono = telefono;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getCognome() {
return cognome;
}
public void setCognome(String cognome) {
this.cognome = cognome;
}
public String getTelefono() {
return telefono;
}
public void setTelefono(String telefono) {
this.telefono = telefono;
}
public String[] toArray(){
return new String[] {nome, cognome, telefono};
}
}
view raw Contatto.java hosted with ❤ by GitHub

Rubrica.java

Per poter rappresentare i dati sulla tabella, Rubrica estende AbstractTableModel, dovremo quindi implementare 4 funzioni:

  • getRowCount() dovrà ritornare il numero di contatti nella rubrica.
  • getColumnCount() ritornerà il numero di colonne, nel nostro caso 3, nome, cognome e telefono.
  • getColumnName(int) ritornerà il nome di una determinata colonna.
  • getValueAt(int, int) dovrà ritornare nome, cognome o telefono di un contatto.

package Model;
import javax.swing.table.AbstractTableModel;
import java.io.File;
import java.util.Vector;
public class Rubrica extends AbstractTableModel {
private Vector<Contatto> rubrica;
private String[] columnNames = {"Nome", "Cognome", "Telefono"};
public Rubrica(){
this.rubrica = new Vector<Contatto>();
}
public Rubrica(Vector<Contatto> rubrica){
this.rubrica = rubrica;
}
@Override
public int getRowCount() {
return rubrica.size();
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return rubrica.get(rowIndex).toArray()[columnIndex];
}
view raw Rubrica.java hosted with ❤ by GitHub

FileOps.java

FileOps offrirà due metodi statici per leggere e scrivere su file un Vector di tipo Contatto.

package Model;
import java.io.*;
import java.util.Vector;
public class FileOps {
public static void writeAll(File f, Vector<Contatto> rubrica) throws IOException {
FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(fos);
for(Contatto p : rubrica){
oos.writeObject(p);
oos.reset();
}
}
public static Vector<Contatto> readAll(File f) throws ClassNotFoundException, IOException{
Vector<Contatto> rubrica = new Vector<Contatto>();
FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(fis);
try{
while(fis.available() != -1){
Contatto p = (Contatto) ois.readObject();
rubrica.add(p);
}
}
catch(EOFException e){}
return rubrica;
}
}
view raw FileOps.java hosted with ❤ by GitHub

Controller

Controller.java

Controller implementa ActionListener, ascolta il click dei bottoni di Finestra e in base a quello che viene premuto mostra un JDialog o modifica Rubrica

package Controller;
import Model.*;
import View.*;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Controller implements ActionListener {
private Finestra finestra;
private Rubrica rubrica;
public Controller(Finestra f, Rubrica r){
this.finestra = f;
this.rubrica = r;
//Aggiungo l'action Listener ai vari bottoni dell' app
finestra.getAggiungiButton().addActionListener(this);
finestra.getModificaButton().addActionListener(this);
finestra.getRimuoviButton().addActionListener(this);
finestra.getImportaButton().addActionListener(this);
finestra.getEsportaButton().addActionListener(this);
finestra.getTabellaRubrica().setModel(rubrica);
}
@Override
public void actionPerformed(ActionEvent e) {
String errore = "";
// Filtro il componente che ha chiamato actionPerformed(), in base a questo eseguo azioni diverse
if (e.getSource() == finestra.getAggiungiButton()) errore = aggiungiPersona();
else if (e.getSource() == finestra.getModificaButton()) errore = modificaPersona();
else if (e.getSource() == finestra.getRimuoviButton()) errore = rimuoviPersona();
else if(e.getSource() == finestra.getImportaButton()) errore = importaFile();
else if(e.getSource() == finestra.getEsportaButton()) errore = esportaFile();
// Se la stringa errore non è vuota mostro un dialog con l'errore
if(!errore.isEmpty()) JOptionPane.showMessageDialog(finestra.getFrame(), errore, "Avviso", JOptionPane.WARNING_MESSAGE);
}
view raw Controller.java hosted with ❤ by GitHub

© Nicola Bovolato 2020