PPS-22-smartgh

5. Implementazione

Il seguente capitolo motiva e dettaglia aspetti implementativi ritenuti rilevanti per una corretta comprensione del progetto.

Va inoltre sottolineato che il codice realizzato è stato opportunamente documentato mediante la Scaladoc, la quale può essere utilizzata come ulteriore riferimento per meglio comprendere l’implementazione del programma e il suo comportamento.

5.1 Utilizzo del paradigma funzionale

Durante lo sviluppo del progetto, si è cercato di utilizzare il più possibile il paradigma funzionale e di raffinare sempre di più la soluzione adottata, per poter sfruttare al meglio i vantaggi che questo paradigma offre.

Se, durante la realizzazione di una determinata funzionalità, ci si rendeva conto di aver utilizzato un approccio più legato all’object-oriented che a al paradigma funzionale, dopo aver valutato i diversi aspetti di una possibile soluzione più funzionale, si procedeva con il refactoring del codice.

Nelle seguenti sezioni verranno descritti con maggiore dettaglio alcuni elementi della programmazione funzionale che sono stati utilizzati all’interno del progetto, riportando alcuni esempi del loro utilizzo.

5.1.1 Higher-order functions

Un meccanismo efficace, spesso utilizzato nella programmazione funzionale, è quello delle funzioni higher order. Queste sono delle funzioni che accettano altre funzioni come parametri e/o restituiscono una funzione come risultato.

L’utilizzo di queste funzioni ha permesso di rendere il codice riusabile e di adoperare facilmente il pattern Strategy, in quanto consente di passare alle funzioni delle strategie esterne.

Esse sono state utilizzate in molte parti del progetto: un esempio del loro utilizzo si può trovare negli oggetti Factory delle funzioni per il calcolo dei nuovi valori dei sensori (vedi codice riportato di seguito).

/** Updates the current soil moisture value according to the precipitation value when gates are open. */
val updateGatesOpenValue: (Double, Double) => Double = _ - _ * RainFactor

In particolare, si tratta di una funzione utilizzata per calcolare il valore dell’umidità del suolo nel caso in cui la porta dell’area sia aperta.

La funzione prende in ingresso due valori: il valore corrente dell’umidità e il valore delle precipitazioni. Nell’esempio l’implementazione della funzione è stata specificata attraverso l’utilizzo delle funzioni literal (funzioni lambda in Java) e, grazie alla sintassi di Scala e all’inferenza dei tipi, è possibile semplificare la funzione utilizzando il placeholder underscore_, rendendo il codice il più idiomatico possibile.

5.1.2 Currying

In Scala è possibile definire funzioni curried, per cui la valutazione di una funzione che assume parametri multipli può essere tradotta nella valutazione di una sequenza di funzioni.

È un meccanismo che consente l’applicazione del principio DRY (Don’t repeat yourself), favorendo quindi il riuso di codice. Infatti, quando una funzione è curried, è possibile applicare parzialmente la funzione per poterla utilizzare in più punti del codice.

Nel seguente estratto di codice è possibile vedere un esempio dell’utilizzo di questo meccanismo.

private def extractTerm(solveInfo: SolveInfo)(term: String) =
  extractTermToString(solveInfo, term).replace("'", "")

La funzione extractTerm si occupa di estrarre il termine dalla soluzione ottenuta da Prolog, rimuovendo gli apici.

override def getCityInfo(city: String): Option[(String, String, String)] =
  searchCity(city).headOption.fold(None) { s =>
    val e = extractTerm(s)
    Some(e("X"), e("Y"), e("Z"))
  }

La funzione viene utilizzata all’interno del Model di select city per estrarre le informazioni associate alla città selezionata. Per chiamare la funzione si deve mantenere la stessa notazione currying. Nell’esempio la funzione viene applicata parzialmente passando un solo argomento e, in questo modo, ritorna una nuova funzione che può essere consumata in seguito specificando il secondo argomento.

5.1.3 Type members

