PPS-22-smartgh

4. Design dettagliato

In questo capitolo verrà descritta nel dettaglio l’architettura del sistema, analizzandone i principali componenti e le rispettive caratteristiche.

L’applicazione è costituita da diverse scene, ciascuna delle quali racchiude una propria logica applicativa ed effettua delle elaborazioni a seguito delle azioni compiute dall’utente. Per riuscire a rispettare i requisiti e realizzare un sistema che fosse sufficientemente modulare, facilmente estendibile e quanto più possibile riutilizzabile e manutenibile, si è deciso di utilizzare il Pattern MVC in combinazione al Cake pattern.

Tramite il Pattern MVC, come già descritto nella sezione Sec. 3.2, abbiamo la possibilità di separare la logica di presentazione dei dati da quella di business, realizzando una View del tutto indipendente dal modello. In questo modo, se in un futuro si decidesse di adottare una tecnologia diversa da ScalaFX per l’implementazione della View, si potrebbe tranquillamente intraprendere questo cambiamento, senza dover modificare il Model associato alle diverse schermate.

Il Cake pattern, invece, ci dá la possibilità di risolvere in modo agevole le dipendenze che legano gli elementi dell’MVC tramite l’utilizzo di meccanismi della programmazione funzionale come: mix-in, self-type, type members, ecc…

Questa strategia, sostanzialmente, prevede di implementare il pattern MVC come una composizione di tre elementi: Model (M), View (V) e Controller (C), i quali presentano le seguenti dipendenze: C->V, V->C e C->M. Più precisamente, possiamo realizzare questi tre elementi incapsulando già al loro interno la risoluzione delle dipendenze precedentemente citate: alla fine, potremo istanziare un oggetto MVC che li detiene tutti e tre e che è in grado di accedere alle rispettive proprietà, senza doversi preoccupare del loro collegamento.

Gli elementi Model, View e Controller vengono racchiusi in moduli, composti da:

Fig. 4.1 - Model module

Fig. 4.2 - View module

Fig. 4.3 - Controller module

Tutti gli elementi principali dell’applicazione, che richiedono di eseguire operazioni o di elaborare informazioni e fornire risultati a seguito delle azioni compiute dall’utente, sono stati realizzati seguendo questa strategia e nelle seguenti sezioni verranno descritti con maggiore dettaglio.

4.1 View e Controller: elementi comuni

I componenti, descritti nelle successive sottosezioni, fattorizzano elementi comuni del codice e permettono di evitarne la ripetizione.

ViewComponent

ViewComponent è un’interfaccia generica che rappresenta un componente della View e, come si può vedere dalla figura (vedi Fig. 4.1.1), richiede che il tipo generico A sia sottotipo di Parent. Quest’ultimo è la classe base dei nodi con figli di JavaFx.

Per l’implementazione di ViewComponent si è rispettato il pattern Template Method, definendo una classe astratta AbstractViewComponent dove è contenuto il template dei componenti. In tale classe viene incapsulata la logica necessaria per il caricamento dei layout e per la loro inizializzazione, lasciando alle sottoclassi la definizione del file FXML associato.

Tutte le View estenderanno da tale classe, in modo da creare componenti modulari ed evitare ripetizioni del codice nel caricamento dei layout.

Fig. 4.1.1 - View Component

ContiguousSceneView

ContiguousSceneView (vedi Fig. 4.1.2) è un’interfaccia generica che risulta utile per definire un componente della View che ha la necessità di richiedere al proprio Controller di effettuare operazioni particolari prima di notificare la View principale di visualizzare la nuova scena.

Tale interfaccia richiede che il nuovo elemento View da impostare sia di un tipo generico A sottotipo di Parent, ossia la classe base dei nodi con figli di JavaFX.

Fig. 4.1.2 - ContiguousSceneView

SceneController

Gli elementi comuni ai diversi Controller sono stati racchiusi all’interno dell’interfaccia SceneController (vedi Fig. 4.1.3), contenente il metodo beforeNextScene che si occupa di eseguire le operazioni che devono essere effettuate, prima di poter effettuare il cambio di scena.

Fig. 4.1.3 - SceneController

4.2 Gestione della simulazione

La struttura articolata dell’applicazione ha introdotto la necessità di sviluppare un elemento che coordinasse i vari componenti Model, View e Controller, collocandosi ad un livello superiore. Nella sezione seguente si discuterà il design di tale elemento.

SimulationMVC

SimulationMVC (vedi Fig. 4.2.1) rappresenta l’elemento MVC principale della simulazione. Ad alto livello, questo componente si colloca al di sopra di tutti gli altri in quanto permette di:

L’Interface di SimulationMVC sarà estesa dalla maggior parte dei componenti MVC del progetto.

In particolare, la classe SimulationMVC racchiude i sottocomponenti SimulationView e SimulationController, derivanti dai rispettivi moduli. Come si può vedere dalla rappresentazione, SimulationMVC non racchiude un componente di tipo Model in quanto questo aspetto viene gestito da altri componenti MVC.

Fig. 4.2.1 - SimulationMVC

View della simulazione

Il SimulationViewModule (vedi Fig. 4.2.2) rappresenta la View principale dell’applicazione e si occupa di gestire: la scena, le sotto-view e gli elementi comuni alle interfacce.

Al suo interno troviamo il trait SimulationView, il quale include i metodi utili per l’avvio dell’applicazione, per gestire gli elementi comuni delle schermate e per passare da una sotto-view all’altra.

