Mangiare Senza Glutine disponibile su App Store

Per altre informazioni scrivi a fabriziocaldarelli@negusweb.it

Model View Controller: esempio pratico in Java

Da Programmazione Software.


Introduzione

La componente di visualizzazione è tanto importante quanto la gestione delle informazioni, ed è bene quindi trovare un costrutto semplice, organizzato, facilmente estendibile e riutizzabile da poter utilizzare in ogni visualizzazione.

Dedicare, quindi, tempo sufficiente alla progettazione solida di un sistema per scrivere applicazioni in cui parte grafica e informazioni siano NETTAMENTE separate ed indipendenti ma FACILMENTE comunicanti è ormai un requisito.

L'MCV (acronimo di model-controller-view) è un design pattern (stile di progettazione) altamente efficiente, che pone le sue basi sulle necessità di generalizzazione e scalabilità che ogni software ha intrinsecamente. Vedremo come, affrontando un semplice esempio, arriveremo a realizzare l'MCV senza accorgercene.


Il nostro semplice programma per capire MVC

Affronteremo la realizzazione di un semplice software, che ci porterà ad utilizzare il design pattern MVC (model controller view) senza accorgercene e soprattutto a capire perchè sia il riferimento per ogni applicazione in cui la visualizzazione dati riveste un ruolo particolarmente importante. Per questo tipo di applicazione utilizzeremo le Swing (per cui interfaccia grafica) in pseudo codice, ovvero utilizzando metodi e inizializzazioni "inventati" che vogliono essere una guida alla comprensione del codice, piuttosto che fornire codice eseguibile.

Prima di passare al testo del programma, vi ricordo quali sono i "comandamenti" che terrò sempre in considerazione per lo sviluppo del software (per ora sono solo tre):

  1. Il programma dev'essere ben organizzato e chiaro (coerente e modulare);
  2. Il programma dev'essere facilmente scalabile;
  3. Le macro sezioni (componenti) in cui il programma è diviso (la modularità del primo "comandamento") devono essere autonome e indipendenti (riusabili).

Più in particolare, nella programmazione ad oggetti, ogni "oggetto" (componente) dev'essere a sè stante e la sua vita indipendente da qualsiasi altro oggetto; cioè il componente che noi creaiamo deve poter funzionare anche se avviato singolarmente: questo garantisce la riusabilità dei nostri componenti.

Benissimo, dopo tutte queste premesse ecco il programma da sviluppare per renderci conto di come sia arriva a reinventare il model-controller-view: vogliamo realizzazione un software che legga da db delle informazioni e che le visualizzi in una tabella appartenente ad un frame.

Nello sviluppo non affronterò minimamente la parte relativa alla lettura dati dal DB e alla scrittura dati sulla tabella perchè non rientra negli intenti di questo articolo, ma userò degli pseudo metodi che facciano capire semplicemente quali sono le azioni da svolgere (e quindi da implementare).

Cosi', ad una prima occhiata, il programma non è affatto complesso e potrebbe essere risolto inserendo in una classe un JFrame ed una JTable.

Codice - Prima semplice soluzione

import javax.swing.*;
 
class soluzione_1
{
     public static void main(String[] args) 
     {
          // Dichiaro la variabile che farà riferimento alla mia finestra
          JFrame f;
          JTable t;
          String[][] dati;
 
          // Inizializzo la variabile f
          f=new JFrame();
 
          // Inizializzo la tabella t
          t=new JTable();
 
          // Leggo i dati dal db
          // La variabile dati è una matrice in cui le righe sono i singoli record
          dati=leggiDatiDaDB();
 
          // Riempio la tabella: suppongo esistano quattro metodi:
          // pulisciTabella(): cancella tutto il contenuto della tabella
          // nuovaRiga(): crea una nuova riga nella tabella
          // ultimaRiga(): restituisce l'ultima riga creata, che è proprio quella appena creata
          // add(): aggiunge una cella nella riga corrente con il contenuto passato nel parametro
          t.pulisciTabella(); 
          for (int i=0;i<dati.length;i++)
          {
               t.nuovaRiga();
               for (int k=0;k<dati[i].length;k++)
               t.ultimaRiga().add(dati[i][k]);
          }
 
          // Inserisco la tabella nel frame f
          f.getContentPane().add(t);
     }
}


Quello che avete appena letto è ovviamente uno pseudo codice java-oriented e non vuole essere assolutamente eseguibile, ma semplicemente guida per capire come si arriva ad una modellizzazione del problema.