La keyword type in Scala introduce il concetto di type members all’interno di una classe, oltre ai field e method members che, solitamente, già troviamo. Viene impiegata principalmente per creare l’alias di un tipo più complicato: il type system sostituirà l’alias con l’actual type quando effettuerà il type checking.

I type members, analogamente agli altri membri delle classi, possono essere abstract ed è, dunque, possibile specificare il tipo concreto nell’implementazione.

In merito al progetto, i type members sono stati utilizzati per:

/** Data structure that will contains the city's environment values. */
type EnvironmentValues = Map[String, Any]

/** Data structure that will contains plant's optimal values. */
type OptimalValues = Map[String, Any]
/** The controller requirements. */
  type Requirements = EnvironmentViewModule.Provider with EnvironmentModelModule.Provider with SimulationMVC.Provider

5.1.4 For-comprehension

Al fine di rendere il codice meno imperativo, si è fatto uso della for-comprehension: un costrutto funzionale per operare sulle collezioni e basato sulle monadi.

Oltre a rendere il codice più funzionale, la scelta dell’utilizzo della for-comprehension è supportata dall’incremento della leggibilità del codice, come si può vedere nel seguente estratto di programma. Il costrutto viene ad esempio utilizzato per la creazione degli oggetti ManageSensor, il cui compito è racchiudere tutte le informazioni utili riguardati un sensore.

for
    (key, m) <- mapSensorNamesAndMessages.toList
    optK = m.getOrElse("name", "")
    um = m.getOrElse("um", "")
    msg = m.getOrElse("message", "")
yield ManageSensorImpl(
    key,
    optimalValueToDouble.getOrElse("min_" + optK, 0.0),
    optimalValueToDouble.getOrElse("max_" + optK, 0.0),
    um,
    sensorsMap(key),
    BigDecimal(sensorsMap(key).getCurrentValue).setScale(2, BigDecimal.RoundingMode.HALF_UP).toDouble,
    msg,
    firstSensorStatus(
        BigDecimal(sensorsMap(key).getCurrentValue).setScale(2, BigDecimal.RoundingMode.HALF_UP).toDouble,
        optimalValueToDouble.getOrElse("min_" + optK, 0.0),
        optimalValueToDouble.getOrElse("max_" + optK, 0.0)
    )
)

Nell’esempio si itera sulla mappa contenete le costanti come il nome del sensore, l’unità di misura e il messaggio di errore associato ad esso. Questi valori vengono poi impiegati nella costruzione dell’oggetto ManageSensor, in modo da reperire le informazioni rispetto al sensore per uno specifico parametro, crearne e memorizzarne l’istanza, inizializzare il suo stato e tenere traccia del valore corrente rilevato da esso.

5.1.5 Trait mixins

In Scala le classi possono avere un’unica superclasse ma molti mixins, attraverso l’utilizzo delle keywords extends e with.

Un mixin è una classe o un interfaccia in cui alcuni o tutti i suoi metodi e/o proprietà non sono implementati, richiedendo che un’altra classe o interfaccia fornisca le implementazioni mancanti. Gli elementi mixins sono spesso descritti come “inclusi” o “impilati in”, piuttosto che “ereditati”.

Il mixins, utilizzato con le interfacce, consente ai traits di poter essere concatenati utilizzando la composizione piuttosto che l’ereditarietà.

Per il progetto, in particolare nella realizzazione dei diversi Cake pattern, si è fatto utilizzo di questo meccanismo. Ad esempio, al termine di ogni modulo Model, View o Controller troviamo il trait Interface dichiarato nel seguente modo:

trait Interface extends Provider with Component

Tale dichiarazione indica che l’elemento Interface ha come supertipo Provider e un mixin con Component, che gli consente di utilizzare le relative proprietà.

5.2 Utilizzo del paradigma logico

Il team di sviluppo si è posto, come obiettivo per la realizzazione del progetto, l’utilizzo del paradigma logico. In fase di progettazione ci si è interrogati su come poter sfruttare la programmazione logica all’interno del progetto, giungendo alla conclusione di utilizzare Prolog come database sul quale effettuare delle interrogazioni, per poter ottenere informazioni relative alle piante e alle città.