Quando l’applicazione viene lanciata, viene creato prima di tutto il componente base dell’applicazione, rappresentato dall’elemento BaseView. Quest’ultimo è il componente che funge da contenitore delle sotto-view, che racchiude gli elementi comuni a tutte le pagine e che fornisce i metodi per gestirli.

Fig. 4.2.2 - SimulationViewModule

Controller della simulazione

Il controller della simulazione (vedi Fig. 4.2.3) è stato racchiuso nel SimulationControllerModule che si compone, in particolare, del trait SimulationController, il quale espone:

Fig. 4.2.3 - SimulationController

4.3 Impostazione dei parametri della simulazione

Uno dei requisiti dell’applicazione è quello di permettere all’utente di personalizzare la simulazione (vedi requisito n°1 in sezione Sec. 2.2), impostando:

Al fine di soddisfare queste funzionalità, sono stati sviluppati i seguenti elementi dell’architettura.

4.3.1 Selezione della città

La prima schermata che viene presentata all’utente è quella per la selezione della città, nella quale verranno mostrate una serie di località selezionabili, permettendo di effettuare una ricerca con auto-completamento del testo.

Considerando che la realizzazione di questa funzionalità richiede sia una View che un Model con cui ottenere i dati delle città, si è deciso di seguire il Pattern MVC e il Cake pattern, realizzando l’elemento SelectCityMVC con i rispettivi sotto moduli SelectCityModelModule, SelectCityControllerModule, SelectCityViewModule.

Fig. 4.3.1.1 - MVC per la selezione della città

Model per la selezione della città

Il Model per la selezione della città viene racchiuso all’interno del modulo SelectCityModelModule, costituito, in particolare, dal trait SelectCityModel che espone i diversi metodi utili per effettuare la ricerca delle città e per la verifica della sua esistenza.

Fig. 4.3.1.2 - Model per la selezione della città

Controller per la selezione della città

Il Controller per la selezione della città è racchiuso all’interno del modulo SelectCityControllerModule (vedi Fig. 4.3.1.3) e comprende il trait SelectCityController, il quale rappresenta l’interfaccia del Controller ed espone dei metodi per rispondere alle esigenze della View per interagire con il Model.

Nello specifico, il Controller presenta metodi per:

Fig. 4.3.1.3 - Controller per la selezione della città

Environment

Environment (vedi Fig. 4.3.1.4) è la componente del sistema che rappresenta l’ubicazione della serra. Il suo scopo è quello di, una volta selezionata la città, reperire le previsioni meteorologiche previste per la giornata in cui si svolge la simulazione.

Le informazioni così ottenute vengono poi messe a disposizione dell’applicazione al fine di aggiornare i parametri ambientali durante tutto lo svolgimento della stessa. I parametri ambientali influenzeranno i parametri rilevati all’interno di ogni area, secondo le formule implementate in ogni sensore.

Fig. 4.3.1.4 - Architettura del componente Environment

View per la selezione delle città

La View per la selezione delle città viene racchiusa nel modulo SelectCityViewModule (vedi Fig. 4.3.1.5).

Al suo interno troviamo il trait SelectCityView, il quale rappresenta l’interfaccia della View e detiene metodi che possono essere richiamati sulla stessa. Tale interfaccia espone un metodo per settare il messaggio di errore da mostrare all’utente e un metodo per passare alla scena successiva.

La classe SelectCityViewImpl, invece, è l’implementazione dell’interfaccia, e rappresenta anche il Controller dell’FXML associato. Infatti, estendendo da AbstractViewComponent, contiene già la logica necessaria al caricamento del file.

Fig. 4.3.1.5 - View per la selezione della città

4.3.2 Selezione delle piante

Per poter realizzare il meccanismo di selezione delle piante si è deciso di adottare, come già detto precedentemente, il Pattern MVC e il Cake pattern.

In particolare, come si può vedere dalla figura Fig. 4.3.2.1, PlantSelectorMVC racchiude i componenti plantSelectorModel, plantSelectorController e selectPlantView, derivanti dai rispettivi moduli.

Fig. 4.3.2.1 - MVC per la selezione delle piante

Model per la selezione delle piante

Il Model per la selezione delle piante (vedi Fig. 4.3.2.2) viene racchiuso all’interno di un modulo chiamato PlantSelectorModelModule, nello specifico all’interno del suddetto modulo troviamo il trait PlantSelectorModel, il quale espone i diversi metodi che potranno essere richiamati sul Model e che consentono la gestione del meccanismo di selezione delle piante.

Fig. 4.3.2.2 - Model per la selezione delle piante

Il Model ha come obiettivo principale quello di mantenere sempre aggiornata la lista delle piante selezionate dall’utente. Per fare questo, è necessario che il Controller lo informi ogni qual volta l’utente compia un’azione relativa alla selezione delle piante.

La lista di piante rappresenta un elemento osservabile dal Controller: infatti, ogni qual volta viene aggiunto o rimosso un elemento a questa lista, il Controller viene notificato e si occupa di propagare tale informazione alla View. Il Controller, richiamando il metodo registerCallbackPlantSelection, si registra all’Observable della lista delle piante e specifica quali siano le azioni che devono essere intraprese quando:

Infine, il Model, una volta che l’utente decide di dare il via alla simulazione, si occupa di istanziare gli oggetti Plant, rappresentanti le piante scelte e contenenti tutte le diverse informazioni utili per la loro gestione.

View per la selezione delle piante

La View per la selezione delle piante (vedi Fig. 4.3.2.3) viene racchiusa all’interno del modulo SelectPlantViewModule in cui possiamo trovare il trait SelectPlantView, che detiene i diversi metodi che potranno essere richiamati sulla View e che si occupano di gestire l’interazione con l’utente.