Cominciamo ad analizzare le caratterische di questo programma.

1. Coerenza e modularità: (ok)

La nostra classe soluzione_1 non estende nessun'altra classe e quindi non ha un tipo base (la considero "generica"). Quindi possiamo mettere al suo interno quello che vogliamo e cioè oggetti del tipo che vogliamo.

N.B. Se la classe soluzione_1 avesse esteso, per esempio, la classe JFrame, per quanto mi riguarda sarebbe stato inaccettable che al suo interno ci fossero state delle operazioni con DB, dichiarazione di un oggetto JTable, ecc.., perchè il suo contenuto sarebbe stato "incoerente" con il suo tipo base.

Per me estendere una classe significa mantenere la coerenza con il tipo base (una classe che estende una JFrame non può/deve lavorare con un DB, per esempio) aggiungedone funzionalità.

Per quanto riguarda la modularità, avremmo potuto includere i cicli for in una funzione, ma questo non comporta molta fatica farlo in un secondo momento; in questo momento avrebbe soltanto complicato inutilmente il programma.

Diciamo, quindi, che per la coerenza e la modularità non ci sono problemi.

2. Scalabilità del programma: (no)

Il programma è poco scalabile sia da un punto di vista della gestione delle informazioni che per la visualizzazione.

Cominciamo dalla gestione delle informazioni: ogni riga del DB ha sicuramente un campo identificatore univoco che si autoincrementa per rintracciare in modo univoco ciascun record. Ci piacerebbe, quindi, in primo luogo avere una struttura per i dati in grado di selezionare le informazioni di ciascun record caricato nella variabile dati attraverso questo identificatore univoco. La seconda esigenza potrebbe essere avere la variabile dati "strutturata", ovvero divisa in campi facilmente rintracciabili attraverso lo stesso nome dato al campo del db.

E' da rivedere, quindi, la gestione delle informazioni.

La mancanza di scalabilità della parte grafica è dovuta al fatto che la JTable di base potrebbe essere insufficiente per le nostre necessità. Ma questo tipo di problema è facilmente superabile, in qualsiasi momento del programma: basta dichiarare una nuova classe che estende la JTable ed il problema è risolto. A questo punto è meglio definire da principio una classe che estende una JTable, piuttosto che la JTable stessa. C'e' anche un altro motivo per cui è importante fin da subito portare il componente visivo su un'altra classe, separata dalla principale: supponiamo che il programma si espanda e che in futuro oltre a poter inserire righe nella tabella, sia in grado anche di cancellarle e modificarle. Quindi avremmo utilizzato questi pseudo metodi: posizionatiRiga(numeroRiga).setCampo(posizioneCampo, valoreCampo) e cancellaRiga(numeroRiga), che supponiamo essere nativi dello specifico componente (JTable, in questo caso).

Bene. Abbiamo complemente delegato al programma principale la capacità di lavorare direttamente sul componente, cioè lo abbiamo programmato a "basso livello" utilizzando i suoi metodi nativi (che, molto probabilemente, esisteranno soltanto nel componente che stiamo utilizzando).

Cosa succede se, per caso, non vogliamo (= il nostro cliente non vuole) più una JTable ma vuole una JList ?

Dobbiamo spulciare tutto il programma alla ricerca dei metodi, appunto, che si occupano dell'inserimento, modifica e cancellazione dei dati nella JTable (e magari ci dimentichiamo del metodo per conoscere la riga correntemente selezionata, perchè lo abbiamo utilizzato una sola volta!). Operazione sicuramente lunga, ma peggio ancora pericolosa e sicuramente portatrice di bug, se il programma è abbastanza esteso. Numericamente parlando, dovremmo fare tante modifiche quante sono le chiamate ai metodi nativi del componente. Basta armarsi di santa pazienza e tanta attenzione ed il gioco del trova/sostituisci è fatto. Avrete capito che utlizzare i metodi nativi del componente è comodo e veloce, ma sicuramente poco scalabile. Infatti la scalabilità è una deficienza intrinseca nel momento in cui utilizziamo i metodi nativi del componente: infatti quei metodi saranno esclusivamente di quel componente ed il codice di inserimento/modifica/cancellazione dati sarà difficilmente portabile su un altro componente.

