versione 31/3/2016 - prec. 13/09/2015
Introduzione ai Pattern
Prima di andare nel dettaglio del pattern
Model-View-Controller facciamo una breve introduzione ai Design Patterns.
I Pattern vengono divisi in base a 2 criteri: lo
scopo e il raggio di azione.
In base allo scopo, i Pattern,
si distinguono in 3 gruppi:
1.Creazionali: pongono attenzione al
modo in cui gli oggetti devono essere creati
2.Strutturali: si riferiscono al
modo in cui le classi e gli oggetti vengono composti
3.Comportamentali: si concentrano
sulle interazioni che avvengono
tra classi e oggetti
Mentre invece in base al
raggio di azione si distinguono in 2 gruppi:
1.Classi: si concentrano sulla
relazione di ereditarietà delle classi, definita a compile-time quindi statica
2.Oggetti: si focalizzano sulla
relazione tra gli oggetti, definita a run-time quindi dinamica
Ecco come si presenta la loro
classificazione nel celebre libro “Design Pattern” scritto dalla “Gang of Four”
(Gamma, Helm, Johnson e Vlissides).
Analizzeremo i pattern che servono al nostro scopo: Pattern Strategy e Pattern Observer.
Pattern Strategy
Lo strategy pattern è uno dei pattern comportamentali. L'obiettivo di questa architettura è isolare un algoritmo all'interno di un oggetto. Il pattern strategy è utile in quelle situazioni dove sia necessario modificare dinamicamente gli algoritmi utilizzati da un'applicazione. Il client definisce l’algoritmo da utilizzare, incapsulandolo in un contesto, il quale verrà utilizzato nella fase di elaborazione. Il contesto detiene i puntamenti alle informazioni necessarie al fine della elaborazione, cioè dati e funzionalità.
Questo pattern prevede che gli algoritmi siano intercambiabili tra loro (in base ad una qualche condizione) in modo trasparente al client che ne fa uso. In altre parole: la famiglia di algoritmi che implementa una funzionalità (ad esempio ordinamento) esporta sempre la medesima interfaccia, in questo modo il client dell'algoritmo non deve fare nessuna assunzione su quale sia la strategia istanziata in un particolare istante.
Gli algoritmi definiscono il modo in cui vengono elaborate le informazioni. Possiamo creare delle classi di algoritmi che implementano in modo diverso uno stesso algoritmo oppure possiamo creare delle nuove classi di algoritmi. Per esempio nel caso di un problema di ordinamento, possiamo creare diverse classi che implementano l’algoritmo BubbleSorting ma in modo diverso, così come possiamo creare delle classi di ordinamento basate su diversi algoritmi, per esempio SelectionSorting oppure InsertionSorting.
L’utilizzo di questo pattern consente al client di poter scegliere quale algoritmo e quale implementazione utilizzare senza dover conoscere i loro dettagli implementativi. Questo pattern è composto dai seguenti partecipanti:
•Strategy: dichiara una
interfaccia che verrà invocata dal Context in base all’algoritmo prescelto
•ConcreteStrategy: effettua
l’overwrite del metodo del Context al fine di ritornare l’implementazione
dell’algoritmo prescelta
•Context: detiene le informazioni di
contesto (dati ed algoritmo da utilizzare) ed ha il compito di invocare
l’algoritmo
La classe base richiede una strategia esterna per portare a termine correttamente il suo compito
interface Strategy { public int doOperation(int num1, int num2); } class OperationAdd implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 + num2; } } class OperationSubstract implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 - num2; } } class OperationMultiply implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 * num2; } } class Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public int executeStrategy(int num1, int num2) { return strategy.doOperation(num1, num2); } } public class StrategyPatternDemo { public static void main(String[] args) { Context context = new Context(new OperationAdd()); System.out.println("10 + 5 = " + context.executeStrategy(10, 5)); context = new Context(new OperationSubstract()); System.out.println("10 - 5 = " + context.executeStrategy(10, 5)); context = new Context(new OperationMultiply()); System.out.println("10 * 5 = " + context.executeStrategy(10, 5)); } }
La cui esecuzione produce il seguente output:
C:\Java>"C:\Program Files\Java\jdk1.7.0_21\bin\javac.exe" StrategyPatternDemo.java C:\Java>"C:\Program Files\Java\jdk1.7.0_21\bin\java.exe" StrategyPatternDemo 10 + 5 = 15 10 - 5 = 5 10 * 5 = 50
Vediamo come si presenta il pattern in UML dell’esempio appena presentato:
Nella libreira del JDK il pattern Strategy viene utilizzato nelle librerie grafiche (awt e swing) per esempio per definire la tipologia di Border da applicare al JPanel. Il Context è definito da JPanel, in particolare da JComponent, che detiene il puntatore alla “strategia” del bordo da utilizzare.
jPanel.setBorder(new
LineBorder(Color.RED));
jPanel.setBorder(new TitledBorder("Hello Border!"));
Pattern Observer
Il pattern Observer risulta utile per gestire dinamicamente l'accoppiamento tra "Sorgenti di eventi" e classi che devono reagire quando questi si verificano. La sua realizzazione prevede una classe "osservabile" (in Java si implementa con la classe Observable) e degli oggetti "osservatori" (in Java sono implementabili con l'interfaccia Observer). Il ruolo di osservatore (subject) è il ruolo svolto da colui che si mostra interessato ai cambiamenti di stato. Il ruolo di Observable (osservato) è il ruolo svolto da colui che viene monitorato.
Questo pattern comportamentale basato su oggetti risulta
utile quando si vuole realizzare una dipendenza uno-a-molti dove il cambiamento
di stato di un soggetto viene notificato a tutti i soggetti che si sono mostrati
interessati.
Questo pattern è composto dai seguenti partecipanti:
- Subject: espone l’interfaccia che consente agli osservatori di iscriversi e cancellarsi; mantiene una reference per tutti gli osservatori iscritti. Il Subject include una lista degli osservatori che si registrano presso il Subject osservato tramite i metodi addObserver() e si cancellano tramite il metodo removeObserver(). Mentre invece il metodo notifyObservers() viene invocato dalla classe concreta ConcreteSubject quando interviene un cambio di stato.
import java.util.ArrayList;
import java.util.List;
public abstract class Subject
{
private List<Observer> list = new ArrayList<Observer>();
public void addObserver(Observer observer)
{
list.add( observer );
}
public
void removeObserver(Observer observer)
{
list.remove( observer );
}
public void notifyObservers()
{
for (Observer observer: list)
{
observer.update();
}
}
}
- Observer: espone l’interfaccia (update) che consente di aggiornare gli osservatori qualora si verifichi un cambio di stato del soggetto osservato. Pertanto quando avvengono delle modifiche nel soggetto osservato, verrà invocato il metodo update()di tutti gli osservatori.
public interface Observer
{
public void update();
}
- ConcreteSubject: mantiene lo stato del soggetto osservato e notifica gli osservatori in caso di un cambio di stato con (notifyObservers().
public class ConcreteSubject extends
Subject
{
private boolean state;
public void setState(boolean state)
{
this.state = state;
notifyObservers();
}
public boolean
getState()
{
return this.state;
}
}
- ConcreteObserver: implementa l’interfaccia dell’Observer definendo il comportamento in caso di cambio di stato del soggetto osservato. Il ConcreteObserver implementa il metodo update() per definire l’azione da intraprendere quando interviene un cambio di stato del Subject.
public class ConcreteObserver
implements Observer
{
@Override
public void update()
{
System.out.println("Sono " + this + ": il Subject e' stato modificato!");
}
}
Il Subject sa che una lista di Observer sono interessati al suo stato ma non conosce le classi concrete degli Observer, pertanto non vi è un accoppiamento forte tra di loro. Vediamo come si presenta il Pattern Observer utilizzando il Class Diagram in UML:
Creiamo la classe Client che si occupa di creare il soggetto da osservare e due osservatori che si registrano per essere notificati in caso di cambio di stato del soggetto osservato. Successivamente rimuoviamo un osservatore e cambiamo lo stato del soggetto osservato per notare che non riceverà più notifiche.
public class Client
{
public static void main(String[] args)
{
ConcreteSubject subject = new ConcreteSubject();
Observer observer1 = new ConcreteObserver();
Observer observer2 = new ConcreteObserver();
//aggiungo 2 observer che saranno notificati
subject.addObserver(observer1);
subject.addObserver(observer2);
//modifico lo stato
subject.setState( true );
//rimuovo il primo observer che non sarà + notificato
subject.removeObserver(observer1);
//modifico lo stato
subject.setState( false );
}
}
Combinando i precedenti script otteniamo il seguente programma:
import java.util.ArrayList; import java.util.List; import java.io.*; interface Observer { public void update(); } abstract class Subject { private Listlist = new ArrayList (); public void addObserver(Observer observer) { list.add( observer ); } public void removeObserver(Observer observer) { list.remove( observer ); } public void notifyObservers() { for (Observer observer: list) { observer.update(); } } } class ConcreteObserver implements Observer { @Override public void update() { System.out.println("Sono " + this + ": il Subject e' stato modificato!"); } } class ConcreteSubject extends Subject { private boolean state; public void setState(boolean state) { this.state = state; notifyObservers(); } public boolean getState() { return this.state; } } public class Client { public static void main(String[] args) { ConcreteSubject subject = new ConcreteSubject(); Observer observer1 = new ConcreteObserver(); Observer observer2 = new ConcreteObserver(); //aggiungo 2 observer che saranno notificati subject.addObserver(observer1); subject.addObserver(observer2); //modifico lo stato subject.setState( true ); //rimuovo il primo observer che non sarà + notificato subject.removeObserver(observer1); //modifico lo stato subject.setState( false ); } }
la cui esecuzione produce il seguente output:
C:\Java>"C:\Program Files\Java\jdk1.7.0_21\bin\javac.exe" Client.java C:\Java>"C:\Program Files\Java\jdk1.7.0_21\bin\java.exe" Client Sono ConcreteObserver@1befab0: il Subject e' stato modificato! Sono ConcreteObserver@13c5982: il Subject e' stato modificato! Sono ConcreteObserver@13c5982: il Subject e' stato modificato!
Il pattern del nostro esempio in UML diventa:
Nelle librerie Java il Subject e l’Observer sono già presenti con le classi java.util.Observable e java.util.Observer per cui non occorre definirle.
Pertanto, per non reinventare la ruota ogni volta, possiamo applicare queste classi all'esempio precedente. Procediamo con il refactoring del nostro codice per adattarlo alle classi java.util.Observable e java.util.Observer.
import java.io.*; import java.util.Observable; import java.util.Observer; class ConcreteObserver implements Observer { @Override public void update(Observable o, Object arg) { System.out.println("Sono " + this + ": il Subject e stato modificato!"); } } class ConcreteSubject extends Observable { private boolean state; public void setState(boolean state) { this.state = state; setChanged(); notifyObservers(); } public boolean getState() { return this.state; } } public class Client { public static void main(String[] args) { ConcreteSubject subject = new ConcreteSubject(); Observer observer1 = new ConcreteObserver(); Observer observer2 = new ConcreteObserver(); //aggiungo 2 observer che saranno notificati subject.addObserver(observer1); subject.addObserver(observer2); //modifico lo stato subject.setState( true ); //rimuovo il primo observer che non sarà + notificato subject.deleteObserver(observer1); //modifico lo stato subject.setState( false ); } }
Nel metodo setState() occorre invocare il metodo setChanged() che esprime la volontà di notificare un cambiamento agli osservatori. Infatti se commentiamo questo metodo, gli osservatori non saranno avvisati nonostante l’invocazione del metodo notifyObservers(). Ciò avviene in quanto nel metodo notifyObservers(), all'interno della classe java.util.Observable, è presente una semplice condizione “if (!changed)” che forza l’uscita dal metodo qualora il cambio non venga confermato. Qui di seguito vediamo il codice effettivo del metodo notifyObservers() della classe java.util.Observable:
public void notifyObservers(Object
arg)
{
Object[] arrLocal;
synchronized (this)
{
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i =
arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
L'interfaccia observable fornisce i metodi per registrare/depennare gli "Osservatori":
• addObserver(Observer o)
•
removeObserver(Observer o)
Inoltre fornisce i metodi per notificare eventi agli Observer quando qualcosa nell’oggetto Observable cambia:
• notifyObservers(Object arg)
Per notificare un cambiamento di stato basta chiamare notifyObservers() e attivare il flag changed utilizzando una chiamata al metodo setChanged().
Observer è un'interfaccia implementata dalle classi concrete che devono ricevere notifiche di eventi da parte delle classi Observable. Ha un unico metodo:
• update(Observable o, Object arg)
che viene invocato quando si verifica un cambiamento nell’oggetto osservato.
Esempio: Semplice programma che utilizza il pattern ObserverC:\Java>"C:\Program Files\Java\jdk1.7.0_21\bin\javac.exe" EsempioPatternObserver.javaPer eseguire scriviamo:
C:\Java>"C:\Program Files\Java\jdk1.7.0_21\bin\java.exe" EsempioPatternObserver Ho ricevuto un aggiornamento sul saldo: 30
import java.io.*; import java.util.Observable; import java.util.Observer; public class EsempioPatternObserver { public static void main(String args[]) { Conto conto = new Conto(); // Osservabile ContoObserver ControllaConto = new ContoObserver(); conto.addObserver(ControllaConto); conto.aggiornaSaldo(30); } } class ContoObserver implements Observer { public void update(Observable conto, Object saldo) { System.out.println("Ho ricevuto un aggiornamento sul saldo: " + saldo); } } class Conto extends Observable { private Integer saldo = 0; public void aggiornaSaldo(Integer importo) { saldo = importo; setChanged(); notifyObservers(saldo); } }
Il pattern Observer è utilizzato nel Model View Controller per notificare alla View eventi generati nel Model e nel Controller.
Model View Controller Pattern (MVC)
Un Pattern Architetturale
definisce:
• il più alto livello di astrazione di un sistema
software
• la struttura
del sistema software in termini di
– sottosistemi e relative
responsabilità
– linee guida per gestire le relazioni e l’interazione tra sottosistemi
La scelta del pattern architetturale è una scelta
fondamentale ed influenza direttamente le fasi di progettazione e realizzazione
del sistema software.
La struttura di una applicazione software, e più in generale di un sistema informativo, è caratterizzata da tre livelli
![]() |
|
Presentazione: insieme delle componenti che gestiscono l'interazione con l'utente | |
Logica Applicativa: insieme delle componenti che realizzano le funzionalita richieste e gestiscono il flusso dei dati |
|
Dati e Risorse: insieme delle componenti che gestiscono i dati che rappresentano le informazioni utilizzate dall'applicazione secondo il modello concettuale del dominio |
MVC (acronimo di model-view controller) è un pattern architetturale (schema generale di riferimento) per la progettazione e strutturazione di applicazioni di tipo interattivo. Consente di separare e disaccoppiare il modello dei dati (model) e la logica applicativa (controller) dalle modalità di visualizzazione e interazione con l’utente (view).
Il paradigma MVC identifica, all’interno di un sistema informatico, tre tipologie di componenti:
- Model: rappresenta il modello dei dati di interesse per l’applicazione
- View: fornisce una rappresentazione grafica ed interattiva del model.
La logica di presentazione dei dati viene gestita solamente dalla View. Ciò implica che questa deve gestire la costruzione dell' interfaccia grafica (GUI) che rappresenta il mezzo mediante il quale gli utenti interagiranno con il sistema. Ogni GUI può essere costituita da schermate diverse che presentano più modi di interagire con i dati dell'applicazione. Per far sì che i dati presentati siano sempre aggiornati è possibile adottare due strategie note come "push model" e "pull model". Il "push model" adotta il pattern Observer, registrando le View come osservatori del Model. Le View possono quindi richiedere gli aggiornamenti al Model in base alle notifiche che il modello eroga. Con la strategia "pull model" la View richiede gli aggiornamenti quando "lo ritiene opportuno". Infine la View non decide la schermata da utilizzare ma delega ciò alla decisione del Controller (che grazie al pattern Strategy può anche essere anche cambiata dinamicamente al runtime). In definitiva la componente View si occupa della costruzione e della presentazione all'utente della schermata stessa.
- Controller: definisce la logica di controllo e le funzionalità applicative
Questo componente ha la responsabilità di trasformare le interazioni dell'utente della View in azioni eseguite dal Model. Il Controller non rappresenta un semplice "ponte" tra View e Model ma realizzando la mappatura tra input dell'utente e i processi eseguiti dal Model e selezionando la schermate della View richieste, il Controller implementa la logica di controllo dell'applicazione.
Il paradigma detta la distinzione di responsabilità tra le tre categorie e il flusso di informazioni tra di esse. In particolare:
• Model:
notifica
cambiamenti di stato/dei dati al view
• View
– fa riferimento al model e può interrogarlo per ottenere lo stato corrente
– notifica al controller gli eventi generati dall’interazione con l’utente ed
eventuali richieste di modifica
•
Controller
– Riceve le richieste dal view
(se sono di modifica aggiorna lo stato del model, se sono di selezione view
richiama la view corrispondente)
Nella fase di
inizializzazione dell’applicazione abbiamo i seguenti step:
1. viene creato il model
2. viene creato il view
fornendo un riferimento al model
3. viene creato il
controller fornendo riferimenti al model e al view
4. il view si registra come observer (listener) del model per
ricevere le notifiche di aggiornamento dal model (observable)
5. il
controller si registra come observer (listener) della view per ricevere dal view
(observable) gli eventi generati dall’utente. In realtà la connessione avviene
mediante il meccanismo di propagazione e gestione eventi Swing/AWT quindi
EventListener (es. ActionListener, MouseListener...) associati ai componenti
grafici di view (es. JButton).
Quando l'utente interagisce con l’applicazione
1. il
view riconosce l’azione dell’utente (es. pressione di un
bottone) e la notifica al controller registrato come observer
2. il controller interagisce con il model per
realizzare la funzionalità richiesta ed aggiornare/modificare lo stato o i dati
3. il model notifica al view registrato come
observer le modifiche e gli aggiornamenti
4. il view
aggiorna la visualizzazione sulla base del nuovo stato. Il nuovo stato e le info
aggiornate per modificare la visualizzazione possono essere ottenuti dal view
con
approccio push: il model notifica al view sia il cambiamento di stato che le informazioni aggiornate
approccio pull: il view riceve dal model la notifica del cambiamento di stato e poi accede al model per ottenere le informazioni aggiornate
Il pattern Model-View-Controller risulta utile per impostare la struttura generale dell’applicazione in fase di progettazione ma non definisce in maniera univoca schemi realizzativi ed implementazione.
Una progettazione architetturale che si basa sulla separazione dei ruoli dei componenti software e rende strutturalmente indipendenti moduli con funzionalità differenti favorisce qualità esterne ed interne del software
Qualità esterne:
>>
estendibilità
–
semplicità di progetto e decentralizzazione dell’architettura
– software facilmente estendibile agendo su moduli specifici
>>
riusabilità
– possibilità
di estrarre e riutilizzare componenti
>>
interoperabilità
– interazione tra moduli con ruoli
differenti
– possibilità di creare gerarchie tra
componenti
Qualità interne
>>
strutturazione
–
struttura del software riflette le caratteristiche del dominio applicativo (dati
+ controllo + interazione e visualizzazione)
>>
modularità
– organizzazione del software in
componenti con funzionalità definite
>>
comprensibilità
– ruoli e funzioni dei componenti
sono facilmente identificabili
>> manutenibilità
– possibilità di intervenire su componenti specifici con effetti nulli o
limitati su altri componenti
Nel paradigma MVC l’interazione tra componenti è basata su meccanismi di propagazione e gestione di eventi:
componenti view notificano le azioni dell’utente ai componenti controller
componenti model notificano cambiamenti di stato ai componenti view
In Java si ha che l’interazione tra view e controller avviene in base al meccanismo di propagazione e gestione eventi Swing/AWT quindi EventListener (es. ActionListener, MouseListener...) associati ai componenti grafici di view (es. JButton)
L’interazione tra model e view avviene
secondo il pattern Observer-Observable
• i componenti
model estendono la classe Observable
• i componenti
view implementano l’interfaccia Observer e si
registrano presso i componenti model (model.addObserver(view))
• i componenti model notificano i cambiamenti ai componenti
view registrati come observer (notifyObservers())
• i componenti view ricevono le notifiche (update(model))
e aggiornano la visualizzazione
In pratica, View e Model sono relazionati tramite il pattern Observer dove la View "osserva" il Model. Anche il legame tra View e Controller è caratterizzato da un pattern Observer (implementato in java mediante la gestione degli eventi Swing/AWT e non mediante la classe Observable e l'interfaccia Observer) ma in questo caso è la View ad essere "osservata" dal Controller.
Nel pattern MVC tra i 3 soggetti esiste un forte legame in merito al cambiamento di stato. In particolare il Controller è interessato ai cambiamenti di stato della View, mentre la View è interessata ai cambiamenti di stato del Model. Il Model è osservato dalla View mentre il Controller è un osservatore della View. Quindi il pattern Observer trova applicazione 2 volte nell’MVC su coppie di soggetti diversi (Model-View e View-Controller). La View svolge un ruolo doppio poichè si trova ad essere osservata dal Controller e nello stesso tempo ad essere osservatore nei confronti del Model a differenza del Model e del Controller che invece giocano un ruolo singolo .
Esempio 1: Programma Termometro (MVC)
Vediamo un'applicazione progettata seguendo il pattern architetturale MVC
Listato del programma
/* * NOTA: questo e' un semplice esempio didattico. * Spesso la separazione tra model-view-controller non e' netta e definita. * In questo esempio ho considerato come unico observer la view intera ma * potevo considerare i 3 elementi: * SLIDER => controllo (e' sia di input che di output) * ANALOGVIEW => e' il termometro grafico (è sia di input che di output) * DIGITALVIEW => mostra la temperatura. E' solo di output * Cambiando il valore della temperatura nel MODEL l'observer VIEW aggiorna i * controlli indicati nel metodo UPDATE */ public class Applicazione { public static void main(String[] args) { TemperaturaModel model = new TemperaturaModel(); // Creazione Modello TemperaturaView mainFrame = new TemperaturaView();// Creazione View model.addObserver(mainFrame); // Pongo la View come Observer // Potevo porre come Observer i 3 elementi della maschera: // ANALOGVIEW (In/Out) - SLIDER (In/Out) - DIGITALVIEW (Out) TemperaturaController controller=new TemperaturaController(model, mainFrame); mainFrame.setVisible(true); } }
Listato del MODEL
import java.util.Observable; public class TemperaturaModel extends Observable { private int temperatura; public TemperaturaModel() { temperatura = 0; } // Metodo di lettura per le VIEW public int getValoreTemperatura() { return temperatura; } // Metodo di scrittura per i Controller public void setValoreTemperatura(int valore) { if (valore >= 0 && valore <= 100) { temperatura = valore; setChanged(); notifyObservers(temperatura); } } }
Listato del VIEW
import java.util.Observer; import static javax.swing.JFrame.*; public class TemperaturaView extends JFrame implements Observer { public JSlider slider; private final JLabel tempLabel; public AnalogView analogico; public TemperaturaView() { super("Temperatura"); // --- 1° Componente ------------------------------ // Etichetta che mostra la temperatura // ------------------------------------------------ tempLabel = new DigitalView(); // --- 2° Componente ------------------------------ // Etichetta che mostra la temperatura // ------------------------------------------------ slider = new JSlider(0, 100, 0); slider.setMajorTickSpacing(10); slider.setMinorTickSpacing(2); slider.setPaintTicks(true); slider.setPaintLabels(true); slider.setBorder(BorderFactory.createEmptyBorder(0, 15, 15, 15)); // --- 3° Componente ------------------------------ // Termometro grafico // ------------------------------------------------ analogico = new AnalogView(); // Definizione Layout dei controlli sul JFrame JPanel pannelloCentrale = new JPanel(); pannelloCentrale.setLayout(new BoxLayout(pannelloCentrale, BoxLayout.X_AXIS)); pannelloCentrale.add(tempLabel); pannelloCentrale.add(Box.createRigidArea(new Dimension(50,0))); pannelloCentrale.add(analogico); getContentPane().add(pannelloCentrale, BorderLayout.CENTER); getContentPane().add(slider, BorderLayout.PAGE_END); setSize(600, 345); setResizable(false); setLocationRelativeTo(null); // Centro la finestra nello schermo setDefaultCloseOperation(EXIT_ON_CLOSE); } // Controllo analogico class DigitalView extends JLabel { DigitalView() { super("0° C"); setFont(new Font("Arial", Font.BOLD, 150)); setPreferredSize(new Dimension(470,150)); setMaximumSize(getPreferredSize()); setMinimumSize(getPreferredSize()); setHorizontalAlignment(JLabel.RIGHT); } } @Override public void update(Observable model, Object valore) { tempLabel.setText((Integer)valore + "° C"); slider.setValue((Integer)valore); analogico.setTemperature((Integer)valore); } }
Listato del Controllo ANALOGVIEW (è parte del VIEW)
import java.awt.*; // Color, Dimension, Graphics ... import javax.swing.JPanel; public class AnalogView extends JPanel { private static final int top = 1; // Posizione del controllo nella finestra private static final int left = 11; // principale private static final int width = 20; private static final int height = 200; private static final int MAX_TEMP = 100; private static final int MIN_TEMP = 0; private int valoreCorrente = 0; private Color colore = Color.GREEN; public AnalogView() { setMaximumSize(new Dimension(43,240)); } public void paintComponent(Graphics g) { super.paintComponent(g); // Rettangolo con sotto ... g.setColor(Color.black); g.drawRect(left, top, width, height); // ... un cerchio pieno di "colore" g.setColor(colore); // Riempimento dell'ovale g.fillOval(left-width/2, top+height-3,width*2, width*2); g.setColor(Color.black); // Bordo dell'ovale g.drawOval(left-width/2, top+height-3,width*2, width*2); g.setColor(Color.white); // Riempimento rettangolo g.fillRect(left+1,top+1, width-1, height-1); g.setColor(colore); // Riempimento livello del termometro long redtop = height*(valoreCorrente-MAX_TEMP)/(MIN_TEMP-MAX_TEMP); g.fillRect(left+1, top + (int)redtop+1, width-1, height-(int)redtop); } public void setTemperature(int t) { valoreCorrente = t; if (valoreCorrente <= 30) colore = Color.GREEN; else if (valoreCorrente <= 70) colore = Color.ORANGE; else colore = Color.RED; repaint(); } }
Listato del CONTROLLER
public class TemperaturaController { private final TemperaturaModel M; private final TemperaturaView V; TemperaturaController(TemperaturaModel model, TemperaturaView view) { M = model; V = view; // controller si registra come listener presso il view V.slider.addChangeListener(new SliderController(M)); V.analogico.addMouseMotionListener(new AnalogMoveController(M)); V.analogico.addMouseListener(new AnalogController(M)); } // Controller Drag del mouse sul termometro class AnalogMoveController implements MouseMotionListener { private final TemperaturaModel termoModel; AnalogMoveController(TemperaturaModel model) { termoModel = model; } @Override public void mouseDragged(MouseEvent me) { // Notifica al model del cambiamento di stato termoModel.setValoreTemperatura((201-me.getY())/2); } @Override public void mouseMoved(MouseEvent me) {} } // Controller Click del mouse sul termometro class AnalogController implements MouseListener { private final TemperaturaModel termoModel; AnalogController(TemperaturaModel model) { termoModel = model; } @Override public void mouseClicked(MouseEvent me) {} @Override public void mousePressed(MouseEvent me) { // Notifica al model del cambiamento di stato termoModel.setValoreTemperatura((201-me.getY())/2); } @Override public void mouseReleased(MouseEvent me) {} @Override public void mouseEntered(MouseEvent me) {} @Override public void mouseExited(MouseEvent me) {} } // Controller dello Slider class SliderController implements ChangeListener { private final TemperaturaModel termoModel; SliderController(TemperaturaModel model) { termoModel = model; } @Override public void stateChanged(ChangeEvent e) { // Notifica al model del cambiamento di stato termoModel.setValoreTemperatura(((JSlider)e.getSource()).getValue()); } } }
Lo schema MVC dell'applicazione può essere riassunto con questo schema
Il progetto completo è scaricabile qui.
Esempio 2: Programma Chat locale (MVC) senza librerie
In questo esempio non vengono utilizzate la classe Observable ne l'interfaccia Observer del Java
Listato del programma (ChatApp.java)
public class ChatApp { public static void main(String[] args) { ChatModel model = new ChatModel(); ChatView view1 = new ChatView(model); ChatView view2 = new ChatView(model); ChatView view3 = new ChatView(model); ChatController controller1 = new ChatController(view1, model); ChatController controller2 = new ChatController(view2, model); ChatController controller3 = new ChatController(view3, model); } }
Listato dell'interfaccia chatObservable
public interface ChatObservable { void addObserver(ChatObserver forumObserver); void notifyObservers(String Command, String arg); }
Listato dell'interfaccia chatObserver
public interface ChatObserver { void update(String Cmd, String Arg); }
Listato della componente chatController
// Osserva il VIEW public class ChatController implements ChatObserver { private final ChatModel model; private final ChatView view; public ChatController(ChatView v, ChatModel m) { view = v; model = m; view.addObserver(this); // Si pone come observer di VIEW } // Implementazione metodo UPDATE dell interfaccia ChatObserver // Esegue i comandi ricevuti dall'interfaccia (VIEW) @Override public void update(String Command, String arg) { switch (Command) { case "doCmd.SendMessage": model.addMessage(arg); break; case "doCmd.ChangeView": view.showDisplay(Integer.parseInt(arg)); break; } } }
Listato della classe chatView
import java.util.*; // ArrayList, Collections, Enumeration import javax.swing.*; import java.awt.*; // Container import java.awt.event.*; // Osserva il MODEL, Osservato dal CONTROLLER public final class ChatView implements ChatObserver, ChatObservable { private final ArrayList chatObserversVector = new ArrayList(); private final ChatModel model; private final JScrollPane scroll; private final Container container; private final JButton submit; private final ButtonGroup group; private final JRadioButton firstGui; private final JRadioButton secondGui; private final JTextArea textArea; private final JPanel panel; private final JTextField textField; private final JFrame frame; private static int counter = 0; // conta le finestre private int id; public ChatView(ChatModel model) { this.model = model; id = ++counter; frame = new JFrame("Client " + id); scroll = new JScrollPane(); container = frame.getContentPane(); textArea = new JTextArea("", 10, 40); textArea.setEditable(false); panel = new JPanel(); textField = new JTextField(20); submit = new JButton("Submit"); group = new ButtonGroup(); firstGui = new JRadioButton("FirstGui", true); secondGui = new JRadioButton("SecondGui"); container.setLayout(new BorderLayout()); secondGui.addItemListener(selectionHandler); submit.addActionListener(submitHandler); model.addObserver(this); CreateView(BorderLayout.SOUTH); frame.setLocation(0,(id-1)*frame.getHeight()); } // Creazione della Finestra public void CreateView(String BorderPosition) { scroll.setViewportView(textArea); container.add(scroll, BorderLayout.CENTER); container.add(panel, BorderPosition); group.add(firstGui); group.add(secondGui); panel.add(firstGui); panel.add(secondGui); panel.add(textField); panel.add(submit); frame.pack(); frame.setVisible(true); } // Cambio di Vista public void showDisplay(int display) { String text = model.getMessages(); frame.setVisible(false); textArea.setText(text); if (display == ItemEvent.SELECTED) CreateView(BorderLayout.NORTH); else CreateView(BorderLayout.SOUTH); } // Listener ItemListener selectionHandler = new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { // Notifica al Controller: Cambio Vista notifyObservers("doCmd.ChangeView", Integer.toString(e.getStateChange())); } }; ActionListener submitHandler = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // Notifica al Controller: Invio Messaggio if (textField.getText().trim().length()>0) notifyObservers("doCmd.SendMessage", "Client"+id+": "+textField.getText()); } }; // Implementazione interfaccia Observer e Observable @Override public void update(String Cmd, String Arg) { textArea.setText(model.getMessages()); textField.setText(""); } @Override public void addObserver(ChatObserver chatObserver) { chatObserversVector.add(chatObserver); } @Override public void notifyObservers(String Cmd, String Arg) { Enumeration e = Collections.enumeration(chatObserversVector); while (e.hasMoreElements()) ((ChatObserver)e.nextElement()).update(Cmd,Arg); } }
Listato della classe chatModel
import java.util.*; // Osservato da VIEW public class ChatModel implements ChatObservable { private final ArrayList messages; private final ArrayList forumObserversVector = new ArrayList(); public ChatModel() { messages = new ArrayList(); } public void addMessage(String msg) { messages.add(msg); notifyObservers("doCmd.UpdateView",this.getMessages()); } public String getMessages() { String allMessages = ""; for (int i = 0; i < messages.size(); ++i) { if (!allMessages.equals("")) allMessages += "\n"; allMessages += (String)messages.get(i); } return allMessages; } // Implementazione interfaccia Observable @Override public void addObserver(ChatObserver forumObserver) { forumObserversVector.add(forumObserver); } @Override public void notifyObservers(String Cmd, String Arg) { Enumeration e = Collections.enumeration(forumObserversVector); while (e.hasMoreElements()) ((ChatObserver)e.nextElement()).update(Cmd,Arg); } }
Lo schema MVC dell'applicazione può essere riassunto con questo schema
Il progetto completo è scaricabile qui.
Esempio 3: Programma Cambio Valuta (MVC)
In questo esempio ...
Listato del programma
Esempio 4: Programma Schiaccia la talpa (MVC)
Vediamo un'altra applicazione progettata seguendo il pattern architetturale MVC:
Listato del programma
/* * NOTA: questo e' un semplice esempio didattico. * Spesso la separazione tra Model-View-Controller non e' netta e definita. * In questo esempio ho considerato come unico observer la VIEW ma * potevo includere anche i 3 elementi: * TALPA => La talpa che appare e scompare * PUNTEGGIO => il punteggio * TERRENO => mostra lo sfondo. * Colpendo la talpa, il CONTROLLER genera un evento che nel MODEL * determina un cambio di stato (M.TalpaColpita). L'observer VIEW * tramite il metodo UPDATE aggiorna la videata in modo opportuno. */ public class SchiacciaLaTalpa { public static void main(String[] args) throws InterruptedException { TalpaModel model=new TalpaModel(); // Creazione Modello TalpaView view=new TalpaView(); // Creazione View model.addObserver(view); // Pongo la View come Observer TalpaController Ctrl =new TalpaController(view,model); view.MostraTalpaCasualmente(); // Preparo la visualizzazione della talpa } }
Listato del MODEL
import java.util.Observable; /* --------------------------------------------------------------------------- Il Model: - definisce lo stato/dati correnti dell’applicazione - gestisce l’accesso allo stato /dati > per la VIEW, offre metodi che espongano i dati in sola lettura > per il CONTROLLER mette a disposizione i metodi che consentono di modificare lo stato in base alle richieste dell’utente; - notifica alla VIEW i cambiamenti di stato/dati affinché questi possano essere evidenziati a video. -- ------------------------------------------------------------------------ */ public class TalpaModel extends Observable { private int Punti; private boolean TalpaAttivata; public TalpaModel() { Punti=0; TalpaAttivata=false; setChanged(); notifyObservers("INIT"); } public int getPunti() { return Punti; } public void DisattivaTalpa() { Punti=0; TalpaAttivata=false; setChanged(); notifyObservers("OFF"); } public void AttivaTalpa() { Punti=0; TalpaAttivata=true; setChanged(); notifyObservers("ON"); } public void TalpaColpita() { Punti++; setChanged(); notifyObservers("HIT"); } public void TalpaSvanita() { setChanged(); notifyObservers("VANISH"); } public void TalpaMancata() { if (TalpaAttivata==true) { Punti--; setChanged(); notifyObservers("MISS"); } } }
Listato del VIEW
import java.util.Observer; import java.util.Observable; import java.awt.*; // Point, Toolkit, ActionListener, GridLayout, Font ... import javax.swing.*; // JFrame, JComboBox ... import javax.swing.border.*; /* ----------------------------------------------------------------------- VIEW: fornisce la rappresentazione grafica ed interattiva del MODEL. - definisce le modalità di presentazione dei dati e dello stato dell’applicazione - consente l’interazione con l’utente - riceve notifiche dal MODEL e aggiorna la visualizzazione - demanda al CONTROLLER l'esecuzione dei processi richiesti dall'utente e la scelta delle eventuali schermate da presentare. -------------------------------------------------------------------------- */ public class TalpaView extends JFrameCentered implements Observer { // final per indicare che viene inizializzata una sola volta final public JButton Reset; final public JButton Start; final private Punteggio Punti; final private Talpa T; final private Terreno Sfondo; final private Titolo Tit; // ************************************************* // TITOLO DELLA MASCHERA // ************************************************* public TalpaView() { // La keyword super usata nel costruttore serve per riferirsi alla // classe padre i.e. quella da cui si deriva. In casi come questo // nei quali la classe padre ha un costruttore con parametri // la classe figlia (derivata) ha l’obbligo di chiamare esplicitamente // il costruttore del padre che invece è implicitamente chiamato // nel caso che sia presente un costruttore vuoto (senza argomenti). super(945,582,"Schiaccia la Talpa"); // --------------------------------------------------- // Creo le varie Componenti ... Tit = new Titolo("Schiaccia la Talpa".toUpperCase()); T=new Talpa(); // Talpa Sfondo=new Terreno(); // Sfondo // --------------------------------------------------- // ... e le dispongo nel pannello dell'applicazione JPanel PannelloCentrale = new JPanel(null); PannelloCentrale.add(T); PannelloCentrale.add(Sfondo); PannelloCentrale.setComponentZOrder(Sfondo, 1); PannelloCentrale.setComponentZOrder(T, 0); // davanti a tutto // --------------------------------------------------- // Parte pie pagina JPanel piepagina = new JPanel(); // ------- Gestione del Layout: LINE_AXIS ------------------------- // | PUNTI | HorizontalGlue | START | RigidArea | RESET | // ---------------------------------------------------------------- piepagina.setLayout(new BoxLayout(piepagina, BoxLayout.LINE_AXIS)); // Bordo doppio Border BordoEst=BorderFactory.createMatteBorder(0,1,1,1,Color.black); Border BordoInt=BorderFactory.createEmptyBorder(5, 5, 5, 5); Border BordoComposto=BorderFactory.createCompoundBorder(BordoEst, BordoInt); piepagina.setBorder(BordoComposto); // Aggiungo gli elementi sul Pie pagina piepagina.add(new JLabel("Punteggio: ")); piepagina.add(Punti=new Punteggio()); piepagina.add(Box.createHorizontalGlue()); piepagina.add(Start = new JButton("START")); piepagina.add(Box.createRigidArea(new Dimension(10, 0))); piepagina.add(Reset = new JButton("RESET")); // *********************************************** // ------------- Gestione del Layout // | NORTH | // ------------- // | CENTER | // ------------- // | SOUTH | // ------------- // *********************************************** getRootPane().setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); getContentPane().add(Tit,BorderLayout.NORTH); getContentPane().add(PannelloCentrale,BorderLayout.CENTER); getContentPane().add(piepagina,BorderLayout.SOUTH); setVisible(true); // Mostro la finestra del gioco } public void MostraTalpaCasualmente() throws InterruptedException { for (;;) T.CompariCasualmente(); } public Talpa getTalpa() { return T; } public Terreno getSfondo() { return Sfondo; } // ------------------------------------------------------ // Classe interna relativa al titolo class Titolo extends JLabel { public Titolo(String frase) { Font font = new Font("Serif", Font.ITALIC, 20); setText(frase); this.setHorizontalAlignment( SwingConstants.CENTER); setFont(font); setForeground(Color.blue); setOpaque(true); setBackground(new Color(0,0,0,64)); setBorder(BorderFactory.createLineBorder(Color.black)); } } @Override public void update(Observable model, Object valore) { int p=((TalpaModel) model).getPunti(); Punti.SetPunteggio(p); if ((valore=="OFF") || (valore=="INIT")) // Sospendo la visualizzazione della talpa T.Disattivata(); else if (valore=="ON") // Inizio a mostrare la talpa T.Attivata(); else if (valore=="MISS") // Talpa mancata T.Hide(); else if (valore=="HIT") // Talpa colpita (pressione mouse) T.Hit(); else if (valore=="VANISH") // Talpa colpita (rilascio del mouse) T.Vanish(); } }
Listato del CONTROLLER
import java.awt.event.*; /* -- ------------------------------------------------------------------ CONTROLLER: definisce la logica di controllo e le funzionalità applicative - gestisce gli eventi conseguenti ai comandi dell’utente - opera sul MODEL apportando modifiche, aggiornamenti, inserimenti in base agli eventi ed ai comandi ricevuti ----------------------------------------------------------------------- */ public class TalpaController { private final TalpaModel M; private final TalpaView V; public TalpaController(TalpaView view, TalpaModel model) { M=model; V=view; // -------------------------------------------- // Imposto i vari Listener V.Reset.addActionListener(new ResetController()); V.Start.addActionListener(new StartController()); V.getSfondo().addMouseListener(new ColpoMancatoController()); // Esempio di definizione del listener mediante // un'anonymous inner class listeners MouseAdapter ColpoRiuscitoController = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { M.TalpaColpita(); } @Override public void mouseReleased(MouseEvent e) { M.TalpaSvanita(); } @Override public void mouseExited(MouseEvent e) { M.TalpaSvanita(); } }; V.getTalpa().addMouseListener(ColpoRiuscitoController); } class ColpoMancatoController extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { M.TalpaMancata(); } } class ResetController implements ActionListener { @Override public void actionPerformed(ActionEvent event) { M.DisattivaTalpa(); } } class StartController implements ActionListener { @Override public void actionPerformed(ActionEvent event) { M.AttivaTalpa(); } } }
Listato del Controllo TALPA (utilizzato nella VIEW)
import javax.swing.*; // Necessaria per JLabel import java.awt.*; // Cursor, Image, Point, Toolkit import java.util.Random; public class Talpa extends JLabel { // Le costanti vengono definite come: "static final" private static final int MaxDurataV=2000; private static final int MaxDurataN=1000; private static final int MinDurata=500; private static final int[][] Posiz = { {98, 3}, {304 ,3} , {510 ,3} , {716 ,3}, {98, 131}, {304 ,131} , {510 ,131} , {716 ,131}, {98, 270}, {304 ,270} , {510 ,270} , {716 ,270} }; private final Random randomTalpa; final private Random randomDurataVisib; final private Random randomDurataNasc; private boolean InEsecuzione; private boolean Beccata; private final Cursor cursor; public Talpa() { Toolkit toolKit = Toolkit.getDefaultToolkit(); Image boom = toolKit.getImage("src\\img\\boom.png"); cursor = toolKit.createCustomCursor(boom, new Point(0, 0), "boom"); setVisible(false); // Inizialmente la talpa non è visibile InEsecuzione=false; // e neppure in esecuzione Beccata=false; setSize(115, 123); setIcon(new ImageIcon(toolKit.getImage("src\\img\\talpa.png"))); setCursor(new Cursor(Cursor.HAND_CURSOR)); // ----------------------------------------- // Inizializzo i contatori casuali randomTalpa = new Random(); randomDurataVisib = new Random(); randomDurataNasc = new Random(); } public void Attivata() { InEsecuzione=true; } public void Disattivata() { InEsecuzione=false; setVisible(false); } public void Show(int i) { // Sposto la talpa su una delle buche setLocation(Posiz[i][0], Posiz[i][1]); setVisible(true); } public void Hit() { Beccata=true; // Segno che ho beccato la talpa setCursor(cursor); } public void Vanish() { setCursor(new Cursor(Cursor.HAND_CURSOR)); if (Beccata) // se ho beccato la talpa la nascondo setVisible(false); Beccata=false; } public void Hide() { setVisible(false); } @SuppressWarnings("empty-statement") public void CompariCasualmente() throws InterruptedException { if (!InEsecuzione) return; int Qualebuca=randomTalpa.nextInt(Posiz.length); int millsecN=randomDurataNasc.nextInt(MaxDurataN)+MinDurata; int millsecV=randomDurataVisib.nextInt(MaxDurataV)+MinDurata; Show(Qualebuca); // Thread.sleep(millsecV); // Lascio la talpa visibile per X millisecondi. while (Beccata); // Fino a quando premo il mouse sulla talpa aspetto! Hide(); Thread.sleep(millsecN); } }
Listato del Controllo TERRENO (utilizzato nella VIEW)
import java.awt.*; // Color, Toolkit import javax.swing.*; // JLabel, ImageIcon, BorderFactory public class Terreno extends JLabel { public Terreno() { Toolkit toolKit = Toolkit.getDefaultToolkit(); setIcon(new ImageIcon(toolKit.getImage("src\\img\\sfondo.png"))); /* * Potevo caricare l'icona anche in questo modo. In questo caso * occorre effettuare l'import di: * import javax.imageio.ImageIO * import java.io.*; * try { * setIcon(new ImageIcon(ImageIO.read(new File("img\\sfondo.png")))); * } catch (IOException e) {} */ setBorder(BorderFactory.createMatteBorder(0,1,1,1,Color.black)); setLocation(0,0); setSize(929,477); } }
Listato del Controllo PUNTEGGIO (utilizzato nella VIEW)
import javax.swing.*; // JFrame, JComboBox ... import javax.swing.border.*; import java.awt.*; // Point, Toolkit, ActionListener, GridLayout, Font ... public class Punteggio extends JLabel { public Punteggio() { Font font = new Font("Courier New", Font.BOLD, 20); setText(Integer.toString(0)); setFont(font); setPreferredSize(new Dimension(100,14)); setHorizontalAlignment(JLabel.RIGHT); Border paddingBorder = BorderFactory.createEmptyBorder(10,10,10,10); Border border = BorderFactory.createMatteBorder(1,1,1,1,Color.red); setBorder(BorderFactory.createCompoundBorder(border, paddingBorder)); setOpaque(true); setForeground(Color.YELLOW); setBackground(Color.BLUE); } void SetPunteggio(int p) { setText(Integer.toString(p)); } }
Listato della classe JFrameCentered (la classe VIEW è implementata come una sua estensione)
import javax.swing.*; // JFrame, JComboBox ... import java.awt.*; // Point, Toolkit, ActionListener, GridLayout, Font ... public class JFrameCentered extends JFrame { public JFrameCentered(int frameWidth, int frameHeight, String s) { super(); // Non necessario Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Point center = GraphicsEnvironment.getLocalGraphicsEnvironment().getCenterPoint(); setBounds((int) (screenSize.getWidth() - frameWidth)/2,center.y-frameHeight/2, frameWidth, frameHeight); setTitle(s); setResizable(false); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } }
Lo schema MVC dell'applicazione può essere riassunto con questo schema
Il progetto completo è scaricabile qui.
Link utilizzati:
Blog Giuseppe dell'Abate (ITA) - https://dellabate.wordpress.com/category/gof-pattern
Learn Java Design Patterns (ENG) http://www.tutorialspoint.com/design_pattern/index.htm
Model View Controller Pattern -MVC (ITA) http://www.claudiodesio.com/ooa&d/mvc.htm
Design Patterns - Slide (ITA) http://home.deib.polimi.it/dubois/provafinale/designpatterns.pdf
Una versione multithreaded di ThObservable (ITA) http://www.mokabyte.it/1998/03/foresta.htm
Esercitazioni di Progettazione del Software - A.A.
2012/2013 - Docente: Massimo Mecella
Lezione 8 @ 10 maggio
http://www.dis.uniroma1.it/~mecella/didattica/2013/EsercitazioniProgettazioneSoftware/registro.htm