Nello specifico, sono stati realizzati due file .txt: uno contenente l’elenco delle città in cui può essere ubicata la serra e l’altro contenente l’elenco dei nomi delle piante assieme ai loro identificativi.

5.2.1 Utilizzo di Prolog per la selezione della città

All’inizio l’utente, quando si trova nella schermata iniziale dell’applicazione, deve effettuare l’inserimento del nome della città nella quale verrà ubicata la serra.

Per consentire quest’operazione, la classe UploadCities si occupa di convertire il file cities.txt in un file Prolog cities.pl, che verrà inserito all’interno della home directory dell’utente, all’interno della cartella pps.

Il file cities.pl contiene le regole sulle città, scritte in questo modo:

city('Bologna', '44.4939', '11.3428').
city('Cesena', '44.1333', '12.2333').

Tale file contiene, inoltre, una regola search_city che consente di convertire i nomi delle città in array di caratteri, in modo da facilitare il meccanismo di ricerca.

search_city([H|T], X, Y, Z) :- city(X, Y, Z), atom_chars(X, [H|T]).

Infine, SelectCityModelModule utilizza questo file e la libreria TuProlog per poter visualizzare i diversi nomi delle città e implementare il live search. Infatti, ogni qual volta l’utente inserisce dei caratteri nel TextField della schermata, questi vengono utilizzati per definire il goal che si intende risolvere, al fine di determinare la città che l’utente intende selezionare.

private def searchCity(city: String, start: String = "['", sep: String = "','", end: String = "']"): Iterable[SolveInfo] =
  engine("search_city(" + city.mkString(start, sep, end) + ", X, Y, Z)")

5.2.2 Utilizzo di Prolog per la selezione delle piante

Per consentire all’utente la selezione delle piante che si intende coltivare all’interno della serra, la classe UploadPlants, prima che venga mostrata l’apposita schermata, si occupa di convertire il file plants.txt in un file Prolog plants.pl, contenente i records che detengono le informazioni sulle piante. Quest’ultimi risultano essere scritti nel seguente modo:

plant('Alcea rosea', 'alcea rosea').
plant('Basil', 'ocimum basilicum').

Una volta che il file plants.pl è stato scritto e inserito all’interno della home directory dell’utente, PlantSelectorModelModule utilizza questo file, tramite la libreria TuProlog, per poter mostrare le piante selezionabili dall’utente e in seguito per poter prendere il loro identificativo e istanziare gli oggetti Plant.

override def getAllAvailablePlants: List[String] =
  engine("plant(X, Y).").map(extractTermToString(_, "X").replace("'", "")).toList

override def getPlantsSelectedIdentifier: List[String] =
  selectedPlants
    .flatMap(s => engine("plant(\'" + s + "\', Y).")
    .map(extractTermToString(_, "Y")))
    .toList

5.3 Programmazione reattiva e asincrona

Per lo sviluppo del progetto si è fatto uso sia della programmazione reattiva (di tipo event-based) che di quella asincrona, scegliendo di sfruttare i metodi forniti dalla libreria monix.io.

I meccanismi di programmazione asincrona, come Task, sono stati utilizzati per effettuare operazioni che possono richiedere un periodo di tempo considerevole per poter essere completate e, pertanto, possono risultare bloccanti per il flusso di controllo dell’applicazione. Ad esempio, l’impostazione della velocità del tempo virtuale della simulazione:

override def setSpeed(speed: Double): Unit =
  Task {
    timer.changeTickPeriod(timeSpeed(speed))
  }.executeAsync.runToFuture

Per quanto riguarda la programmazione reattiva, sono stati sfruttati meccanismi come il data type Observable e la classe astratta ConcurrentSubject che sono stati impiegati, ad esempio, per:

private def timer(from: FiniteDuration, period: FiniteDuration): Unit =
  cancelable = Observable
    .fromIterable(from.toSeconds to duration.toSeconds)
    .throttle(period, 1)
    .map(Duration(_, TimeUnit.SECONDS))
    .foreachL(consumer)
    .doOnFinish(onFinishTask)
    .runToFuture