E se, invece, avessimo creato una classe che estendeva la JTable, sarebbe cambiato qualcosa? Sicuramente sì. Delegando completamente la scrittura sul componente grafico direttamente al programma principale, abbiamo dovuto utilizzare i metodi nativi del componente, che probabilmente apparterranno soltanto a quel componente specifico e che non saranno sicuramente funzionanti (probabilmente neanche esistono) su altri componenti. Se invece nella nostra classe che estende la JTable avessi creato dei metodi: posizionatiRiga(numeroRiga) e scriviVettoreSuRigaCorrente(vettoreDati) eventuali modifiche sono sicuramente più agevoli. Semplicemente perchè posizionatiRiga(...) e scriviVettoreSuRigaCorrente(...) sono metodi scritti da noi che, cambiando il tipo base della nostra classe del componente grafico, potremmo portare con noi. Quindi, un giorno, passare da una JTable ad una JList, per esempio, comporterebbe queste modifiche:

* Modificare il tipo base della classe del componente (da JTable a JList)
* Cambiare il contenuto del metodo posizionatiRiga(...)
* Cambiare il contenuto del metodo scriviVettoreSuRigaCorrente(...)

Cioè abbiamo dovuto cambiare il contenuto di soli tre metodi per passare da un componente all'altro! Questo discorso è tanto più valido quanto più il programma è esteso, ma comunque usare una classe che estende il tipo base del componente piuttosto che usare la classe che identifica il componente è, secondo me, sempre la soluzione giusta.

3. Riusabilità: (no)

La riusabilità ovviamente non c'e', perchè il componente su cui lavoriamo (la JTable) fa parte del programma principale. Un motivo in più per creare una classe che estende il componente da utilizzare, in questo caso la JTable.

In definitiva dobbiamo lavorare sulla scalabilità del programma sia per quanto riguarda la gestione dei dati, sia per quanto riguarda la visualizzazione del componente grafico, tenendo sempre d'occhio la coerenza delle classi che andremo a creare. La riusabilità verrà fuori buona parte in automatico nel momento in cui avremo implementato la scalabilità del componente grafico. Il prossimo passo, quindi, sarà quello di portare la gestione dei dati (model) e la visualizzazione (view) del componente grafico su classi separate dal programma principale (controller), il quale si occuperà di gestirle e coordinarle. Come vedete, la programmazione MCV (model-controller-view) sta prendendo vita solo con semplici lungimiranti osservazioni sul programma.


Scalabilità del programma: gestione delle informazioni

Il primo passo per aumentare la scalabilità del programma, ovvero la facilità con cui può essere modificato e/o aggiornato il software in futuro, è quello di separare i vari strati di gestione delle informazioni. Possiamo considerare come strato "più basso" quello che gestisce, appunto, il contatto con le informazioni, ovvero il database. Gli strumenti per interagire con i database restituiscono, solitamente, degli oggetti particolari che vengono utilizzati esclusivamente nella comunicazione con il database stesso. A questo punto, bisogna creare uno strato intermedio tra il database e l'applicazione stessa, in modo che le informazioni siano raccolte in modo pulito e preciso. Questo fondamentalmente per due motivi:

1. dobbiamo pensare all'applicazione come ad un datore di lavoro al quale non interessa come viene svolto il lavoro, l'importante è che venga svolto. Per cui all'applicazione, in generale, non dovrebbe interessare come e dove le informazioni vengono memorizzate (su db, su file, ecc.), ma semplicemente sapere come reperirle e memorizzarle. Questo garantisce che, se vogliamo cambiare supporto di memorizzazione, lo possiamo fare in qualsiasi momento, l'importante è implementare le funzioni che l'applicazione usa per richiamare e memorizzare i dati.
2. non vogliamo inserire nell'applicazione l'utilizzo di oggetti Recordset, appartenenti alla gestione del database, sempre perchè all'applicazione non deve interessare come vengono reperite le informazioni. Questo infatti richiederebbe all'applicazione la scrittura di metodi per la gestione di questi oggetti, legando fortemente la gestione delle informazioni all'utilizzo del db (togliendo scalabilità, perchè se volessimo passare alla memorizzazione delle informazioni sui file, questo richiederebbe come minimo la riscrittura di tali metodi). Tornando al paragone con il datore di lavoro, se al dipendente viene richiesto di ritirare dei soldi dalla cassetta di sicurezza, il committente non vuole avere come risultato la chiave della cassetta, ma il denaro!