Tale trait rappresenta il Controller dell’FXML per la rispettiva schermata ed, inoltre, implementa le interfacce ViewComponent e ContiguousSceneView.

Fig. 4.3.2.3 - View per la selezione delle piante

La View per la selezione delle piante, inizialmente, si occuperà di mostrare le piante selezionabili dall’utente, ottenendole dal Controller; dopodiché, si occuperà di notificare il Controller ogni qual volta l’utente compirà un’azione di selezione o di deselezione. Nel caso in cui il Controller notifichi un errore, la View si occuperà di mostrare un messaggio all’utente.

Controller per la selezione delle piante

Il Controller per la selezione delle piante (vedi Fig. 4.3.2.4) è stato racchiuso all’interno del modulo PlantSelectorControllerModule, al cui interno troviamo il trait PlantSelectorController.

Tale trait estende l’interfaccia SceneController, contenente i metodi comuni a tutti i Controller, e detiene i diversi metodi che potranno essere richiamati per la gestione della selezione delle piante.

Fig. 4.3.2.4 - Controller per la selezione delle piante

Inizialmente, il Controller si occupa di impostare la schermata di selezione delle piante richiedendo al Model la lista di piante selezionabili e alla View di mostrarle all’utente.

Dopodiché, il suo compito principale consiste nel notificare il Model ogni qual volta l’utente compia un’azione di selezione o deselezione per una specifica pianta e, nel caso in cui si verifichi una situazione di errore, nel richiedere alla View di mostrare all’utente un apposito messaggio.

Plant

Il trait Plant (vedi Fig. 4.3.2.5) espone dei metodi per ottenere le informazioni principali rispetto alle piante selezionate dall’utente: queste verranno visualizzate all’interno delle aree e verranno utilizzate per monitorare i parametri vitali delle stesse.

Il companion object Plant permette di creare un’istanza della pianta che verrà salvata all’interno del SimulationController, col fine di renderla accessibile ai componenti del sistema che necessitano delle informazioni relative alle piante scelte.

Fig. 4.3.2.5 - Architettura del componente Plant

4.4 Caricamento dei dati delle piante

Una volta che l’utente ha provveduto a selezionare le piante che intende coltivare all’interno della serra e ha richiesto l’avvio della simulazione, l’applicazione provvede a raccogliere tutti i dati relativi alle piante e ai loro parametri ottimali.

Per poter raccogliere tali informazioni, l’applicazione impiega un certo tempo: di conseguenza, per mantenere l’interfaccia reattiva e fornire all’utente informazioni relative ai compiti che il sistema sta svolgendo in tale istante, si è deciso di inserire un componente intermedio che mostri il caricamento dei dati.

A tal proposito, è stato realizzato l’elemento LoadingPlantMVC, il quale racchiude i componenti del Pattern MVC dedicati al caricamento dei dati delle piante (vedi Fig. 4.4.1): PlantSelectorModelModule, LoadingPlantControllerModule e LoadingPlantViewModule. Di conseguenza, risulta che il Model del LoadingPlantMVC è lo stesso di PlantSelectorMVC in quanto detiene già le informazioni relative alle piante selezionate dall’utente e può essere utilizzato per istanziare l’oggetto Plant.

Fig. 4.4.1 - MVC per il caricamento dei dati delle piante

Dato che il Model è già stato discusso nel precedente paragrafo (Model per la selezione delle piante), di seguito verranno discussi solamente i componenti View e Controller per il caricamento dei dati.

View per il caricamento dei dati delle piante

La View per il caricamento dei dati delle piante (vedi Fig. 4.4.2) si trova all’interno del modulo LoadingPlantViewModule al cui interno troviamo il trait LoadingPlantView, che contiene i metodi della View che possono essere richiamati per gestire l’interazione con l’utente.

LoadingPlantView estende sia l’interfaccia ViewComponent che l’interfaccia ContiguousSceneView, in quanto rappresenta una scena che viene inserita all’interno di quella madre e consente il proseguimento alla scena successiva. Inoltre, la View per poter svolgere le proprie funzioni necessita anche di accedere alle proprietà di SimulationMVC.

Fig. 4.4.2 - View per il caricamento dei dati delle piante

Questa View presenta un ProgressIndicator che viene incrementato di volta in volta, a mano a mano che i diversi dati delle piante vengono caricati e i rispettivi oggetti Plant vengono istanziati. Una volta che il caricamento dei dati risulta essere completato, si può passare alla schermata successiva.

Controller per il caricamento dei dati delle piante

Il Controller per il caricamento dei dati delle piante si trova all’interno del modulo LoadingPlantControllerModule e, nello specifico, all’interno del suddetto modulo troviamo il trait LoadingPlantController, il quale estende l’interfaccia SceneController e detiene i diversi metodi che potranno essere richiamati per svolgere le funzioni intermediarie fra View e Model.

Fig. 4.4.3 - Controller per il caricamento dei dati delle piante

Come possibile vedere dalla figura Fig. 4.4.3, il LoadingPlantController presenta un unico metodo setupBehaviour, il quale si occupa di registrare la callback sul Model relativa al caricamento dei dati delle piante. Difatti, all’interno di questo metodo viene richiamata la funzione registerCallbackPlantInfo di PlantSelectorModel in cui viene specificato quali siano le azioni da intraprendere quando:

Ne risulta che quando verrà prodotta una nuova pianta il Controller richiamerà il metodo incrementProgressIndicator della View e che, quando il caricamento dei dati delle piante risulterà essere completato, il Controller richiederà alla View di passare alla schermata successiva.