private val subjectTimerValue = ConcurrentSubject[String](MulticastStrategy.publish)
override def notifyTimeValueChange(timeValue: String): Unit =
  Task {
    subjectTimerValue.onNext(timeValue)
  }.executeAsync.runToFuture
override def computeNextSensorValue(): Unit =
  Task {
    currentValue = checkInRange(areaComponentsState.humidityActions match
      case Watering => updateWateringValue(currentValue)
      case MovingSoil => updateMovingSoilValue(currentValue)
      case _ =>
        areaComponentsState.gatesState match
          case Open if currentEnvironmentValue > 0.0 => updateGatesOpenValue(currentValue, currentEnvironmentValue)
          case _ => updateEvaporationValue(currentValue)
    )
    areaComponentsState.humidityActions = None
    subject.onNext(currentValue)
  }.executeAsync.runToFuture
override protected def registerTimerCallback(verifyTimePass: String => Boolean): Unit =
  addTimerCallback(s => if verifyTimePass(s) then computeNextSensorValue())
override def updateView(): Unit =
  ghDivisionModel.areas.foreach(
    _.areaModel
      .changeStatusObservable()
      .subscribe(
        s => {
          s match
            case AreaStatus.ALARM => drawView()
            case _ =>
          Continue
        },
        _.printStackTrace(),
        () => {}
      ) :: subscriptionAlarm
  )
  subscriptionTimeout = timeoutUpd.subscribe()
override def startEmittingPlantsSelected(): Unit =
  Task {
    selectedPlants
      .zip(getPlantsSelectedIdentifier)
      .foreach((name, identifier) => subjectPlantInfo.onNext(Plant(name, identifier)))
    subjectPlantInfo.onComplete()
  }.executeAsync.runToFuture

Per quanto riguarda gli aggiornamenti degli elementi della GUI, che sono stati implementati mediante l’utilizzo della libreria JavaFX, si è disposto del metodo runLater della classe Platform presente all’interno della stessa libreria. Tale metodo prende in input un oggetto di tipo Runnable che verrà eseguito dal JavaFX Application Thread, quando questo non sarà impegnato nell’esecuzione di altri lavori. Ciò permette, quindi, di utilizzare un meccanismo asincrono diverso dal Task: quest’ultimo, infatti, non avrebbe funzionato in quanto non può modificare lui stesso il JavaFX scene graph.

override def updateState(state: String): Unit =
  Platform.runLater { () =>
    statusLabel.setText(state)
    statusLabel.getStyleClass.setAll(state)
  }

5.4 Richieste dei dati

Per reperire i dati relativi alle previsioni metereologiche della città in cui è ubicata la serra e quelli relativi alle piante, si è fatto uso di richieste HTTP. A tal fine, si è deciso di utilizzare la libreria requests per permettere di effettuare la richiesta ai rispettivi url: weatherapi, per le previsioni meteorologiche e open.plantbook, per le piante.

val query =
          "http://api.weatherapi.com/v1/forecast.json?key=" + apiKey + "&q=" + nameCity.replace(
            " ",
            "%20"
          ) + "&days=1&aqi=no&alerts=no"
val r: Response = requests.get(query)

Ottenuta la risposta dal web Server, si procede nei seguenti due modi:

  1. con esito positivo si effettua il parsing per ottenere il JSON, mediante la libreria json4s;
  2. con esito negativo si imposta un valore di default.

Al fine di valutare l’esito della risposta, si è fatto uso del Try match per identificare se la richiesta è andata a buon fine, caso di Success, o meno, caso di Failure.

Try(requests.post(url = url, data = data)) match {
  case Success(r: Response) =>
    implicit val formats: DefaultFormats.type = org.json4s.DefaultFormats
    parse(r.text()).extract[RequestResult].get("access_token").fold[String]("")(_.toString)
  case Failure(_) => ""
}