Potreste obiettare dicendo che, comunque, le ipotesi che facciamo sono esagerate (il passare da un supporto db ad un supporto file, per esempio), ma la scalabilità è una forma mentis che si costruisce tenendo il software pulito, ordinato e modulare. Infatti, il pensare di poter cambiare supporto da un momento all'altro senza problemi è come dire creare il software modulare, al quale si possono aggiungere e togliere moduli (e quindi funzionalità) in qualsiasi momento. Imparare fin da subito a programmare in questo modo vi aiuterà moltissimo. L'idea è di creare un package (una cartella, in definitiva), in cui mettere tutte le classi che gestiscono le informazioni provenienti da un qualsiasi supporto di memorizzazione.

Un esempio è il seguente:

package informazioni;
 
class gestione_informazioni_tabella_1
{
	Vector vecDati;
 
	public gestione_informazioni_tabella_1()
	{
		vecDati=new Vector();
	}
 
	public void aggiungi_dati(informazioni_tabella_1 clsInput)
	{
		vecDati.add(clsInput);
	}
 
	public informazioni_tabella_1 recupera_dati(int posizione)
	{
		return (informazioni_tabella_1) (vecDati.get(posizione));
	}
 
	public int numero_elementi()
	{
		return vetDati.size();
	}
}	
 
 
class informazioni_tabella_1
{
	int id;
	StringBuffer nome;
	StringBuffer cognome;
	StringBuffer telefono;
	StringBuffer indirizzo;
	StringBuffer cellulare;
	StringBuffer email;
}


Osservazione: avremmo potuto inserire la classe informazioni_tabella_1 anche all'interno di gestione_informazioni_tabella_1 ma è sconsigliabile per almeno due motivi:

1. Avremmo mischiato le informazioni con la loro gestione (metodi di inserimento, cancellazione, ...) e quindi se volessimo scandire tutto il vettore delle informazion avremmo avuto come riferimento un oggetto che non conteneva solo le informazione ma anche i metodi di inserimento, cancellazione, ecc., cosa non pulita;
2. Il database lavora soltanto sui campi della classe e quindi non sarebbe pulito lavorare con una classe con i campi del db che contiene anche metodi di inserimento, modifica, ecc.

Questi approfondimenti acquistano maggior valore nel caso in cui si faccia della classe gestione_informazioni_tabella_1 una classe singleton: infatti in questo caso sono più facilmente monitorabili le azioni svolte indirettamente sul vettore contenente i dati.

Altra osservazione è che, volendo, si sarebbe potuto accedere alle variabili della classe informazioni_tabella_1 non direttamente, ma attraverso dei metodi: questo sia in lettura che in scrittura.


Passiamo ora ad analizzare come dev'essere la classe che gestisce l'interazione con il database.

package database;
 
import java.sql.*;
import informazioni.*;
 
class clsDatabase
{
	boolean blDbAperto;
	Statement stmt;
 
	public clsDatabase()
	{
		blDbAperto=false;
	}
 
	public ApriDb()
	{
		blDbAperto=true;
		Set stmt ...
	}
 
	public ChiudiDb()
	{
		blDbAperto=false;
		stmt.close();
	}
 
	public gestione_informazioni_tabella_1 EseguiQueryTabella(String querySQL)
	{
		gestione_informazioni_tabella_1 git1;
		informazioni_tabella_1 it1;
		ResultSet rs;
 
		Set rs=...
 
		git1=new gestione_informazioni_tabella_1();
		do while (rs.HasNext())
		{
			it1=new informazioni_tabella_1();
 
			// Riempie tutti i campi
			it1.nome=rs("nome");
			it1.cognome=rs("cognome");
			...
       		        ...
			rs.movenext();
 
			git1.aggiungi_dati(it1);
		}
 
		return git1;
	}
}


Come avrete visto la classe è molto semplice: ci sono fondamentalmente due variabili, una per la connessione con il db e l'altra usata come flag per sapere se il db è aperto o meno ed un metodo centrale "EseguiQueryTabella" che acquisisce i dati dalla sorgente recordset, li riversa in una classe di tipo informazioni_tabella_1 e li aggiunge al vettore interno primavo della classe gestione_informazioni_tabella_1.

Appare ora ancora più evidente il motivo per il quale abbiamo separato la classe con i singoli campi del db, con la classe che li gestisce: facendo cosi' possiamo lavorare indipendentemente prima sull'una e poi sull'altra, senza doverle intrecciare in modo pericoloso per la chiarezza del codice.

La gestione delle informazioni ora è completa e scalabile: qualsiasi modifica (aggiunta, rimozione di campi) deve essere fatta sulle classi relative al package informazione e al package database. La nostra applicazione è quindi centralizzata e modulare.