4.5 Avvio Simulazione

Una volta che l’utente ha personalizzato i parametri della simulazione e sono state caricate le informazioni relative alle piante, viene avviata la simulazione e, in particolare, vengono inizializzati gli elementi utili alla visualizzazione dello stato globale della serra, delle variazioni ambientali esterne e dello scorrere del tempo.

Nello specifico, in questa sezione verranno discussi gli elementi architetturali che soddisfano il requisito utente n°2 (vedi Sec. 2.2).

4.5.1 Variazioni ambientali

Per poter realizzare la visualizzazione delle variazioni ambientali esterne nell’arco della giornata si è introdotto l’elemento EnvironmentMVC (vedi Fig. 4.5.1.1), sviluppato mediante il Cake Pattern, che racchiude i componenti Model, View e Controller responsabili dell’aggiornamento dei valori ambientali.

Fig. 4.5.1.1 - Architettura di EnvironmentMVC

Model per l’aggiornamento dei valori ambientali

Il Model viene racchiuso nel EnvironmentModelModule (vedi Fig. 4.5.1.2), al cui interno troviamo il trait EnvironmentModel, che espone metodi per:

Fig. 4.5.1.2 - Architettura di EnvironmentModel

View per l’aggiornamento dei valori ambientali

La View viene racchiusa nel EnvironmentViewModule (vedi Fig. 4.5.1.3), al cui interno troviamo il trait EnvironmentView, che espone i metodi per:

Tale trait estende ViewComponent in quanto rappresenta una scena inserita all’interno di quella madre.

Invece, l’oggetto context di tipo Requirements specifica quali siano le dipendenze che devono essere soddisfatte affinché la View possa lavorare correttamente. Nello specifico, ha bisogno dell’EnvironmentController per notificarlo delle interazioni dell’utente (es: modifica della velocità della simulazione), e del SimulationMVC per accedere al suo elemento View e notificarlo di passare ad una nuova scena (es: scena di fine simulazione) oppure di modificare lo stile di un elemento grafico comune (es: pulsante comune a tutte le View).

Fig. 4.5.1.3 - Architettura di EnvironmentView

Controller per l’aggiornamento dei valori ambientali

Il Controller è racchiuso all’interno del modulo EnvironmentControllerModule (vedi Fig. 4.5.1.4), al cui interno troviamo il trait EnvironmentController, che espone i metodi per:

L’oggetto context di tipo Requirements specifica quali siano le dipendenze che devono essere soddisfatte affinché il controller possa lavorare correttamente (es: EnvironmentView per richiedere la visualizzazione del tempo trascorso, EnvironmentModel per richiedere l’aggiornamento dei valori ambientali, SimulationMVC per controllare la componente tempo).

Fig. 4.5.1.4 - Architettura di EnvironmentController

4.5.2 Tempo virtuale

TimeModel (vedi Fig. 4.5.2.1) è un trait che espone metodi per gestire il tempo virtuale della simulazione, il quale è modellato, a sua volta, dal trait Timer.

TimeModel e Timer rappresentano anche due companion object che racchiudono l’implementazione delle rispettive interfacce e possono essere utilizzati per inizializzarne un’istanza.

In particolare, il Model si occupa di avviare e stoppare il Timer, oltre che di modificarne la velocità: queste operazioni vengono richiamate dal SimulationController, a seguito di feedback ricevuti dall’EnvironmentMVC.

All’avvio del Timer, il TimeModel ha il compito di specificare i task da eseguire ad ogni tick e al concludersi del tempo stabilito per la simulazione. A questo scopo, il Model detiene un riferimento al SimulationController che utilizzerà per notificarlo del valore timeValue aggiornato, dello scoccare di una nuova ora (al fine di aggiornare i currentEnvironmentValues) e dell’esaurimento del tempo della simulazione.

Fig. 4.5.2.1 - Architettura per la gestione del tempo virtuale

4.6 Serra

In questa sezione vengono descritti i componenti necessari a soddisfare i seguenti requisiti utente (vedi requisiti n° 3,4,5,6 in sezione Sec. 2.2):

4.6.1 Suddivisione in aree

Per poter realizzare la suddivisione in aree si è deciso di adottare, come detto precedentemente, il Pattern MVC e il Cake pattern.

In particolare, come si può vedere nella figura Fig. 4.6.1.1, la classe GreenHouseMVC racchiude i componenti: GreenHouseModel, GreenHouseController e GreenHouseView derivanti dai rispettivi moduli.

Tale classe verrà istanziata all’interno dell’EnvironmentController e si occuperà di creare gli MVC delle singole aree, assegnando ad ognuno di esse una pianta tra quelle selezionate dall’utente.

Fig. 4.6.1.1 - MVC per la suddivisione in aree

Model per la suddivisione in aree

Il Model viene racchiuso nel suo rispettivo modulo GHModelModule (vedi Fig. 4.6.1.2), al cui interno troviamo il trait GreenHouseModel che espone il metodo per ottenere la lista dei componenti MVC delle singole aree che compongono la serra (areas, vedi sezione).

Il Model, quindi, ha l’obiettivo di memorizzare la lista dei singoli MVC di cui è composta la serra.

Fig. 4.6.1.2 - Model per la suddivisione in aree

View per la suddivisione in aree

La View viene racchiusa nel modulo GHViewModule (vedi Fig. 4.6.1.3), al cui interno troviamo il trait GHDivisionView, che definisce i metodi che possono essere richiamati sulla View e, nello specifico, quello per richiedere di ripulire e disegnare lo spazio di interfaccia relativa alla visualizzazione della composizione della serra.