Il JSON ottenuto in caso di successo è stato poi utilizzato per l’implementazione del type definito nell’interfaccia della classe Environment, nel caso della città, e Plant, nel caso delle informazioni relative alla pianta.

Per entrambe le implementazioni, si è deciso di assegnare a type il tipo concreto Map[String, Any] in quanto il JSON ottenuto come risposta, contiene valori anche complessi (ad esempio, sub-json).

A partire dall’oggetto contenente l’implementazione del type, sono state estrapolate le informazioni utili alla simulazione.

Nel caso delle città, questo è rappresentato dalle previsioni meteorologiche orarie, che vengono successivamente filtrate in base all’orario richiesto,. Nel caso delle piante, è rappresentato da: nome della pianta, valori ottimali dei sensori (temperatura, luminosità ed umidità del suolo e dell’aria) e l’url per reperire l’immagine. Inoltre, a partire dal nome, è stata effettuata la richiesta al sito Wikipedia per ottenere la descrizione della pianta.

5.5 Utilizzo di ScalaFX e JavaFX

Per l’implementazione dell’interfaccia grafica sono state utilizzate le librerie: ScalaFX, un DSL scritto in Scala che fa da wrapper agli elementi di JavaFx, e JavaFX.

Nello specifico, si è deciso di gestire le parti statiche dell’applicazione attraverso la creazione di layout in formato FXML. Siccome nella libreria ScalaFX non è prevista la gestione di questa funzionalità, si è deciso di integrare quest’ultima con la libreria JavaFX.

FXML è un formato XML che permette di comporre applicazioni JavaFX, separando il codice per la gestione degli elementi dalla parte di visualizzazione dei layout. Inoltre, l’utilizzo di SceneBuilder ha facilitato la creazione delle pagine attraverso il suo ambiente grafico, fornendo una renderizzazione visiva e intuitiva.

La logica di caricamento del file FXML viene racchiusa nella classe astratta AbstractViewComponent: tutti i componenti della View estendono da tale classe, specificando il file FXML associato, e possono ottenere in automatico il layout caricato.

Di fatto, il componente View rappresenta il Controller associato al layout. Il Controller può ottenere il riferimento agli elementi dell’interfaccia attraverso gli id specificati nell’FXML e mediante il caricatore, ossia FXMLLoader che cercherà di istanziarli e di renderli accessibili. Tale Controller ha il compito di inizializzare gli elementi dell’interfaccia utente e di gestirne il loro comportamento dinamico.

5.6 Testing

Per testare le funzionalità principali del programma, si è deciso di utilizzare la modalità Test Driven Development (TDD). Questa strategia prevede di scrivere per prima cosa il codice di testing, indicando il comportamento corretto della funzionalità che si vuole testare, e successivamente di scrivere il codice di produzione, affinché i test individuati passino correttamente. Una volta scritto il codice di produzione e passato i test, si può procedere al refactoring e al miglioramento della soluzione ottenuta.

Il TDD, quindi, si compone di tre diverse fasi che si susseguono: red, green e refactor. Nella fase red si ha solo il codice di testing e, di conseguenza, i test che sono stati scritti non passeranno in quanto il codice di produzione risulta essere mancante. Nella fase green, invece, si procede alla scrittura del codice di produzione, in modo da poter superare i test precedentemente definiti. Infine, nella fase di refactor, il codice di produzione scritto viene migliorato.

Il team di lavoro, per lo sviluppo dell’applicazione, ha inoltre deciso di adottare la pratica di Continuous Integration, decidendo di realizzare due flussi di lavoro sul relativo repository: il primo dedicato all’esecuzione dei test sui diversi sistemi operativi (Windows, Linux e MacOS); il secondo diretto a determinare la coverage ottenuta mediante i test effettuati.

Per questo progetto, le funzionalità del modello che racchiudono la logica di business sono state testate mediante l’utilizzo di ScalaTest mentre, per testare gli elementi della View e siccome è stata utilizzata la libreria ScalaFX, si è deciso di utilizzare la libreria di testing TestFx.

