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.