Questa interfaccia rappresenta inoltre il Controller dell’FXML per la relativa sezione: infatti, bisogna ricordare che la ghDivisionView è racchiusa all’interno della più ampia View che è EnvironmentView.

Inoltre, per poter essere inserita all’interno della scena principale come le altre View, il trait implementa ViewComponent.

La View ha come ruolo principale quello di mostrare e mantenere aggiornata la suddivisione della serra in aree. Questo obiettivo viene raggiunto mediante il metodo printDivision. Quest’ultimo verrà richiamato sia all’avvio della schermata dell’EnvironmentMVC che ad ogni intervallo di tempo (per aggiornare i valori rilevati all’interno delle aree o quando lo stato di un’area cambia e passa da NORMALE ad ALLARME).

Fig. 4.6.1.3 - View per la suddivisione in aree

Controller per la suddivisione in aree

Il Controller viene racchiuso all’interno del modulo GHControllerModule (vedi Fig. 4.6.1.4), il quale include il trait GreenHouseController, che definisce i metodi che possono essere richiamati sul Controller per:

Il compito principale del Controller è quello di richiedere l’aggiornamento della View affinché questa mostri lo stato delle aree e i rispettivi valori rilevati all’interno.

Fig. 4.6.1.4 - Controller per la suddivisione in aree

Aree

Per realizzare le singole aree che compongono la serra si è deciso di implementare ancora una volta il Pattern MVC e il Cake pattern.

In particolare, come si può vedere nella Fig. 4.6.1.5 , la classe AreaMVC racchiude i componenti: AreaModel, AreaController e AreaView derivanti dai rispettivi moduli; inoltre, racchiude all’interno del proprio contesto l’istanza corrente del SimulationMVC in modo tale che questa sia accessibile sia dalla View che dal Controller.

Tale classe verrà istanziata durante il setup della divisione della serra e memorizzata all’interno del greenHouseModel.

Fig. 4.6.1.5 - Rappresentazione MVC di un'area

Model della singola area

Il Model viene racchiuso nel rispettivo modulo AreaModelModule (Fig. 4.6.1.6), al cui interno troviamo il trait AreaModel che espone i diversi metodi che potranno essere richiamati sul Model per:

Il Model dell’area ha come principale obiettivo quello di memorizzare lo stato dell’area e le operazioni effettuate dagli utenti sui singoli sensori. Per raggiungere questo obiettivo, si appoggia sulle classi ManageSensor e AreaSensorHelper che si occuperanno di memorizzare e gestire i singoli sensori, e sull’oggetto AreaComponentState, il quale memorizza le operazioni effettuate dall’utente.

Il Model, come si può intuire, risulta essere condiviso con l’MVC del dettaglio dell’area in quanto è necessario poter ricondurre le operazioni dell’utente all’area su cui le ha effettuate.

Fig. 4.6.1.6 - Model dell'area

View della singola area

La View viene racchiusa nel modulo AreaViewModule (Fig. 4.6.1.7), al cui interno troviamo il trait AreaView, che definisce i metodi che possono essere richiamati sulla View per richiedere il disegno dell’area.

Questa interfaccia rappresenta, inoltre, il Controller dell’FXML per la relativa sezione: infatti, bisogna ricordare che la AreaView è racchiusa all’interno della più ampia View che è GHDivisionView.

Questo trait, come gli altri, per poter essere inseriti all’interno della scena principale, implementa ViewComponent . Oltre a ciò, implementa anche ContiguousSceneView per permettere di passare alla scena incaricata di mostrare il dettaglio dell’area.

La View ha come ruolo principale quello di mostrare lo stato di un’area, il nome della pianta e i valori dei parametri rilevati all’interno di essa; inoltre, dà la possibilità all’utente di accedere al dettaglio dell’area selezionata.

Fig. 4.6.1.7 - View dell'area

Controller della singola area

Il Controller viene racchiuso all’interno del modulo AreaControllerModule (vedi Fig. 4.6.1.8), il quale include il trait AreaController che definisce i metodi che possono essere richiamati sul Controller per richiamare il disegno dell’area.

Il trait estende SceneController, necessario per poter accedere alla scena che mostra il dettaglio dell’area.

Il compito principale del Controller è quello di richiamare la creazione dell’interfaccia grafica delegata alla View affinché questa mostri lo stato delle aree e i rispettivi valori rilevati al loro interno, oltre a gestire il cambio di scena da quella generale a quella specifica della singola area.

Fig. 4.6.1.8 - Controller dell'area

4.7 Visualizzazione del dettaglio di un’area

4.7.1 Dettaglio di un’area

Per realizzare il dettaglio delle aree si è deciso di implementare ancora una volta il Pattern MVC e il Cake pattern.

In particolare, come si può vedere nella Fig. 4.7.1.1, la classe AreaDetailsMVC racchiude i componenti: AreasModel, AreaDetailsController e AreaDetailsView derivanti dai rispettivi moduli.

Tale classe verrà istanziata nel momento in cui un utente decide di visionare il dettaglio di un’area, scelta tra quelle che compongono la serra.

Fig. 4.7.1.1 - Rappresentazione MVC del dettaglio di un'area

Model del dettaglio area

Come si può vedere nella Fig. 4.7.1.1, il Model è lo stesso implementato per le singole aree poiché risulta necessario che vengano memorizzate le operazioni effettuate dull’utente. In questo modo sarà possibile aggiornare, con una determinata frequenza, il valore rilevato dai sensori.

Per questo motivo si rimanda al paragrafo Aree per i dettagli.