Nelle seguenti sezioni, è possibile trovare una descrizione maggiormente dettagliata relativa ai test effettuati, le modalità utilizzate e la coverage ottenuta.

5.6.1 Utilizzo di ScalaTest

Per testare le funzionalità legate alla logica di business dell’applicazione, sono state realizzate diverse suits mediante la libreria ScalaTest.

Tutte le diverse classi realizzate estendono AnyFunSuite e i test sono stati scritti seguendo questo stile:

test("At the beginning the temperature should be initialized with the default value") {
    val defaultTemperatureValue = 27
    areaComponentsState.temperature shouldEqual defaultTemperatureValue
}

Per verificare determinate condizioni come, ad esempio, di uguaglianza, minoranza o maggioranza, si è fatto utilizzo dei matchers di ScalaTest. Nello specifico, se la classe di testing estende il trait Matchers, ha la possibilità di utilizzare all’interno dei test delle keywords come should be, equal, shouldEqual, ecc… che consentono di verificare le condizioni espresse.

Infine, per testare il verificarsi di determinati risultati o condizioni che, però, possono impiegare un certo tempo per avvenire da quando è stato generato l’evento che ne è la causa, si è fatto uso di eventually. In particolare, se la classe di test estende il trait Eventually, ha la possibilità di definire dei test che presentano una condizione che prima o poi si deve verificare entro un lasso di tempo predefinito.

test("The air humidity value should decrease because the ventilation and the humidity are inactive") {
  setupTimer(500 microseconds)
  initialValueTest()

  eventually(timeout(Span(1000, Milliseconds))) {
      humiditySensor.getCurrentValue should be < initialHumidity
  }
}

5.6.2 Utilizzo di Unit test e TestFx

Per poter testare gli aspetti relativi alla visualizzazione dei dati e all’interfaccia utente, si è deciso di utilizzare le librerie TestFx e JUnit.

TestFx richiede che, per poter scrivere dei test che vadano a verificare degli elementi di JavaFX, la classe di testing estenda la classe ApplicationExtension. Dopodiché, è necessario definire un metodo contrassegnato dalla notazione @Start per impostare la schermata che si vuole testare: una volta fatto questo, si ha la possibilità di definire i test per la GUI.

Nello specifico, i diversi Unit tests che si vogliono realizzare devono prendere come argomento FxRobot, il quale rappresenta un oggetto che può essere utilizzato per poter simulare i comportamenti dell’utente sull’interfaccia grafica, come mostrato nel seguente esempio.

@Test def testAfterPlantSelection(robot: FxRobot): Unit =
  //...
  val checkBox = robot.lookup(selectablePlantsBoxId)
                      .queryAs(classOf[VBox])
                      .getChildren
                      .get(plantIndex)
  //when:
  robot.clickOn(checkBox)

  //then:
  assertEquals(robot.lookup(selectedPlantBoxId)
              .queryAs(classOf[VBox])
              .getChildren.size, selectedPlantNumber)
  verifyThat(numberPlantsSelectedId, hasText(selectedPlantNumber.toString))

Come si può vedere sempre dall’esempio, per verificare le proprietà degli elementi dell’interfaccia, è stata utilizzata la classe FxAssert e il metodo verifyThat, il quale consente, una volta passato l’id del componente FXML, di verificare una determinata proprietà su di esso. Le proprietà possono essere definite tramite i matchers di TestFX.

In questo modo è stato possibile effettuare dei test automatici sull’interfaccia grafica che si intende mostrare all’utente.

Va comunque sottolineato che, per testare gli aspetti di View, sono stati svolti anche numerosi test manuali. Molto spesso risultava essere complicato, tramite i soli test automatici, verificare determinate condizioni perciò questo tipo di test, di fatto, non può essere considerato completamente esaustivo nella verifica degli aspetti di interazione con l’utente.

5.6.3 Coverage

Come detto in precedenza, il team di sviluppo ha realizzato anche un flusso di lavoro dedicato alla coverage, in modo tale da poter analizzare la copertura ottenuta ogni qual volta vengono inseriti dei nuovi tests o modificati quelli precedenti.

