Project conway26Java

Costruiamo?. Si veda Processi di costruzione del software .

Seguendo quanto detto in Il nostro metodo di lavoro, consideriamo questo progetto come lo Sprint1 per la costruzione incrementale ed evolutiva di un prodotto software.

Si veda GAME OF LIFE DI CONWAY

Premessa

Si suppone di avere definito un sito GIT che contiene solo Il file README.md e di avere clonato questo sito entro una directory di lavoro (es. iss2026) sul nostro PC.

Il file README.md

Il file README.md di un sito GIT è molto più di un semplice file di testo: è il biglietto da visita e lo strumento di marketing delle nostre attività.

Inoltre potrebbe fornire indicazioni utili come manuale d’uso delle applicazioni.

Il file deve essere scritto usando il Linguaggio Markdown.

# issLab2026
 Laboratorio di <b>Ingegneria dei Sistemi Software</b> a.a. 2025/2026 di Cognome Nome Matricola

 [Testo di riferimento:](https://anatali.github.io/issLab2026/_static/docs/Protobook.pdf)
 <!-- comment: [ancora personalizzata] -->

 <h2 id="ParteA">Parte A: Dai programmi ai Sistemi a Microservizi</h2>

 ### Sistema ConwayLife in locale
 [Riferimento: conway26Java Dai requisiti al deployment](https://anatali.github.io/issLab2026/Project%20conway26Java.html#conway26java-dai-requisiti-al-deployment)
 * [ConwayLife Sprint1](ConwayLife/Sprint1/conway26Java): impostazione di un primo prototipo
   in Java con dispositivi Mock di I/O. <i>Distribuzione</i>: file jar.
 * [ConwayLife Sprint2](ConwayLife/Sprint2/conway26Java): <b>evoluzione</b> del primo prototipo
   con un dispositivo di output realizzato in Swing. <i>Distribuzione</i>: file jar
 ### Sistemi come servizi
 * ...
 ### Sistema ConwayLife con pagine HTML
 * [ConwayLife Sprint3(ConwayLife/Sprint3/conway26Java): evoluzione del sistema in locale
   usando una pagina HTML come dispositivo di I/O. <i>Distribuzione</i>: Docker yaml.

Creiamo il progetto e impostiamo in Eclipse

.

_images/conway25JavaProjectEclipse.PNG

Inizializzazione

  • operiamo nella directory di lavoro iss2026

  • copiamo in questa la directory unibolibs (dal sito del corso)

  • creiamo la subdirectory iss2026/conway26Java

  • nella directory iss2026/conway26Java eseguire: gradle init 1 2 - -

  • importiamo il progetto in un workspace eclipse

  • definiamo il build.gradle

  • aggiungiamo Java nature

  • creiamo il source folder src

  • eseguiamo da un termimale gradlew eclipse per generare i file per Eclipse

  • scriviamo un file HelloWorld.java di prova

Esecuzione del prodotto

  • gradlew run o da Eclipse

Settaggio filtri per redere visibili resources e Gradle build folder

Parte applicativa: la fase di analisi e progettazione

  • leggiamo Il gioco come caso di studio

  • aggiungiamo folder userDocs e al suo interno:

    • il file di nome conway26Java_v0.html ricavato da template che ha il ruolo di un diario di bordo (con varie versioni)

    • la subdirectory userDocs/css per gli stili usati dal diario

    • la subdirectory userDocs/img per le immagini usate dal diario

  • riportiamo nel file conway26Java_v0.html i dati del progettista e scriviamo le sezioni Requirement analysis e Problem analysis con le opportune motivazioni

  • pensiamo alle procedure di testing funzionale (si veda procedure di testing funzionale) e scriviamo la sezione Test plans

  • impostiamo il progetto e scriviamo la sezione Project (prima di scrivere il codice)

Parte applicativa: la fase di realizzazione

  • aggiungiamo (se non esiste) come sourcefolder: src/main/java e le altre directory specificate in build.gradle

  • aggiungiamo il package conway

  • procediamo con la produzione del codice

  • aggiungiamo procedure di testing

conway26Java-build.gradle

plugins {
    id 'application'
    id 'java'
    id 'eclipse'
}
version '1.0'
java {
    toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}
repositories {
    mavenCentral()
    flatDir {   dirs '../unibolibs'    }
}
dependencies {
    /* UNIBO ******************************* */
    implementation name: 'uniboInterfaces'
    implementation name: '2p301'
    implementation name: 'unibo.basicomm23-1.0'
}
sourceSets {
    main.java.srcDirs += 'src'
    main.java.srcDirs += 'src/main/java'
    main.java.srcDirs += 'src/main/kotlin'
    main.java.srcDirs += 'src/main/resources'
    test.java.srcDirs += 'src/main/java/test'         //test is specific
    test.java.srcDirs += 'src/main/kotlin/test'
}
eclipse {
    classpath {
        sourceSets -= [sourceSets.main, sourceSets.test]
    }
}
application {
    //Define the main class for the application.
    mainClassName    = 'conway26Java.HelloWorld'
}
jar {
    println("building jar")
    from sourceSets.main.allSource
    from('./') {
      include '*.pl'
      include '*.json'
    }
    manifest {
        attributes 'Main-Class': "$mainClassName"
    }
}
task dovesiamo {
    println("projectDir= $projectDir")
    println("buildDir  = $buildDir")
}

In questa versione vi sono dipendenze solo relative alla libreria JUnit per fare testing.

unibolibs contiene le librerie

conway26Java Dai requisiti al deployment

Il nostro metodo di lavoro impone che si debba partire dai requisiti, che il committente ha specificato come segue:

conway26Java-Requirements

  1. Realizzare una versione in Java del gioco Life di Conway, come gioco zero-player. Il gioco consiste nell’introdurre una Griglia di Celle il cui stato (cella ‘viva’ o cella ‘morta’) evolve come stabilito dallle regole di ConwayLife

  2. L’utente umano deve poter:

    • specificare la configurazione iniziale della griglia del gioco

    • vedere l’evoluzione del gioco in forma opportuna (si veda Problema della vista del gioco)

    • fermare e far ripartire l’evoluzione del gioco

    • pulire (a gioco fermo) la configurazione della griglia del gioco

Il passo successivo è impostare l’analisi del problema e delineare una architettura logica del sistema.

conway26Java Analisi del problema

Una prima analisi del problema si può trovare in Analsi del problema GofLife, mentre un primo schema organizzativo del sistema da realizzare si trova in Progetto ConwayLife26

Lo schema rappresenta un modello del sistema, espresso utilizzando notazioni grafiche in stile UML. Lo riportiamo qui sotto, come prodotto della analisi del problema, prima ancora che di progetto.

Infatti:

  1. I requisiti menzionano una Griglia di Celle. Questi termini sono formalizzati nelle classi Grid e Cell ritenendo adeguato che questi enti siano realizzati in software come POJO.

  2. Le regole di Conway sono il ‘core business’ applicativo e fanno riferimento a Grid. La specifica di queste regole è affidata a un altro POJO, istanza di una classe Life.

  3. Il gico è zero-player, ma deve essere controllato dall’utente attraverso dispostivi di I/O. Come analisti, non riteniamo che questo compito sia responsabilità di nessuna delle classi precedenti, per cui si ritiene necessario introdurre un nuovo POJO, di classe LifeController

  4. I dispositivi di I/O potrebbero essere i più diversi. Occorre rendere LifeController indipendente dai dettagli tecnologici dell’I/O definiìendo opportuni contratti (formalizzati come interface Java) che i dispostivi dovrenno rispettare.

conway26Java-ArchitetturaLogica

Questo modello è statto definito in fase di analisi del problema per delineare una architettura (logica) del sistema che:

  • tiene conto dei Principi SOLID separando le responsabilità dei diversi componenti (espressi in stile OOP )

  • imposta il componente LifeController in modo tale che la parte proattiva del gioco sia indipendente dalle parti che realizzano l’input-output di interazione con l’utente umano e dai dettagli tecnologici di realizzazione dei dispositivi di I/O.

  • imposta il sistema software in modo molto diverso (e migliore) da come si presenta nel codice (ispirato al Paradigma Funzionale ) ConwayLife.py proposto dalla AI, come detto nel preludio

    Perchè migliore?

    • esprime in modo esplicito (con opportune classi Java) i concetti-base del dominio applicativo (Grid e Cell) stabilto nel testo dei requisiti

    • l’entità Cell non viene ‘ridotta’ a un array di interi o booleani

    • introduce una entità (Life) che ha come sola responsabilità le regole del gioco, seprandola da una diversa entità (LifeController) responsabile della interazione cone l’utente (che nella versione Python viene espressa nel main !!)

    • prevede la possibilità di configurare il sistema con diversi dispositivi di I/O, lasciando inalterato il codice realativo al controllo del gioco, come anticipato in MainConwayLifeJava.java

    • ammette la possibilità che LifeController possa interrompere e riavviare la esecuzione del gioco, cosa che la versione Python non fa

  • permette di definire un primo insieme di test funzionali concentrati sulla classe Life, che definisce il ‘motore logico’ del gioco.

conway26Java Piani di Testing funzionale

La introduzione di un Piano di test funzionale fin dalle fasi di analisi, è fondamentale per guidare la fase di realizzazione del codice. Ma sorge un problema:

come si fa a definire un piano di test funzionale se non si è ancora scritto una riga di codice?

La risposta consiste nel basarsi sul cosa (WHAT) il sistema dovrà fare e non sul come (HOW) verrà realizzato il sistema software.

Prendiamo ad esempio la classe Life che definisce il motore logico del gioco. Dalla specifica del gioco, possiamo dedurre che questa classe dovrà sostenere almeno a un insieme di funzionalità esprimibile in modo formale mediante una sinterface Java:

public interface LifeInterface {
    /** Restituisce il numero di righe e colonne della griglia*/
    int getRows();
    int getCols();
    /** Imposta lo stato di una cella */
    void setCell(int x, int y, boolean alive);
    /** Restituisce lo stato di una cella specifica */
    boolean isAlive(int x, int y);
    /** Calcola l'evoluzione dello stato alla generazione successiva */
    void nextGeneration();
    /** Pulisce la griglia (nessuna cella viva) */
    void clear();
    /** Restituisce una rappresentazione grafica testuale della grglia*/
    public String gridRep( );
}

A questo punto, possiamo definire un piano di test funzionale per il sistema, basandoci sui requisiti del gioco e sulla logica del gioco stesso. Ad esempio:

conway26Java-Impostazione Testing

public class ConwayLifeTest {

    @Before
    public void setup() {
        System.out.println("setup");        }

    @After
    public void down() {
        System.out.println("down");
    }

conway26Java-testOscilla

  @Test
  public void testOscilla() {
          System.out.println("testOscilla ---------"  );
  LifeInterface liferules = new Life(5, 5);
  // Configurazione orizzontale
    liferules.setCell(2, 1, true);
    liferules.setCell(2, 2, true);
    liferules.setCell(2, 3, true);
    System.out.println("testOscilla | Stato Iniziale:\n" + liferules.gridRep());

    liferules.nextGeneration();
    System.out.println("testOscilla | after 1 gen:\n" + liferules.gridRep());
    // Verifica che sia diventato verticale
    assertTrue(liferules.isAlive(1, 2));
    assertTrue(liferules.isAlive(2, 2));
    assertTrue(liferules.isAlive(3, 2));
    assertFalse(liferules.isAlive(2, 1));

    liferules.nextGeneration();
    System.out.println("testOscilla | after 2 gen :\n" + liferules.gridRep());
    // Verifica che sia tornato orizzontale (Periodo 2)
    assertTrue(liferules.isAlive(2, 1));
    assertTrue(liferules.isAlive(2, 2));
    assertTrue(liferules.isAlive(2, 3));
}

conway26Java-testOscillaFromFile

Un test potrebbe anche essere impostato in modo Data-Driven, leggendo la configurazione iniziale da un file.

@Test
public void testOscillaFromFile() throws Exception {
    // Carico un Blinker (periodo 2)
    boolean[][] initial = PatternLoader.loadFromResource("src/test/resources/blinker.txt", 5, 5);

    Life liferules = new Life(initial);

    System.out.println( liferules.formatGrid() );
    System.out.println( "__________________________________________" );

    liferules.nextGeneration(); // Generazione 1 (cambia stato)
    System.out.println( liferules.formatGrid() );
    System.out.println( "__________________________________________" );
    liferules.nextGeneration(); // Generazione 2 (deve tornare all'originale)
    System.out.println( liferules.formatGrid() );
    System.out.println( "__________________________________________" );

    assertArrayEquals("L'oscillatore deve tornare allo stato iniziale dopo 2 passi",
                      initial, liferules.getGrid());
}

conway26Java-Costruttori

Scrivere un TestPlan prima che la classe Life esista potrebbe indurre a decidere subito:

“Come passo la (specifica della) griglia? Con una matrice di booleani? Con un array di oggetti Cell? Come leggo il risultato?”.

Tuttavia ci sono modi per ‘differire’ queste scelte.

La procedura di testing conway26Java-testOscillaFromFile passa al costruttore di Life una griglia preconfigurata di nome initial. Se il costruttore di Life scrive this.grid=initial, si copia un riferimento. Ciò può essere pericoloso, perchè una modifica (dall’esterno di Life) di initial modifica anche l’interno della classe. Per evitare problemi, è opportuno fare una deep copy dell’array:

//Da Java8 in avanti
private boolean[][] deepCopyJava(boolean[][] original) {
    return Arrays.stream(original)
                .map(boolean[]::clone)
                .toArray(boolean[][]::new);
}

Come Gemini spiega il codice

  • Arrays.stream(original): Prende la matrice (che è un “array di array”) e la trasforma in un flusso di dati (Stream). Ogni elemento di questo stream è una singola riga (boolean[]).

  • .map(boolean[]::clone): Per ogni riga che passa nello stream, viene chiamato il metodo clone().

    • L’operatore :: (Method Reference) dice a Java: “Prendi ogni riga e fanne una copia indipendente”.

    • Questo assicura che le righe della nuova matrice siano fisicamente diverse da quelle della vecchia.

  • .toArray(boolean[][]::new): Infine, raccoglie tutte queste nuove righe clonate e le impacchetta in una nuova matrice bidimensionale (boolean[][]).

conway26Java-Progettazione&realizzazione

La conway26Java-ArchitetturaLogica, Life definisce la griglia come un oggetto di classe Grid che contiene al suo interno una array (Cell[][] gridrep) di Cell che danno ‘consistenza ontologica’ alle celle, non più ‘ridotte’ ad array di booolean o di integer.

La procedura di testing conway26Java-testOscilla è più aderente a questa impostazione in quanto passa al costruttore di Life il numero di righe e colonne, lasciando alla classe la responsabilità (e l’opportunità) di definire la griglia come crede.

Si potrebbe anche pensare (con riferimento ai design patterns) di introdurre un Factory method in cui le dimensioni della griglia possono essere:

  • stabilite di default

  • lette da un file di configurazione

  • passate come parametri al metodo

MainConwayLifeJava

Il Main (o Composition Root) è il luogo in cui la teoria dell’architettura incontra la pratica dell’esecuzione. In questa fase, invece di lasciare che le classi si istanzino tra loro (creando accoppiamento), è il programmatore a “iniettare” le dipendenze.

Ecco come Gemini propone di assemblare il sistema seguendo il diagramma UML:

public class Main {
    public static void main(String[] args) {
        // 1. Inizializziamo il Modello (Il "Cosa")
        // Life crea internamente la sua Grid (Composizione)
        Life gameModel = new Life(20, 40);

        // 2. Inizializziamo l'Output (Il "Dove")
        // Possiamo decidere qui se usare la Console o una GUI
        IOutDev outputDevice = new OutDev();

        // 3. Inizializziamo il Controller (Il "Come")
        // Iniettiamo il modello e l'output nel controller (Dependency Injection)
        GameController lifeController = new LifeController(gameModel, outputDevice);

        // 4. Inizializziamo l'Input (L' "Innesco")
        // L'input comunica con il controller
        InputDev inputDevice = new InputDev(lifeController);

        // 5. Avvio del sistema
        System.out.println("Sistema S: Inizializzazione completata.");
        inputDevice.onStartButtonPressed();
    }
}

conway26Java-Coding

Il passaggio dal modello al codice richiede che quanto definito dall’analista sia ben compreso dal progettista e che questi lo traduca in codice in modo confacente alla specifica.

Anche in questa fase, la AI può aiutare: si veda conway26Java-Chiesto a Gemini. In pratica, utilizziamo la AI come un ‘generatore di codice’ guidato dal nostro modello di sistema e non semplicemente da quanto la AI trova in rete.

Un prompt per Gemini

La figura allegata vuole definire l’architettura logica di un sistema software che realizza il gioco Life di Conway. Vorrei che tu ricavassi i concetti importanti che si possono desumere da questo daigramma (in stile UML)

— Uno schema per partire —

Uno schema per partire

Per agevolare questa transizione, è disponibile un workspace di riferimento nella cartella ConwayLifeProject (in ConwayLifeSprint1). Si noti che:

  • sono definiti due package (domain e devices) per non mescolare il codice relativo al dominio e il codice relativo ai dispositivi di I/O.

  • le interfacce IOutDev.java e GameController.java sono definite nel package domain per ribadire che sono i dispositivi che dovranno adattarsi alle esigenze del dominioe non viceversa

conway26Java-TestReports

Una volta definito il codice, le specifiche di testing possono essere eseguite come Run As Junit Test oppore attivando gradle:

  • gradlew test e aprire il file build\reports\tests\test\index.html per vedere il risultato del test

conway26Java-JavaCodeCoverage

Aggingiamo nel file build.gradle il plugin JaCoCo e la configurazione del task jacocoTestReport:

jacocoTestReport {
  dependsOn test // Il report viene generato solo dopo l'esecuzione dei test

  reports {
      xml.required = false
      csv.required = false
      html.outputLocation = layout.buildDirectory.dir('reports/jacoco')
  }
}

Ora possiamo generare report che mostrano quali righe di codice sono state “visitate” dai test JUnit:

  • gradlew test jacocoTestReport e aprire il file build\reports\jacoco\index.html per vedere il risultato del test

jacoco-sessions.html

Questo file è uno strumento di diagnostica che dice “quando” e “come” è avvenuta la raccolta dei dati. Serve a confermare che il “motore” di JaCoCo stia effettivamente analizzando i file .class giusti nel momento in cui JUnit esegue i test.

conway26Java-Esecuzione

  • In build.gradle definiamo mainClassName='conway26Java.MainConwayLifeJava'

  • gradlew run per eseguire il sistema

conway26Java-Deployment

Al momento, ci limitiamo a generare un file jar con le classi dello Sprint1:

Il file .jar

Un file .jar (Java ARchive) è un formato compresso basato su ZIP utilizzato per raggruppare classi Java, risorse (immagini, suoni) e metadati in un unico file per una facile distribuzione ed esecuzione. Richiede Java Runtime Environment (JRE) installato per funzionare e spesso contiene un file “manifest” per definire la classe principale eseguibile.

  • gradlew jar per generare il file jar conway26Java-1.0.jar nella directory build\libs

Il file non è eseguibile, perchè non include le librerie per le dipendenze. Tuttavia esso può essere usato come libreria nello Sprint2 del progetto.

Generazione di un file jar eseguibile

Volendo generare codice eseguibile autocontenuto, il modo più semplice è usare il task distZip (o distTar) reso disponibile dal plugin application.

Questo task impacchetta tutto il necessario per far girare il programma (file JAR, dipendenze esterne e script di avvio per Windows/Linux) in un unico archivio compresso. Il task deposita il risultato nella directory build/distributions/conway26Java-1.0.zip

Il file zip contiene due directory:

  • bin/ : Contiene gli script eseguibili (uno shell script per Linux/Mac e un file .bat per Windows). Questi script impostano automaticamente il CLASSPATH e lanciano la classe Main

  • lib/ : Contiene il file JAR del sistema più tutti i file JAR delle librerie esterne.

Per eseguire il sistema:

  1. scompattare il file build/distributions/-conway26Java1.0.zip in una directory

  2. nella directory ``bin/, eseguire (in Windows) lo script

conway26Java

Generare un file jar ‘fat’

In alternativa, si può creare un file jar “fat” che ingloba i file jar delle librerie esterne. Un esempio di un tale task è come ad esempio il task jarfat(type: Jar) { ... } definito in: https://github.com/anatali/issLab2026/blob/main/ConwayLife/Sprint1/conway26Java/build.gradle. Il comando:

gradlew jarfat

crea il file build/libs/conway26JavaFat-1.0.jar che può essere eseguito con il comando:

java -jar conway26JavaFat-1.0.jar

Attivare GitHub Pages

Per rendere leggibile il file conway26Java_v0.html direttamente dal proprio repository GIT, procediamo come segue:

  1. Premessa: il sito deve essere public

  2. Creiamo la cartella docs e poi:

    • copiamo in docs il file conway26Java_v0.html insieme alle directories di userDocs (se sono usate dal file) css e img

    • insseriamo un file index.html

  3. Attiviamo le GitHub Pages:

  • Vai in Settings del repository GIT.

  • Clicca su Pages nella colonna a sinistra.

  • Sotto “Build and deployment”, imposta la sorgente su “Deploy from a branch”.

  • Scegli il branch (es. main) e la cartella /docs.

  • Clicca su Save.

Dopo un paio di minuti, il sito sarà online all’indirizzo https://utente.github.io/nome-repo/.