Il Model in questione risulterà essere condiviso anche con gli MVC che gestiscono la parte visuale dei sensori presenti all’interno delle aree (Sec. 4.7.2).

View del dettaglio di un’area

La View del dettaglio di un’area viene racchiusa nell’AreaDetailsViewModule, come raffigurato nella figura Fig. 4.7.1.2.

Oltre agli elementi necessari al Cake pattern, all’interno troviamo il trait AreaDetailsView, il quale estende da ViewComponent e anche da ContiguousSceneView in quanto richiede delle operazioni specifiche prima di passare alla scena successiva, come verrà descritto nel paragrafo successivo relativo al modulo del Controller. Inoltre, espone metodi per consentire l’aggiornamento delle informazioni della View relative:

Fig. 4.7.1.2 - View del dettaglio di un'area

Controller del dettaglio area

Il Controller viene racchiuso all’interno del modulo AreaDetailsControllerModule (Fig. 4.7.1.3), il quale include il trait AreaDetailsController che definisce i metodi che possono essere richiamati sul Controller ed, in particolare, quello per inizializzare la View.

Il trait estende SceneController, necessario per poter ritornare alla schermata principale dell’applicazione e per terminare l’aggiornamento delle informazioni alla View.

Il compito principale del Controller è quello di richiamare la creazione dell’interfaccia grafica rappresentante il dettaglio dell’area. Per assolvere a tale compito il Controller provvede, mediante la classe di utility AreaSensorHelper, alla creazione degli MVC incaricati della gestione dei sensori presenti all’interno dell’area e specificatamente:

Fig. 4.7.1.3 - Controller del dettaglio di un'area

4.7.2 Parametri dell’area

Con l’obiettivo di rendere l’applicazione modulare e scalabile all’aggiunta di nuovi sensori, si è deciso di separare il componente per la gestione del dettaglio di un’area dalla visualizzazione dei valori ottenuti dai sensori e le rispettive azioni che possono essere compiute.

Ciascun componente è rappresentato da un modulo MVC separato, composto da una propria View e un proprio Controller. Il Model rimane quello implementato per le singole aree e per il dettaglio dell’area descritto nel paragrafo Aree.

I comportamenti dei vari parametri sono stati raccolti in interfacce comuni:

MVC dei parametri

Il trait AreaParameterMVC, come mostrato in figura Fig. 4.7.2.1, rappresenta l’interfaccia generale dei componenti MVC dei parametri. In particolare, contiene tre campi che sono necessari alla composizione dell’elemento:

Fig. 4.7.2.1 - Trait MVC dei parametri

View dei parametri

Il trait AreaParameterView (vedi Fig. 4.7.2.2) espone i metodi che consentono l’aggiornamento del valore corrente e la descrizione del parametro.

Di tale interfaccia è stata poi definita una classe astratta AbstractAreaParameterView che implementa i metodi comuni dei parametri. Dato l’utilizzo del template method, lascia la definizione delle variabili descriptionLabel e currentValueLabel, ovvero le label dedicate alla descrizione e al valore corrente, alle sottoclassi che la estendono. La classe astratta si occuperà di aggiornare queste informazioni, incapsulandone la logica.

Fig. 4.7.2.2 - Trait view dei parametri

Controller dei parametri

Il trait AreaParameterController fornisce metodi per l’inizializzazione e l’interruzione dell’aggiornamento dei parametri della View.

Come è possibile vedere nella figura Fig. 4.7.2.3, anche per questa interfaccia è presente una classe astratta AbstractAreaParameterController, utile a fattorizzare le parti in comune ai parametri. Seguendo anche qui il template method, abbiamo che i metodi da implementare nelle sotto-classi sono:

Fig. 4.7.2.3 - Trait controller dei parametri

Singoli parametri

Come formulato nei requisiti, l’applicazione prevede di avere quattro sensori che permettono di rilevare in ogni area i seguenti parametri: luminosità, temperatura, umidità del suolo e dell’aria.

Ognuno di questi è realizzato seguendo il Cake pattern, implementando le interfacce comuni ed estendendo le relative classi astratte descritte precedentemente.

Il compito principale di questi componenti è quello di definire le azioni che possono essere svolte all’interno dell’area e che andranno ad influenzare il valore del rispettivo parametro.

In particolare, la View si occuperà di gestire gli elementi grafici delle azioni mentre, il Controller, di gestire il loro comportamento e il cambiamento dello stato del Model.

Nello specifico, abbiamo i seguenti componenti:

4.8 Sensori

Come detto precedentemente, ogni area è monitorata da dei sensori. Per il progetto non ne sono stati utilizzati dei veri e propri ma bensì simulati ed emulati tramite software.

In particolare, il codice dei sensori rientra nel package model del progetto in quanto essi possono essere sfruttati dai diversi componenti Model dell’applicazione, racchiudendo la logica di aggiornamento e notifica dei nuovi valori rilevati.

Fig. 4.8.1 - Interfacce Sensor e SensorWithTimer

Prima di tutto, per poter realizzare i sensori si è deciso di analizzare quali siano gli aspetti comuni che questi presentano e di raccoglierli all’interno di un interfaccia comune.

Il trait Sensor (Fig. 4.8.1) rappresenta proprio l’interfaccia che assolve a questo scopo e al suo interno troviamo la dichiarazione dei metodi:

Una volta racchiusi gli aspetti comuni dei sensori all’interno dell’interfaccia Sensor, ci si è interrogati su come l’aggiornamento e l’emissione dei valori rilevati dai sensori dovesse avvenire. Si è giunti alla conclusione che esistono due tipologie di sensori: quelli che effettuano un’aggiornamento periodico del valore rilevato, rappresentati dall’interfaccia SensorWithTimer, e quelli che cambiano il valore rilevato istantaneamente, al verificarsi di determinate condizioni.