La code coverage fa riferimento, sostanzialmente, alla quantità di istruzioni di codice che vengono eseguite durante l’esecuzione dei tests. Tuttavia, ottenere una coverage del 100% non significa che il testing effettuato riesca a ricoprire tutti gli scenari: infatti, l’obiettivo che ci si è dati non è stato quello di raggiungere il 100% della copertura ma di testare funzioni mirate.

In particolare, per poter ottenere i risultati relativi alla coverage, si è fatto utilizzo del tool JaCoCo.

Fig. 5.6.3.1 - Coverage finale ottenuta

Come si può vedere dalla Fig. 5.6.3.1, la coverage finale ottenuta è del 80% su un totale di 126 test effettuati.

Gli elementi per cui si ha una coverage più elevata sono quelli che fanno riferimento al Model dell’applicazione, mentre quelli per cui si ha una coverage più bassa fanno riferimento agli elementi della View che, come spiegato nella precedente sezione Sec. 5.6.2, sono stati testati sia tramite test automatici che tramite test manuali.

5.7 Suddivisione del lavoro

Durante lo sprint preview, una volta determinati i diversi prodotti che si vogliono realizzare e i diversi tasks necessari per il loro completamento, questi vengono poi assegnati a uno o più membri del gruppo, incaricati della loro esecuzione.

Il completamento di uno o più tasks, individuati nel product backlog, può dare luogo alla stesura di diversi elementi del codice di produzione.

Nelle seguenti sezioni, ogni membro del gruppo si è impegnato nel descrivere le parti di codice da lui stesso implementate o effettuate in collaborazione con altri membri del gruppo.

5.7.1 Folin Veronika

Inizialmente mi sono occupata della selezione delle piante da coltivare all’interno della serra e, in particolare, ho sviluppato:

All’interno del modulo Environment, ho gestito tramite reactive programming (sfruttando la libreria monix.io) la richiesta per reperire le previsioni metereologiche in quanto questa operazione può richiedere diverso tempo e può influire sulla reattività dell’applicazione.

Dopodiché mi sono occupata della realizzazione dei componenti per la visualizzazione dello stato aggiornato della simulazione, ovvero dei dati relativi all’ambiente in cui è immersa la serra e dello scorrere del tempo. Nello specifico mi sono occupata dell’implementazione:

L’introduzione di EnvironmentMVC ha richiesto la collaborazione degli altri membri del gruppo per collegare l’elemento ai seguenti componenti:

Successivamente, insieme al resto del gruppo, mi sono dedicata allo sviluppo del componente MVC principale dell’applicazione. In particolare, ho realizzato:

Nel terzo sprint mi sono occupata di raccogliere gli elementi comuni relativi alle diverse View dell’applicazione, realizzando l’interfaccia ContiguousSceneView.

Come gli altri membri del gruppo, nell’ultimo sprint ho compiuto operazioni di refactoring e di ottimizzazione del codice per poter migliorare ulteriormente la soluzione proposta.

Infine, ho creato e gestito il componente HelpView che si occupa di visualizzare la guida utente all’interno dell’applicazione.

Per quanto riguarda la parte di testing, ho realizzato le seguenti classi di test:

5.7.2 Mengozzi Maria

Nello sviluppo del progetto mi sono occupata insieme ad Elena della selezione della città in cui è ubicata la serra. Specificatamente ho realizzato:

Sempre insieme ad Elena mi sono occupata della realizzazione dei controller (AreaDetailsController e AreaXXXController) per la schermata del dettaglio di un area e lateralmente alla gestione del suo MVC. Nel caso del controller ho gestito i metodi relativi all’interfacciamento con il Model, da me realizzato.

Inoltre, ho collaborato con Anna per quanto riguarda il collegamento tra i sensori e le aree e i parametri ambientali, e insieme al resto del gruppo per la definizione della struttura del progetto e il collegamento tra le varie parti realizzate.

Per quanto riguarda l’identificazione di una sezione di progetto pienamente riconducibile alla sottoscritta, si possono indicare le aree. Nello specifico, le parti da me singolarmente implementate comprendono:

