Implementazione - Morelli Marco
Il codice prodotto durante lo svolgimento del progetto riguarda principalmente le seguenti parti:
- Grid: contiene l'astrazione di una griglia 2D immutabile e tipizzata
Grid[T]e la sua estensioneDistanceGrid[T]con supporto al calcolo delle distanze e alla ricerca dei vicini. - DistanceGrid: contiene l'implementazione concreta
MapBasedDistanceGrid[T]che decoraMapBasedGrid[T]con funzionalità aggiuntive. - WeatherService: contiene la logica che regola l'interazione tra la griglia delle celle
DistanceGrid[Cell]e quella delle condizioni meteorologicheGrid[Weather]. - Simulation Controller: è il controllore principale della simulazione. Gestisce l'avvio, la pausa, lo stop, la velocità, l'aggiornamento delle celle e la generazione di report o mappe casuali.
- Simulation Listener: è un'interfaccia che rappresenta chi “ascolta” gli eventi della simulazione come l'interfaccia grafica. Riceve notifiche quando cambia lo stato, il timer, una cella o quando la simulazione termina.
- Controller Factory: crea un nuovo SimulationController configurato con tutti i servizi e componenti necessari (engine, store, scheduler)
- View: implementazione del package
util.resource,navigationControllere parte dei componenti del packagecomponent.
Model
Gli elementi appartenenti al model sono stati sviluppati in modo da essere immutabili, seguendo i principi della programmazione funzionale.
Grid
GridElement funge da marker trait, tipo base astratto senza metodi, per tutti gli elementi che possono occupare la griglia. Aderisce ai principi SOLID: Open/Closed Principle e Single Responsibility Principle.
trait GridElementL'intero sistema delle griglie è basato su un parametro di tipo che definisce la natura degli elementi contenuti. T rappresenta un parametro di tipo generico, la griglia non conosce a priori il tipo esatto degli elementi che conterrà, ma impone che essi derivino da GridElement. Il simbolo T <: GridElement è un upper type bound (vincolo di tipo superiore). Ogni tipo T che voglia essere utilizzato come elemento della griglia deve estendere GridElement; ciò consente di mantenere un contratto tipato e sicuro, senza sacrificare la flessibilità.
trait Grid[T <: GridElement]:Il trait Grid definisce l'interfaccia generale per una griglia, indipendentemente dal tipo di elementi T che contiene, garantendo astrazione e immutabilità.
trait Grid[T <: GridElement]:
def width: Int
def height: Int
def getElementAt(coordinate: Coordinate): Option[T]La companion object Grid fornisce metodi factory che creano istanze di Grid senza bisogno di un'istanza esistente. L'uso di Iterator.tabulate genera in modo funzionale tutte le coppie di coordinate senza mutazioni. Questo approccio è lazy ed efficiente in memoria, generando le coppie on-demand invece di materializzare immediatamente l'intera struttura.
object Grid:
def filledWith[T <: GridElement](width: Int, height: Int, element: T): Grid[T] =
val elementsMap =
Iterator.tabulate(width, height)((x, y) => Coordinate(x, y) -> element).flatten.toMap
MapBasedGrid(width, height, elementsMap)DistanceGrid
DistanceGrid estende la funzionalità della griglia introducendo operazioni avanzate per la gestione della simulazione di allagamenti: calcolo delle distanze tra celle, ricerca di celle adiacenti non completamente allagate, e trasformazioni funzionali. Si tratta di un'interfaccia funzionale dove ogni operazione restituisce una nuova istanza della griglia invece di modificare quella esistente.
trait DistanceGrid[T <: GridElement] extends Grid[T]:
def getNotFullNeighbors(coordinate: Coordinate): Seq[(Coordinate, T)]
def overwrite(coordinate: Coordinate, element: T): DistanceGrid[T]
def map(f: (Coordinate, T) => T): DistanceGrid[T]
def getNotEmptyCells: Seq[(Coordinate, T)]MapBasedDistanceGrid rappresenta l'implementazione concreta di DistanceGrid con supporto al calcolo delle distanze. L'architettura si basa su due pilastri fondamentali:
- Export clause: La clausola
exportè una funzionalità che implementa il pattern Delegation a livello sintattico, invece di ridefinire manualmente metodi wrapper. La dichiarazioneexport grid.{elementsMap, getElementAt, height, width}delega automaticamente questi membri, eliminando boilerplate e riducendo la superficie di errore. - Composition Over Inheritance:
MapBasedDistanceGridnon eredita daMapBasedGrid, ma lo contiene come dipendenza privataprivate val grid. Questo approccio favorisce flessibilità, l'implementazione interna può cambiare senza modificare l'interfaccia pubblica.
final case class MapBasedDistanceGrid[T <: GridElement](
private val grid: MapBasedGrid[T]
) extends DistanceGrid[T]:
export grid.{elementsMap, getElementAt, height, width}Calcolo delle distanze e ricerca dei vicini
Il metodo bfs implementa una ricerca in ampiezza (Breadth-First Search) per calcolare la distanza minima di ogni cella dalla lista di celle notFullCells. L'uso della ricorsione tail-recursive, annotata con @tailrec, garantisce che la funzione sia ricorsiva in coda e sia ottimizzata dal compilatore evitando l'overflow dello stack e rendendo l'algoritmo efficiente anche per griglie di grandi dimensioni.
@tailrec
def bfs(
queue: Queue[(Coordinate, Int)],
visited: Set[Coordinate],
distances: DistanceMap
): DistanceMap =
if queue.isEmpty then distances
else
val ((current, dist), dequeued) = queue.dequeue
val neighbors =
for
dx <- -1 to 1
dy <- -1 to 1
if dx != 0 || dy != 0
nextCoord = Coordinate(current.x + dx, current.y + dy)
if contains(nextCoord) && !visited.contains(nextCoord)
yield nextCoord
val newVisited = visited ++ neighbors
val newDistances = neighbors.foldLeft(distances)((acc, coord) => acc + (coord -> (dist + 1)))
val newQueue = dequeued ++ neighbors.map(c => (c, dist + 1))
bfs(newQueue, newVisited, newDistances)
val initialQueue = Queue.from(notFullCells.map(_ -> 0))
val initialDistances = notFullCells.map(_ -> 0).toMap
val initialVisited = notFullCells.toSet
bfs(initialQueue, initialVisited, initialDistances)Tutte le strutture dati utilizzate sono immutabili per default:
Set ++ Seqcrea un nuovoSetcon elementi aggiuntiMap + (key -> value)crea una nuovaMapcon l'entry aggiuntafoldLeftè una higher-order function ovvero una funzione che accetta altre funzioni come argomenti, applicata in modo funzionale per costruire nuove mappe senza mutazioni.
Il companion object implementa il Factory Method Pattern restituendo il tipo astratto DistanceGrid[T], non l'implementazione concreta MapBasedDistanceGrid[T]. L'uso di for-comprehension genera e trasforma collezioni in modo dichiarativo.
object DistanceGrid:
def filledWith[T <: GridElement](width: Int, height: Int, element: T): DistanceGrid[T] =
val elementsMap =
(for
x <- 0 until width
y <- 0 until height
yield Coordinate(x, y) -> element).toMap
MapBasedDistanceGrid(MapBasedGrid(width, height, elementsMap))WeatherService
WeatherEffectService è definito come sealed trait, limita le implementazioni del trait allo stesso file, garantendo un controllo esaustivo di pattern matching.
sealed trait WeatherEffectService:
def applyWeatherEffects(
cellGrid: DistanceGrid[Cell],
weatherGrid: Grid[Weather]
): DistanceGrid[Cell]applyCellWeatherEffect è una funzione curried che cattura il parametro weatherGrid in chiusura, producendo una funzione (Coordinate, Cell) => Cell compatibile con map. È uno degli elementi più idiomatici e rilevanti del codice, perché:
- Permette di preconfigurare il parametro
weatherGrid, riutilizzandolo per tutte le celle (grazie al currying); - Applica una trasformazione pura: la cella viene aggiornata solo se nella griglia meteorologica è presente un valore
Some(weather), altrimenti resta invariata grazie afold. - Evita ripetizioni, catturando
weatherGriduna sola volta invece di passarlo a ogni chiamata.
private def applyCellWeatherEffect(weatherGrid: Grid[Weather])(
coord: Coordinate,
cell: Cell
): Cell =
weatherGrid
.getElementAt(coord)
.fold(cell)(weather => cell.updateWater(weather.waterDelta))Controller
Il package controller rappresenta il livello di controllo e orchestrazione del sistema di simulazione. In questa sezione ho realizzato i seguenti componenti: ControllerFactory, SimulationController e SimulationListener.
ControllerFactory
La classe ControllerFactory funge da Abstract Factory Pattern per la creazione di oggetti SimulationController. DefaultControllerFactory applica il Factory Method Pattern per incapsulare la creazione di SimulationController e delle sue dipendenze.
Utilizza summon per recuperare istanze implicite given di SimulationStore e SimulationScheduler dal contesto, implementando così un pattern di Dependency Injection dichiarativo. Questo approccio evita di passare esplicitamente le dipendenze, migliorando la leggibilità e permettendo di sostituire facilmente le implementazioni nei diversi contesti.
private sealed class DefaultControllerFactory extends ControllerFactory:
def createController(params: SimulationParams): SimulationController =
val waterFlowService = WaterFlowService()
val weatherEffectService = WeatherEffectService()
val engine = DefaultSimulationEngine(params, waterFlowService, weatherEffectService)
val store = summon[SimulationStore]
val scheduler = summon[SimulationScheduler]
SimulationController(engine, store, scheduler)SimulationController
Il trait SimulationController definisce l'interfaccia pubblica del controller della simulazione. L'Observer Pattern viene realizzato tramite SimulationListener, che riceve aggiornamenti sugli eventi della simulazione. L'operatore :: (cons) prepende un elemento a una lista in tempo O(1), idiomatico in Scala per costruire liste immutabili.
def addListener(listener: SimulationListener): Unit =
listeners = listener :: listenersSimulationListener
Il trait SimulationListener definisce il contratto di notifica per i componenti che devono reagire ai cambiamenti della simulazione, rappresenta l'implementazione diretta dell'Observer Pattern.
View
Il package util.resource costituisce il modulo di gestione delle risorse grafiche dell'applicazione, responsabile del caricamento, caching e scalatura delle immagini associate a elementi della simulazione.
Cache
Cache rappresenta un meccanismo di caching generico che permette di caricare un insieme di oggetti una sola volta e di riutilizzarli senza accessi ripetuti al file system. Funge da infrastruttura base per componenti specializzati come ImageCache. L' implementazione PreloadedCache carica tutte le risorse all'inizializzazione (eager loading), evitando latenze durante l'esecuzione.
sealed trait Cache[K, A]:
def get(key: K): LoadResult[A]
private final class PreloadedCache[K, A](contents: Map[K, LoadResult[A]]) extends Cache[K, A]:
def get(key: K): LoadResult[A] =
contents.getOrElse(key, Left(ResourceError.InvalidKey(s"Resource not found: $key")))L' uso dello using nel metodo factory Cache.preloaded inietta implicitamente un ResourceLoader, rispettando il Dependency Inversion Principle.
object Cache:
def preloaded[K, A](keys: Iterable[K])(using loader: ResourceLoader[K, A]): Cache[K, A] =
val preloadedMap = keys.map(k => k -> loader.load(k)).toMap
PreloadedCache(preloadedMap)ImageCache
ImageCache implementa una cache a due livelli per ottimizzare l'accesso alle immagini: una cache base per le immagini originali precaricate all'inizializzazione (eager loading) e una cache secondaria per le versioni scalate calcolate on-demand (lazy loading).
private def forType[K](mapping: Map[K, String]): ImageCache[K] =
given baseLoader: ResourceLoader[K, BufferedImage] = PathBasedImageLoader(mapping)
val baseCache = Cache.preloaded(mapping.keys)
new ImageCache(mapping, baseCache)L'implementazione utilizza una Map immutabile per memorizzare le immagini scalate, aggiornata funzionalmente a ogni richiesta (lazy loading). Le immagini originali sono precaricate tramite Cache.preloaded, mentre le versioni scalate vengono generate solo al primo accesso, utilizzando un ResourceLoader implicito given che inietta automaticamente lo ImageScaler configurato.
ResourceLoader
ResourceLoader definisce il contratto per il caricamento di risorse usando due implementazioni concrete:
PathBasedImageLoadercarica immagini originali dal classpath utilizzandoImageIO.ScaledImageLoaderapplica scalatura bilineare alle immagini base per adattarle dinamicamente alle dimensioni richieste dall'interfaccia grafica.
Either[ResourceError, A] rappresenta il successo Right o l'errore tipizzato Left in modo funzionale.
Resource
Resource definisce l'astrazione per il caricamento di risorse e la gestione degli errori associati.
type LoadResult[A] = Either[ResourceError, A]Il type alias LoadResult[A] rappresenta il risultato di un'operazione di caricamento:
Right(A): risorsa caricata con successoLeft(ResourceError): errore tipizzato che descrive il fallimento
NavigationController
Il NavigationController implementa il controllo della navigazione grafica tra le schermate principali dell'applicazione senza conoscere la logica interna delle view o del modello. Ogni pannello riceve il controller come parametro e può richiedere un cambio di schermata senza manipolare direttamente la finestra.