L’interfaccia SensorWithTimer estende l’interfaccia Sensor e rispetto al sensore normale, effettua l’aggiornamento del parametro rilevato periodicamente, emettendo di volta in volta un nuovo valore.

A tal fine, SensorWithTimer richiede l’implementazione di un unico metodo registerTimerCallback, il quale consente al sensore di specificare al timer della simulazione il tempo virtuale che deve trascorrere tra un aggiornamento e l’altro.

Ogni qual volta il sensore riceve l’evento dal timer, che lo informa del fatto che il tempo specificato è trascorso, esso si occuperà di rilevare il nuovo valore e di emetterlo sul flusso dell’Observable dedicato, in modo tale da informare l’area del nuovo parametro rilevato.

Le classi Sensor e SensorWithTimer vengono inizialmente implementate dalle classi astratte AbstractSensor e AbstractSensorWithTimer. Conseguentemente, viene lasciato alle classi dei sensori solamente l’implementazione del metodo ` protected computeNextSensorValue. Questa implementazione utilizza, quindi, il pattern _template method_ in quanto le classi astratte rappresentano il template dei sensori e il metodo computeNextSensorValue` contiene il comportamento che le sottoclassi devono implementare.

4.8.1 Sensore per la luminosità

Fig. 4.8.1.1 - Sensore della luminosità

Il sensore della luminosità (vedi Fig. 4.8.1.1) non è un sensore periodico: difatti, esso implementa solamente l’interfaccia Sensor ed estende la classe astratta AbstractSensor, la quale racchiude già l’implementazione di alcuni metodi dell’interfaccia.

Il sensore della luminosità, quindi, è un sensore istantaneo: non appena ha luogo un cambiamento della luminosità esterna o viene compiuta un’azione da parte dell’utente, il sensore cambierà subito il suo valore, senza aspettare un aggiornamento periodico, in quanto la velocità con cui la luce cambia all’interno di un ambiente è molto rapida rispetto a quella che può essere la velocità di aggiornamento della temperatura. Ad esempio, se immaginassimo di trovarci in una stanza buia in cui vi è una temperatura più bassa rispetto a quella esterna e decidessimo di aprire la finestra, la luce entrerà subito mentre la temperatura interna impiegherà diverso tempo per alzarsi.

Per poter calcolare correttamente il valore della luminosità, bisogna tenere conto delle azioni che l’utente può compiere, come:

Per poter determinare il valore del parametro rilevato rispetto all’attuale stato dell’area, è stato definito l’oggetto FactoryFunctionsLuminosity (vedi Fig. 4.8.1.1), il quale rappresenta una factory di funzioni che possono essere utilizzate per determinare il nuovo valore rilevato dal sensore.

Ogni qual volta l’utente compie una nuova azione o viene rilevato un nuovo parametro ambientale, a seconda dello stato in cui si trovano i componenti della serra, si richiama la funzione della factory corrispondente, in modo da determinare il nuovo valore rilevato.

Nello specifico, abbiamo detto nella precedente sezione Sec. 4.8 che ogni sensore presenta due Observer: uno che viene notificato ogni qual volta un nuovo valore ambientale viene rilevato e l’altro che viene notificato ogni qual volta l’utente compie una nuova azione sull’area. Quando uno di questi due eventi si verifica, il sensore controlla lo stato attuale dei componenti dell’area e, successivamente, sceglie la funzione da applicare per calcolare il nuovo valore; infine, emette questo nuovo valore sul flusso dell’Observable.

4.8.2 Sensore per la temperatura

Il sensore della temperatura è un sensore dotato di timer, pertanto si occupa di aggiornare periodicamente il valore rilevato.

Come si può vedere dalla figura Fig. 4.8.2.1, il sensore implementa l’interfaccia SensorWithTimer tramite la classe astratta AbstractSensorWithTimer.

Fig. 4.8.2.1 - Sensore della temperatura

L’utente, che regola la temperatura interna dell’area, influisce sulle rilevazioni del parametro, ma anche l’apertura o la chiusura delle porte dell’area possono influenzarne il valore.

In particolare, nel caso in cui le porte dell’area fossero aperte, la temperatura verrà completamente influenzata da quella esterna e il valore rilevato dal sensore si avvicinerà periodicamente a quello ambientale. Quando, invece, le porte dell’area sono chiuse, la temperatura verrà completamente influenzata da quella interna regolata dall’utente e le rilevazioni effettuate dal sensore si avvicineranno periodicamente a questo valore, fino a quando non lo avranno raggiunto.

Per poter calcolare le rilevazioni del sensore della temperatura, è stato realizzato l’oggetto FactoryFunctionsTemperature, il quale rappresenta una factory di funzioni che possono essere applicate per poter calcolare il nuovo valore della temperatura.

Più precisamente, il sensore della temperatura effettua un aggiornamento del valore rilevato:

4.8.3 Sensore per l’umidità dell’aria

Il sensore della l’umidità dell’aria è un sensore in grado di aggiornare periodicamente il valore rilevato.

Come rappresentato nella figura Fig. 4.8.3.1, il sensore implementa l’interfaccia SensorWithTimer, estendendo la classe astratta AbstractSensorWithTimer.

Fig. 4.8.3.1 - Sensore per l'umidità dell'aria

Se le porte dell’area sono aperte, i valori rilevati dal sensore si avvicineranno periodicamente a quello ambientale; se, invece, le porte dell’area sono chiuse, il valore dell’umidità sarà inferiore a quello ambientale.