Per la parte di testing mi sono occupata della realizzazione delle seguenti classi di test:

Inoltre, alcune delle funzionalità da me implementate sono presenti anche nelle altre classi di test.

5.7.3 Vitali Anna

Nelle fasi iniziali di implementazione del progetto, mi sono impegnata nella realizzazione del meccanismo di selezione delle piante, realizzando gli elementi del pattern MVC che si occupano di implementare questa funzionalità, che sono:

Dopodiché, mi sono dedicata alla realizzazione della View di fine simulazione, realizzando la classe FinishSimulationView.

Nel secondo sprint, invece, mi sono concentrata maggiormente sulla realizzazione dei sensori, dei loro meccanismi di aggiornamento e notifica, definendo inizialmente assieme ad Elena, l’architettura generale dei sensori, in particolare abbiamo realizzato: le’interfacce Sensor e SensorWithTimer, le classi astratte AbstractSensor e AbstractSensorWithTimer e la classe AreaComponentsState.

Successivamente, mi sono dedicata all’implementazione dei sensori relativi alla luminosità e alla temperatura, realizzando le classi:

Quando tutti i sensori sono stati completati, assieme alle colleghe Veronika ed Elena, abbiamo provveduto a collegare questi ultimi all’EnvironmentModelModule e successivamente assieme a Maria, all’AreaModelModule.

Infine, siccome durante l’utilizzo dell’applicazione, si è visto che i dati delle piante impiegavano un discreto tempo per venire caricati completamente, per mantenere l’applicazione reattiva e fornire informazioni all’utente, ho provveduto a realizzare il componente LoadingPlantMVC e i suoi elementi:

Una volta che tutti gli MVC sono stati implementati, assieme alle altre compagne di progetto, abbiamo provveduto alla rifattorizzazione di SimulatioinView e alla definizione di SimulationController, nonché alla realizzazione di SimulationMVC. Infine, mi sono occupata di raccogliere gli elementi comuni relativi ai diversi Controller dell’applicazione, realizzando l’interfaccia SceneController.

Nell’ultimo sprint, mi sono dedicata assieme agli altri membri del gruppo, ad operazioni di refactoring e di ottimizzazione del codice, per poter raffinare e migliorare ulteriormente la soluzione proposta.

Per quanto concerne l’attività di testing, invece, ho effettuato la realizzazione dei seguenti tests:

5.7.4 Yan Elena

Durante il primo sprint mi sono occupata soprattutto delle classi di view per fornire un’applicazione concreta con cui l’utente potesse interagire.

In particolare, usando le librerie ScalaFX e JavaFX, ho realizzato le classi:

Successivamente ho lavorato sul componente di selezione della città, in particolare, le parti realizzate riguardano il modulo SelectCityMVC con i rispettivi sottomoduli:

Di seguito ho sviluppato il componente Timer utilizzando la programmazione asincrona con gli Observer forniti dalla libreria monix.io. Dopodiché insieme a Veronika abbiamo collegato il Timer con il TimerModel.

Nel secondo sprint abbiamo poi raffinato la struttura del progetto ed effettuato il collegamento tra le varie parti realizzate, coinvolgendo tutti i membri del gruppo.

Assieme ad Anna, abbiamo progettato l’interfaccia Sensor e SensorWithTimer con le rispettive classi astratte. In seguito ho realizzato l’implementazione dei sensori AirHumiditySensor e SoilHumiditySensor e i relativi oggetti factory (FactoryFunctionsAirHumidity e FactoryFunctionsSoilHumidity).

Durante il terzo sprint, mi sono dedicata allo sviluppo dei componenti del dettaglio area e ai suoi quattro parametri. Nello specifico ho realizzato:

Mentre i moduli del controller sono stati realizzati collaborando con Maria.

L’ultimo sprint è stato lasciato per raffinare lo stile grafico dell’applicazione e alle operazioni di refactoring e di ottimizzazione del codice.

Per quanto riguarda la parte di testing, ho realizzato i seguenti test: