I COMPONENTI DELLE INTERFACCE UTENTE GRAFICHE: PARTE II
1
3.13 (Caso di studio facoltativo) Pensare a oggetti:
Modello-Vista-Controllore
I design pattern descrivono strategie efficaci per costruire sistemi software orientati agli oggetti affidabili. Il nostro caso di studio utilizza l’architettura Modello-Vista-Controllore (ModelView-Controller, MVC), che usa diversi design pattern. MVC divide i compiti del sistema in
tre parti:
1. il modello, che memorizza i dati e la logica del programma;
2. la vista (o le viste), che fornisce una presentazione visuale del modello, e
3. il controllore, che elabora l’input dell’utente ed esegue delle modifiche al modello.
Usando il controllore, l’utente cambia i dati nel modello MVC. Il modello MVC notifica poi il cambio nei dati alle viste. Le viste cambiano la propria presentazione visuale in modo
da riflettere i cambiamenti nel modello MVC.
Per esempio, nella nostra simulazione, l’utente aggiunge un oggetto Person al modello
MVC premendo i JButton First Floor o Second Floor nel controller. Il modello MVC poi
notifica alla vista che è stato creato un oggetto Person. La vista, in risposta a questa notifica,
visualizza una persona su un piano. Il modello MVC non sa come la vista visualizza la persona, e la vista non sa come o perché il modello MVC ha creato l’oggetto Person.
L’architettura MVC aiuta la costruzione di sistemi affidabili e facilmente modificabili. Se
volessimo usare nella simulazione dell’ascensore un output basato su testo, anziché su
un’interfaccia grafica, potremmo creare una vista alternativa che produce output testuale,
senza alterare il modello MVC o il controllore. Potremmo anche fornire una vista tridimensionale che usa una prospettiva in prima persona per permettere all’utente di partecipare
alla simulazione; tali viste vengono comunemente usate in sistemi di realtà virtuale.
L’applicazione di MVC alla simulazione di ascensore
Applichiamo ora l’architettura MVC alla nostra simulazione di ascensore. Ogni diagramma
UML che abbiamo fornito fino a questo punto modella una porzione del modello MVC del
nostro sistema d’ascensori. La figura 3.21 mostra un modello UML di “alto livello” della
simulazione. La classe ElevatorCaseStudy – una sottoclasse di JFrame – aggrega un’istanza
per ciascuna delle classi ElevatorSimulation, ElevatorView e ElevatorController per
creare l’applicazione ElevatorCaseStudy. In UML, un rettangolo con l’angolo superiore
destro “ripiegato” rappresenta una nota. In questo caso, ogni nota punta ad una classe specifica (con una line tratteggiata), e descrive il suo ruolo nel sistema. Le classi ElevatorSimulation,
ElevatorView e ElevatorController incapsulano tutti gli oggetti che comprendono le porzioni del nostro simulatore che descrivono rispettivamente il modello, la vista e il controllore.
La classe ElevatorSimulation è un’aggregazione di diverse classi (ad esempio,
ElevatorShaft, Elevator e Floor). Per risparmiare spazio, non modelliamo questa aggregazione nella figura 3.21. Anche la classe ElevatorView è un’aggregazione di diverse classi:
per mostrarle, espanderemo il modello UML di ElevatorView nella sezione 8.7. La classe
ElevatorController rappresenta il controllore della simulazione. Notate che la classe
ElevatorView implementa l’interfaccia ElevatorSimulationListener, che permette a
ElevatorView di ricevere eventi dal modello MVC.
2
CAPITOLO 3
javax.swing.JFrame
Applicazione
ElevatorCaseStudy
1
1
1
ElevatorSimulationListener
ElevatorSimulation
Modello MVC
Figura 3.21
1
1
1
ElevatorView
ElevatorController
Vista
Controllore
Diagramma di classe della simulazione d’ascensore
Ingegneria del Software 3.1
Quando appropriato, dividete un modello UML in più diagrammi più piccoli, in modo
che ogni diagramma rappresenti un certo sottosistema.
La classe ElevatorCaseStudy non contiene attributi, a parte i suoi riferimenti ad un
oggetto ElevatorSimulation , ad un oggetto ElevatorView e ad un oggetto
ElevatorController. Il solo comportamento della classe ElevatorCaseStudy è far partire
il programma: quindi, in Java, la classe ElevatorCaseStudy deve contenere un metodo
statico main che istanzia un oggetto ElevatorCaseStudy, che a sua volta istanzia gli oggetti
ElevatorSimulation, ElevatorView e ElevatorController. Implementeremo la classe
ElevatorCaseStudy più avanti in questa sezione.
Manufatti
La figura 3.21 ci aiuta a progettare un altro aspetto del nostro sistema: i manufatti. La figura
14.22 modella i “pezzi”, chiamati manufatti, di cui il sistema ha bisogno per eseguire i propri
compiti. Questi pezzi comprendono programmi eseguibili, file .class, file sorgente .java,
immagini, risorse, ecc.
Nella figura 3.22, ogni rettangolo modella un manufatto. Il nostro sistema controlla
cinque manufatti: E l e v a t o r C a s e S t u d y . c l a s s , E l e v a t o r C a s e S t u d y . j a v a ,
ElevatorSimulation.java, ElevatorView.java ed ElevatorController.java.
Nella figura 3.22, gli elementi grafici che assomigliano a delle cartelle (rettangoli con
eitchette nella parte superiore sinistra) rappresentano dei package nel linguaggio UML (da
non confondere con i package Java). Possiamo raggruppare classi, oggetti, manufatti, casi di
utilizzo, ecc. in package. In questo diagramma (e nel resto del nostro caso di studio), i package
UML corrispondono a package Java. Nella nostra discussione, useremo un tipo di carattere
Courier minuscolo grassetto per i nomi dei package. I package nel nostro sistema sono model
(contiene le classi del modello MVC), view (contiene le classi relative alla vista), e controller
(contiene le classi relative al controllore). Il manufatto ElevatorCaseStudy.java contiene
I COMPONENTI DELLE INTERFACCE UTENTE GRAFICHE: PARTE II
3
model
«executable»
ElevatorCaseStudy.class
«file»
ElevatorSimulation.java
«compilation»
«imports»
ElevatorSimulationListener
view
«file»
«file»
«imports»
ElevatorView.java
ElevatorCaseStudy.java
controller
«imports»
«file»
ElevatorController.java
Figura 3.22
Manufatti della simulazione di ascensore
un’istanza di ognuno dei manufatti in questi package. Attualmente, ogni package contiene
un solo manufatto, ovvero un file .java. Il package model contiene ElevatorSimulation.java,
il package view contiene ElevatorView.java e il package controller contiene
ElevatorController.java. Aggiungeremo manufatti ad ogni package quando implementeremo le varie classi del nostro modello UML come manufatti (file .java).
Ogni freccia tratteggiata nella figura 3.22 indica una dipendenza tra manufatti, in cui la
direzione della freccia indica la relazione “dipende da”. Una dipendenza descrive la relazione tra
manufatti in cui i cambiamenti in un manufatto influiscono su un altro manufatto. Per esempio,
il manufatto ElevatorCaseStudy.class dipende dal manufatto ElevatorCaseStudy.java,
perché un cambiamento di ElevatorCaseStudy.java, al momento della compilazione, influisce su ElevatorCaseStudy.class. L’oggetto ElevatorController contiene un riferimento all’oggetto ElevatorSimulation. Quindi, ElevatorController.java dipende da
ElevatorSimulation.java.
Secondo la figura 3.22, ElevatorSimulation.java e ElevatorView.java non dipendono uno dall’altro; essi comunicano attraverso l’interfaccia ElevatorSimulationListener,
che implementa tutte le interfacce nella simulazione. ElevatorView.java realizza l’interfaccia
ElevatorSimulationListener, e ElevatorSimulation.java dipende dall’interfaccia
ElevatorSimulationListener.
La figura 3.22 contiene diversi stereotipi. Abbiamo parlato dello stereotipo <<JavaInterface>>
nella sezione 1.9. Lo stereotipo <<compilation>> descrive la dipendenza tra
ElevatorCaseStudy.class e ElevatorCaseStudy.java: ElevatorCaseStudy.java viene
compilato in ElevatorCaseStudy.class. Lo stereotipo <<executable>> specifica che un
4
CAPITOLO 3
manufatto è un’applicazione, e lo stereotipo <<file>> specifica che un manufatto è un file
contenente codice sorgente per l’eseguibile.
Implementare il controllore
La nostra simulazione implementa entrambi i casi di utilizzo “Crea Persona” e “Riposiziona
Persona”. Implementiamo ora il caso di utilizzo “Crea Persona” attraverso un’interfaccia utente
grafica. Implementiamo la nostra interfaccia nella classe ElevatorController (figura 3.23),
che è una sottoclasse di JPanel contenente due oggetti JButton: firstControllerButton
(riga 19) e secondControllerButton (riga 20). Ogni JButton corrisponde a un piano su
cui un oggetto Person viene posto (se il palazzo avesse 100 piani, chiaramente questa soluzione sarebbe poco funzionale; si potrebbe invece specificare il piano per mezzo di un campo
di testo). Le righe 33-38 istanziano questi pulsanti e li aggiungono a ElevatorController.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Figura 3.23
// ElevatorController.java
// Controllore per la simulazione d’ascensore
package com.deitel.jhtp5.elevator.controller;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
// package Deitel
import com.deitel.jhtp5.elevator.model.*;
import com.deitel.jhtp5.elevator.event.*;
import com.deitel.jhtp5.elevator.ElevatorConstants;
public class ElevatorController extends JPanel
implements ElevatorConstants {
// il controllore contiene due JButton
private JButton firstControllerButton;
private JButton secondControllerButton;
// riferimento al modello
private ElevatorSimulation elevatorSimulation;
public ElevatorController( ElevatorSimulation simulation )
{
elevatorSimulation = simulation;
setBackground( Color.white );
// aggiunge il primo pulsante al controllore
firstControllerButton = new JButton( “First Floor” );
add( firstControllerButton );
// aggiunge il secondo pulsante al controllore
secondControllerButton = new JButton( “Second Floor” );
add( secondControllerButton );
La classe ElevatorController che elabora l’input dell’utente (continua)
I COMPONENTI DELLE INTERFACCE UTENTE GRAFICHE: PARTE II
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Figura 3.23
5
// classe interna anonima registrata per ActionEvents dal
// primo JButton del controllore
firstControllerButton.addActionListener(
new ActionListener() {
// invocato quando viene premuto un JButton
public void actionPerformed( ActionEvent event )
{
// aggiunge Person al primo piano
elevatorSimulation.addPerson(
FIRST_FLOOR_NAME );
// disabilita input dell’utente
firstControllerButton.setEnabled( false );
}
} // fine classe interna anonima
);
// classe interna anonima registrata per ActionEvents dal
// secondo JButton del controllore
secondControllerButton.addActionListener(
new ActionListener() {
// invocato quando viene premuto un JButton
public void actionPerformed( ActionEvent event )
{
// aggiunge Person al secondo piano
elevatorSimulation.addPerson(
SECOND_FLOOR_NAME );
// disabilita input dell’utente
secondControllerButton.setEnabled( false );
}
} // fine classe interna anonima
);
// classe interna anonima che permette input dell’utente su
// Floor se un oggetto Person entra in Elevator su quel piano
elevatorSimulation.addPersonMoveListener(
new PersonMoveListener() {
// invocato quando Person entra in Elevator
public void personEntered(
PersonMoveEvent event )
{
// ottiene il piano di partenza
String location =
event.getLocation().getLocationName();
// abilita il primo JButton se si parte dal primo piano
La classe ElevatorController che elabora l’input dell’utente (continua)
6
CAPITOLO 3
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
Figura 3.23
if ( location.equals( FIRST_FLOOR_NAME ) )
firstControllerButton.setEnabled( true );
// abilita il primo JButton se si parte dal secondo piano
else
secondControllerButton.setEnabled( true );
} // fine metodo personEntered
// altri metodi che implementano PersonMoveListener
public void personCreated(
PersonMoveEvent event ) {}
public void personArrived(
PersonMoveEvent event ) {}
public void personExited(
PersonMoveEvent event ) {}
public void personDeparted(
PersonMoveEvent event ) {}
public void personPressedButton(
PersonMoveEvent event ) {}
} // fine classe interna anonima
);
} // fine costruttore di ElevatorController
}
La classe ElevatorController che elabora l’input dell’utente
La riga 23 della classe ElevatorController dichiara un riferimento a ElevatorSimulation,
perché ElevatorController permette all’utente di interagire con il modello. Le righe 40-54 e
58-72 dichiarano due oggetti A c t i o n L i s t e n e r anonimi e li registrano presso
firstFloorControllerButton e secondFloorControllerButton, rispettivamente, per eventi
ActionEvent. Quando l’utente preme uno qualsiasi dei JButton, le righe 47-48 e 65-66 dei
metodi actionPerformed chiamano il metodo addPerson di ElevatorSimulation, che
istanzia un oggetto Person in ElevatorSimulation sul piano specificato. Il metodo addPerson
prende come argomento una stringa dichiarata nell’interfaccia ElevatorConstants (figura 3.24).
Quest’interfaccia, usata ad esempio dalle classi ElevatorController, ElevatorSimulation,
Elevator, Floor ed ElevatorView, fornisce delle costanti che specificano i nomi degli oggetti Location nella nostra simulazione.
Le righe 51 e 69 dei metodi actionPerformed disabilitano i rispettivi JButton per
impedire che l’utente crei più di una persona per piano. Le righe 76-114 della classe
ElevatorController dichiarano un oggetto PersonMoveListener anonimo che si registra
presso ElevatorSimulation per ri-abilitare i JButton. Il metodo personEntered (righe
80-95) di PersonMoveListener ri-abilita il JButton associato con il piano servito dall’ascensore:
dopo che la persona è entrata nell’ascensore, l’utente può posizionare un’altra persona sul
medesimo piano.
I COMPONENTI DELLE INTERFACCE UTENTE GRAFICHE: PARTE II
7
Il metodo personEntered di PersonMoveListener impedisce all’utente di creare più
di una persona per piano: pertanto, l’attributo capacity (ereditato dalle classi Elevator e
Floor dalla superclasse Location) non è più necessario. La figura 3.25 mostra il diagramma
di classe modificato che tiene conto della rimozione di questo attributo.
Implementazione: ElevatorCaseStudy.java
Usando i diagrammi delle figure 3.22, 3.21 e 2.28, implementiamo ElevatorCaseStudy
(figura 3.26). Le righe 12-14 importano i package model, view e controller come specificato nella figura 3.22.
1
2
3
4
1
2
3
4
5
6
7
8
9
10
Figura 3.24
// ElevatorCaseStudy.java
// Applicazione simulazione di ascensore con MVC
package com.deitel.jhtp5.elevator;
// ElevatorConstants.java
// Costanti usate tra between ElevatorModel e ElevatorView
package com.deitel.jhtp5.elevator;
public interface ElevatorConstants {
public static final String FIRST_FLOOR_NAME = “firstFloor”;
public static final String SECOND_FLOOR_NAME = “secondFloor”;
public static final String ELEVATOR_NAME = “elevator”;
}
L’interfaccia ElevatorConstants che fornisce le costanti per i nomi
degli oggetti Location
Location
- locationName : String
# setLocationName( String ) : void
+ getLocationName( ) : String
+ getButton( ) : Button
+ getDoor( ) : Door
Elevator
- moving : Boolean = false
- summoned : Boolean = false
- currentFloor : Location
- destinationFloor : Location
- travelTime : Integer = 5
+ ride( ) : void
+ requestElevator( ) : void
+ enterElevator( ) : void
+ exitElevator( ) : void
+ departElevator( ) : void
+ getButton( ) : Button
+ getDoor( ) : Door
Figura 3.25
Floor
+ getButton( ) : Button
+ getDoor( ) : Door
Diagramma di classe modificato che mostra la generalizzazione
della superclasse Location e delle sottoclassi Elevator e Floor
8
CAPITOLO 3
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Figura 3.26
// package Java di base
import java.awt.*;
// package Java di estensione
import javax.swing.*;
// package Deitel
import com.deitel.jhtp5.elevator.model.*;
import com.deitel.jhtp5.elevator.view.*;
import com.deitel.jhtp5.elevator.controller.*;
public class ElevatorCaseStudy extends JFrame {
// modello, vista e controllore
private ElevatorSimulation model;
private ElevatorView view;
private ElevatorController controller;
// il costruttore istanzia modello, vista e controllore
public ElevatorCaseStudy()
{
super( “Deitel Elevator Simulation” );
// istanzia modello, vista e controllore
model = new ElevatorSimulation();
view = new ElevatorView();
controller = new ElevatorController( model );
// registra la vista per gli eventi del modello
model.setElevatorSimulationListener( view );
// aggiunge view e controller a ElevatorSimulation
getContentPane().add( view, BorderLayout.CENTER );
getContentPane().add( controller, BorderLayout.SOUTH );
} // fine costruttore di ElevatorSimulation
// metodo main per far partire il programma
public static void main( String args[] )
{
// istanzia ElevatorSimulation
ElevatorCaseStudy caseStudy = new ElevatorCaseStudy();
caseStudy.setDefaultCloseOperation( EXIT_ON_CLOSE );
caseStudy.pack();
caseStudy.setVisible( true );
}
}
La classe ElevatorCaseStudy è l’applicazione per la simulazione di ascensore
I COMPONENTI DELLE INTERFACCE UTENTE GRAFICHE: PARTE II
9
La figura 3.21 specifica che la classe ElevatorCaseStudy è una sottoclasse di JFrame:
quindi, la riga 16 dichiara la classe ElevatorCaseStudy come una classe public che estende
JFrame. Le righe 19-21 implementano l’aggregazione delle classi ElevatorSimulation,
ElevatorView ed ElevatorController (vedi figura 3.21) nella classe ElevatorCaseStudy,
dichiarando un oggetto per ciascuna classe. Le righe 29-31 del costruttore di ElevatorCaseStudy
inizializzano questi oggetti.
Le figure 3.21 e 3.22 specificano che ElevatorView è un ElevatorSimulationListener
per ElevatorSimulation . La riga 34 registra ElevatorView come ascoltatore per
ElevatorSimulation, in modo che ElevatorView possa ricevere eventi da ElevatorSimulation
e rappresentare lo stato del modello in modo appropriato. Le righe 37-38 aggiungono
ElevatorView ed ElevatorController a ElevatorCaseStudy. Secondo gli stereotipi nella figura 3.22, ElevatorCaseStudy.java viene compilato in ElevatorCaseStudy.class, che
è eseguibile: le righe 43-50 forniscono il metodo main che esegue l’applicazione.