Le azioni che può intraprendere l’utente per regolare l’umidità sono:

Anche qui, è stato realizzato un oggetto factory, chiamato FactoryFunctionsAirHumidity, per selezionare la funzione da applicare al valore in base allo stato dell’area.

Il sensore procederà al calcolo del nuovo valore ogni qual volta:

4.8.4 Sensore per l’umidità del suolo

Il sensore dell’umidità del suolo estende da AbstractSensorWithTimer, che detiene già al suo interno i metodi necessari all’aggiornamento periodico dei valori (vedi Fig. 4.8.4.1).

Fig. 4.8.4.1 - Sensore per l'umidità del suolo

Se nella località di ubicazione della serra sta piovendo e se le porte dell’area sono aperte, il valore del sensore sarà influenzato dalla quantità di precipitazioni; se, invece, non sta piovendo oppure se le porte dell’area sono chiuse, il valore diminuirà mano a mano che passa il tempo in quanto l’acqua tende ad evaporare.

L’utente, per regolare l’umidità del suolo, potrà:

Queste azioni sono istantanee, ovvero il valore dell’umidità viene aggiornato immediatamente a seguito dell’azione intrapresa.

Per quanto riguarda le funzioni da applicare al calcolo del nuovo valore, è stato utilizzato l’oggetto factory FactoryFunctionsSoilHumidity.

4.9 Fine simulazione

Nel caso in cui l’utente decida di fermare la simulazione in anticipo o nel caso in cui il tempo virtuale sia interamente trascorso, egli verrà reindirizzato alla schermata di fine simulazione in cui gli verrà data la possibilità di iniziarne una nuova.

Gli elementi grafici della schermata di fine simulazione sono contenuti all’interno del rispettivo file FXML e FinishSimulationView ne rappresenta il Controller.

Fig. 4.9.1 - View fine simulazione

Come si può vedere dalla figura Fig. 4.9.1, per poter realizzare la View di fine simulazione è stata definita l’interfaccia FinishSimulationView, la quale estende l’interfaccia ViewComponent, dichiarando che il pannello principale, contenente tutti i diversi elementi di questa scena, è un BorderPane.

La scena di fine simulazione, quindi, verrà mostrata all’interno della scena madre e, grazie alle relazione che vi sono fra i diversi elementi dell’architettura, FinishSimulationView è in grado di accedere alle proprietà di SimulationView per specificare quale dovrà essere l’azione che dove essere compiuta nel caso in cui l’utente clicchi sul pulsante “Start a new simulation”. In tal caso, verrà istanziato di nuovo l’elemento SelectCityMVC e l’applicazione riprenderà dalla schermata di selezione della città.

4.10 Pattern utilizzati

Per la realizzazione di questo progetto sono stati adoperati i pattern creazionali e comportamentali descritti nelle seguenti sottosezioni.

Factory

Il pattern Factory è un pattern creazionale che ci dà la possibilità di creare degli oggetti senza dover specificare la loro classe e ci consente di cambiare in modo abbastanza agevole l’oggetto creato.

Le factories, come dice il nome, rappresentano delle vere e proprie “fabbriche di oggetti” che possiamo utilizzare per istanziare gli oggetti di cui abbiamo bisogno e con determinate caratteristiche. In generale questo pattern è stato utilizzato tramite i companion object associati alle classi, i quali danno la possibilità di istanziare la classe corrispondente mantenendo privata la sua implementazione.

All’interno del progetto si è fatto utilizzo, in particolare, del pattern StaticFactory, per produrre le funzioni necessarie a calcolare l’aggiornamento dei parametri rilevati dai sensori.

Nella programmazione funzionale, infatti, è possibile specificare dei metodi che abbiano come tipo di ritorno delle funzioni. Si è sfruttata, quindi, questa possibilità per poter realizzare delle factories che restituissero la funzione da applicare per determinare l’aggiornamento del valore rilevato.

Singleton

Per lo sviluppo del progetto si è fatto largo uso di questo pattern creazionale, il quale garantisce che di una determinata classe venga creata una sola istanza, fornendo un punto di accesso globale ad essa.

In Scala è particolarmente semplice implementare tale pattern in quanto gli object sono classi con esattamente una istanza che viene inizializzata in modo lazy (su richiesta) quando ci riferiamo ad essa: prima di ciò, nessuna istanza dell’object sarà presente nello heap.

Template method

Il pattern Template Method è un pattern comportamentale basato sulle classi. Permette di catturare il template dell’algoritmo attraverso dei metodi astratti che verranno poi implementati nelle sottoclassi. Grazie a questo pattern, è possibile fattorizzare in una classe la parte invariante di un algoritmo e lasciare alle sottoclassi il compito di implementare il comportamento che può variare, favorendo un maggiore riuso del codice.

Questo pattern è stato utilizzato all’interno del progetto per definire le seguenti classi astratte:

Strategy

All’interno del progetto è stato ampiamente utilizzato il pattern Strategy, ossia l’incapsulamento di un algoritmo all’interno di una classe, permettendo di mantenere un’interfaccia generica.

Questo pattern è direttamente supportato nel linguaggio mediante il passaggio di funzioni higher-order, facendo sì che le classi che lo utilizzano rendano dinamico il proprio comportamento e utilizzino in modo intercambiabile le diverse implementazioni degli algoritmi definiti nell’interfaccia generica.

4.11 Organizzazione del codice

Il sistema è stato organizzato in 5 package principali:

Fig. 4.11.1 - Organizzazione dei package del progetto