Grammatica UML Rigorosa

L'UML è il linguaggio formale dell'architettura del software. La conoscenza esatta della sua sintassi (i 7 diagrammi principali) è un requisito ineludibile in sede di progettazione.

1. Class Diagram (Diagramma delle Classi)

Descrive la struttura statica. È vitale conoscere la semantica visiva delle relazioni:

  • Inheritance (Ereditarietà): Freccia con punta vuota. Indica una relazione "is-a".
  • Composition (Composizione): Rombo pieno. Relazione forte "part-of". Il ciclo di vita è vincolato: se il contenitore viene distrutto, anche le parti muoiono (es. una Casa e le sue Stanze).
  • Aggregation (Aggregazione): Rombo vuoto. Relazione debole "has-a". Le parti possono esistere indipendentemente (es. un Dipartimento e i suoi Professori).
  • Dependency (Dipendenza): Freccia tratteggiata. Relazione "uses" temporanea (es. un metodo riceve un oggetto come parametro).
classDiagram
    User <|-- Admin : Inheritance (is-a)
    House *-- Room : Composition (part-of)
    Department o-- Professor : Aggregation (has-a)
    OrderController ..> OrderValidator : Dependency (uses)
          
2. Design Sequence Diagram (ISD - Interaction Sequence Diagram)

A differenza dell'SSD, questo diagramma "esplode" la scatola nera e rappresenta le interazioni dinamiche interne tra gli oggetti del sistema. I messaggi si dividono in Sincroni (freccia piena, blocca il chiamante), Asincroni (freccia aperta) e di Ritorno (linea tratteggiata). I flussi logici complessi si modellano coi Combined Fragments:

  • alt: Percorsi alternativi mutuamente esclusivi (il classico costrutto if/else o switch).
  • loop: Iterazioni ripetute finché la guardia (condizione tra parentesi quadre) è vera.
  • opt: Frammento opzionale (un blocco if senza l'else). Si esegue solo se la guardia è vera.
  • par: Frammenti eseguiti in parallelo (gestione della concorrenza / multi-threading).
sequenceDiagram
    actor Cliente
    participant Sistema
    participant Database

    Cliente->>Sistema: checkout(cart)
    alt cart isEmpty
        Sistema-->>Cliente: Errore
    else
        Sistema->>Database: saveOrder()
        Database-->>Sistema: orderId
        loop Per ogni item
            Sistema->>Database: updateInventory()
        end
        opt wantsNewsletter
            Sistema-)Sistema: sendEmail (asincrono)
        end
        Sistema-->>Cliente: Completato
    end
          
Visualizza Esempio Avanzato PlantUML (Design Controller)
@startuml
skinparam sequenceArrowThickness 1.5
skinparam sequenceMessageAlign center

actor Giocatore
participant GameController
participant Simulatore
participant EntityFactory
participant Citta
participant Griglia
participant Cella

group 1.2 CREAZIONE ENTITA' NELLA MAPPA
    Giocatore -> GameController : costruisci(x, y, z, strumento, rotazione)
    activate GameController
        
        GameController -> Simulatore : costruisci(...)
        activate Simulatore
            
            Simulatore -> EntityFactory : creaEntita(strumento)
            activate EntityFactory
                EntityFactory --> Simulatore : nuovaEntita
            deactivate EntityFactory

            Simulatore -> Griglia : posizionaEntita(...)
            activate Griglia
                
                alt cella valida e budget sufficiente
                    Griglia -> Citta : setBudget(budget - costo)
                    Griglia -> Cella : setEntita(nuovaEntita)
                    Griglia --> Simulatore : true
                else
                    Griglia --> Simulatore : false
                end
            deactivate Griglia

        deactivate Simulatore
        GameController --> Giocatore : aggiornaMappa()
    deactivate GameController
end
@enduml
Altri Diagrammi Essenziali
  • Use Case Diagram: Definisce le funzionalità utente. Le relazioni avanzate sono <<include>> (comportamento condiviso obbligatorio) ed <<extend>> (comportamento opzionale/aggiuntivo).
  • Component & Deployment Diagram: Mappatura fisica HW/SW. Si distinguono i Device Node (macchine fisiche) dagli Execution Environment Node (es. JVM, Docker). Il software in esecuzione prende il nome di Artifacts. Le interfacce si delineano in Lollipop (servizio offerto) e Socket (servizio richiesto).
  • Activity & State Diagram: Modellano macchine a stati o workflow, usando nodi iniziali (pallino nero), nodi finali (pallino nero cerchiato) e "composite states" per stati annidati.
flowchart LR
    C((Cliente))
    UC1([Pagare Ordine])
    UC2([Autenticarsi])
    UC3([Aggiungere Assicurazione])
    
    C --> UC1
    UC1 -. "<<include>>" .-> UC2
    UC3 -. "<<extend>>" .-> UC1
          

Costruzione del Domain Model e SSD

Prima di progettare il codice, bisogna modellare i concetti puramente logici del business.

Domain Model e Description Classes
  • Domain vs Data Model: Il Domain Model non è lo schema del database. Rappresenta astrazioni del mondo reale (oggetti fisici, concetti, transazioni) estraendo i "sostantivi" (Noun phrases) dai casi d'uso. Esclude rigorosamente costrutti tecnici software (es. DatabaseConnection).
  • Description Classes: Per evitare la perdita di informazioni di catalogo, si separa la classe fisica (es. Item con un numero seriale univoco) dalla sua descrizione (es. ProductDescription). In questo modo, se il negozio vende tutti gli "Item" fisici di un certo modello, le informazioni su quel prodotto (prezzo, specifiche) non vanno perse.
System Sequence Diagram (SSD) e il Controller

L'SSD mappa le interazioni visualizzando il Sistema come una Scatola Nera (Black Box). Non mostra le chiamate interne, ma solo l'attore esterno che invia messaggi al sistema nel suo complesso. Questi messaggi (Eventi di Sistema) sono fondamentali: essi diventeranno l'elenco esatto dei metodi che il pattern architetturale Controller dovrà intercettare e gestire.

            sequenceDiagram
                actor Giocatore
                participant Sistema

                Giocatore->>Sistema: 1. costruisci(x, y, z, strumento)
                Sistema-->>Giocatore: 2. aggiornaMappa()
                
                Giocatore->>Sistema: 3. demolisci(x, y, z)
                Sistema-->>Giocatore: 4. aggiornaMappa()
          
Come vedi, nell'SSD non esistono oggetti di design interni come EntityFactory o Griglia presenti nell'ISD. Esiste solo l'entità Sistema. I metodi invocati dal Giocatore diventeranno la firma esatta dei metodi pubblici del nostro GameController.

Dal Modello al Codice (Transformations & BCE)

Il passaggio dal Modello UML al Codice avviene tramite trasformazioni formali per massimizzare l'efficienza e ridurre la ridondanza (Refactoring):

1. Mappatura Base (Associazioni e Metodi)

Le Associazioni UML strutturali diventano attributi di riferimento (istanze di altre classi). I messaggi dinamici diventano chiamate a metodo.

                classDiagram
                    class Giocatore {
                      -Mappa mappaAttuale
                      +avviaPartita()
                    }
                    class Mappa {
                      +genera(seed)
                    }
                    Giocatore --> Mappa : "1" mappaAttuale
            
public class Giocatore {
    // L'associazione UML diventa attributo
    private Mappa mappaAttuale;

    public void avviaPartita() {
        // Il messaggio diventa chiamata a metodo
        mappaAttuale.genera(42);
    }
}

2. Pull Up Field & Constructor

Se più sottoclassi condividono un attributo o una logica di inizializzazione, vengono "tirati su" nella superclasse comune per massimizzare il polimorfismo.

classDiagram
    class Entita {
      #int hp
      +Entita(hp)
    }
    class Edificio {
      +Edificio()
    }
    class Unita {
      +Unita()
    }
    Entita <|-- Edificio
    Entita <|-- Unita
            
public abstract class Entita {
    protected int hp;
    
    public Entita(int hp) {
        this.hp = hp;
    }
}

public class Edificio extends Entita {
    public Edificio() {
        super(100); // Costruttore della madre
    }
}

3. Collapsing Objects

Se un oggetto del dominio ha solo dati e non ha comportamento, lo si "collassa" nel design trasformandolo in un semplice attributo della classe che lo possiede.

classDiagram
    %% Prima: Oggetto logico
    class Persona {
      -String nome
    }
    class CodiceFiscale {
      -String codice
    }
    Persona "1" --> "1" CodiceFiscale : possiede >
            
public class Persona {
    private String nome;
    
    // CodiceFiscale rimosso come classe.
    // Viene "collassato" in una String!
    private String codiceFiscale;
}

4. Delaying Expensive Computations

Per caricamenti lenti (es. grosse texture grafiche), si implementa il pattern Proxy per caricare il dato Lazy (solo alla primissima richiesta reale), risparmiando RAM.

classDiagram
    class Texture {
      +disegna()
    }
    class ProxyTexture {
      -Texture veraTexture
      +disegna()
    }
    ProxyTexture ..> Texture : istanzia
            
public class ProxyTexture {
    private Texture veraTexture;
    
    public void disegna() {
        if (veraTexture == null) {
            // Caricata in RAM solo se serve
            veraTexture = new Texture();
        }
        veraTexture.disegna();
    }
}
Categorizzazione Logica BCE

Per garantire un Low Coupling a livello architetturale, le classi vanno rigorosamente divise in 3 strati:

  • Boundary Objects: Gestiscono le interfacce verso il mondo esterno (la UI, le API, altri sistemi).
  • Control Objects: Ricevono gli eventi dai Boundary e coordinano le entità. Non fanno vera logica, ma agiscono da "vigili urbani".
  • Entity Objects: Contengono la vera e propria logica di business e i dati fondamentali del dominio.

I Pattern GRASP

GRASP (General Responsibility Assignment Software Patterns) rappresenta un insieme di nove principi guida per l'assegnazione delle responsabilità alle classi e agli oggetti nell'Object-Oriented Design. Non sono "pezzi di codice" pronti all'uso, ma un vocabolario e una mentalità fondamentale per creare software ben strutturato.

Cosa significa "Responsabilità"?

Nella progettazione a oggetti, una responsabilità rientra generalmente in due categorie:

  • Knowing (Conoscere): La responsabilità di un oggetto di conoscere i propri dati privati, di conoscere gli oggetti a lui correlati, o le informazioni che è in grado di calcolare.
  • Doing (Fare): La responsabilità di fare qualcosa direttamente, coordinare attività con altri oggetti, o creare/distruggere istanze.

Quando ci si siede a progettare un sistema e ci si chiede "A quale classe assegno questa funzione?" oppure "Chi deve creare questo oggetto?", si ricorre ai pattern GRASP per trovare la risposta più razionale (che mantenga il codice pulito, altamente coeso e a basso accoppiamento).

GRASP 01: Information Expert fondamentale

Assegna la responsabilità alla classe che possiede le informazioni necessarie per adempierla. Il principio è semplice: chi sa, fa. Non spostare i dati fuori dalla classe per elaborarli altrove.

Definizione formale · IE

Quando devi assegnare una responsabilità, inizia chiedendoti: chi ha i dati per soddisfarla? Quella classe è l'esperta dell'informazione e dovrebbe gestire la responsabilità in modo autonomo. Questo riduce l'accoppiamento, perché non è necessario esporre i dati verso l'esterno.

responsabilità
Esempio 1 — Gioco (dal progetto)

Player ha score e lives. Quindi è Player a calcolare il proprio totale punti con getTotalScore(), non un manager esterno che legge i campi. Se il calcolo cambia, si modifica solo Player.

Esempio 2 — E-commerce

Un Order conosce i propri OrderItem. Quindi il totale dell'ordine si calcola con order.getTotal(), non con un servizio esterno che somma i prezzi dopo averli estratti uno a uno. Order è l'esperto del suo contenuto.

Esempio 3 — Biblioteca

Un oggetto Book conosce il proprio ISBN, titolo e autore. È lui a decidere se due libri sono uguali (equals()) e come si rappresenta come stringa (toString()). Non serve un BookComparator esterno che accede ai campi pubblici.

Esempio 4 — Scacchi

Un ChessPiece conosce la propria posizione e tipo. È lui a dire se una mossa è valida: piece.isValidMove(from, to). Non è il Board che deve conoscere le regole di ogni singolo pezzo.

Esempio 5 — Simulatore di Città

In un gestionale con Simulatore, Griglia e Cella, siccome la Griglia detiene la mappa con popolazione, inquinamento e strade, è lei a dover calcolare i collegamenti (es. tramite un algoritmo BFS). Non deve essere il Simulatore a farsi estrarre tutti i dati dalla griglia per calcolarsi il percorso esternamente. La griglia è l'esperta della propria topologia.

Violazione

Il Simulatore estrae l'elenco delle celle dalla Griglia e si calcola da solo i percorsi BFS. I dati si trovano nella Griglia, ma la logica è nel Simulatore.

classDiagram
  direction TB
  class Simulatore {
    +calcolaPercorsiBFS(Griglia g)
  }
  class Griglia {
    -celle: List~Cella~
    +getCelle() List~Cella~
  }
  Simulatore ..> Griglia : "Estrae dati"
            
Corretto

Griglia.calcolaPercorsiBFS() incapsula l'algoritmo di calcolo nel posto in cui risiedono i dati (le celle). Il Simulatore si limita a invocare l'operazione.

classDiagram
  direction TB
  class Simulatore {
    +aggiornaTraffico()
  }
  class Griglia {
    -celle: List~Cella~
    +calcolaPercorsiBFS()
  }
  Simulatore ..> Griglia : "Invoca calcolo"
            
Player.java — Information Expert
public class Player {
  private int baseScore;
  private double multiplier;
  private int bonusPoints;

  //  Player è l'esperto: conosce tutti i dati necessari
  public int getTotalScore() {
    return (int)(baseScore * multiplier) + bonusPoints;
  }

  //  Solo Player sa se ha ancora vite
  public boolean isAlive() {
    return lives > 0;
  }
}

//  Violazione — il controller non dovrebbe sapere come si calcola il punteggio
// int total = (int)(player.baseScore * player.multiplier) + player.bonusPoints;

GRASP 02: Creator

B dovrebbe creare istanze di A se: B contiene o aggrega A; B registra A; B usa strettamente A; o B ha i dati di inizializzazione di A. La regola evita creazioni sparse ovunque nel codice.

CR · Chi crea chi?

La responsabilità di creare un oggetto appartiene a chi ha il contesto per farlo in modo sensato. Centralizzare la creazione riduce la dipendenza da dettagli costruttivi e rende il codice più testabile.

creazione
Esempio 1 — Gioco

GameSession aggrega i Player per tutta la sessione, quindi è GameSession a crearli: new Player(name, startingLives). Ha anche i dati di configurazione iniziale.

Esempio 2 — Fatturazione

Un Invoice aggrega le sue InvoiceLine. È Invoice ad aggiungere righe: invoice.addLine(product, qty, price), non un servizio esterno che costruisce righe e le inietta.

Esempio 3 — Forum

Un Thread contiene Post. Quando un utente risponde, è il Thread a creare il nuovo Post: thread.addReply(author, content). Il Thread ha tutto il contesto: data, posizione nella sequenza, metadati.

Esempio 4 — Gioco di carte

Un Deck (mazzo) aggrega e registra tutte le Card. È lui a crearle nella fase di setup: deck.build() genera le 52 carte. Nessun'altra classe deve sapere come si costruisce una carta.

Esempio 5 — Entity Factory (dal progetto)

Nel caso in cui la logica di creazione di un'entità diventi complessa (es. decidere se istanziare una Casa, un Ospedale o un Parco), non usiamo più new direttamente, ma introduciamo una classe EntityFactory. La Factory si assume la piena e unica responsabilità della creazione (diventando il Creator ufficiale) e solleva la Griglia da questo fardello.

Violazione

Un GameController fa new Player(...) in mezzo alla logica di gioco. Il controller non aggrega Player, quindi non è il responsabile naturale della creazione.

Corretto

GameSession.startSession() crea i Player. La sessione li possiede, li gestisce, e ha tutti i parametri di setup necessari.

EntityFactory.java — Factory Pattern
//  La Factory accentra tutta la logica complessa di creazione e inizializzazione
public class EntityFactory {
  public static Entity createEntity(String type, int x, int y) {
    switch (type.toLowerCase()) {
      case "casa":
        return new HomeEntity(x, y, 4); // 4 abitanti di default
      case "ospedale":
        return new HospitalEntity(x, y, 100); // 100 posti letto
      case "parco":
        return new ParkEntity(x, y, 50); // 50 capienza
      default:
        throw new IllegalArgumentException("Tipo entità sconosciuto: " + type);
    }
  }
}

// Nel codice della Griglia o del gestore eventi:
// Entity newBuilding = EntityFactory.createEntity("casa", 10, 20);
// griglia.addEntity(newBuilding);

GRASP 03: Low Coupling fondamentale

Minimizza le dipendenze tra classi. Un'alta dipendenza (high coupling) significa che una modifica in A costringe a cambiare B, C, D. Con basso accoppiamento le classi sono intercambiabili e testabili in isolamento.

LC · Forme di accoppiamento

L'accoppiamento può essere su tipi concreti (il peggiore), su interfacce (accettabile) o su astrazioni (ottimale). L'obiettivo non è zero dipendenze — impossibile — ma dipendenze verso le astrazioni giuste.

accoppiamento

High Coupling (Il problema)

Un "piatto di spaghetti": se modifichi A, l'errore si propaga a cascata a B, C e D perché sono strettamente intrecciate.

flowchart LR
    A[Classe A
Modificata]:::bad --> B[Classe B]:::grey A --> C[Classe C]:::grey B --> D[Classe D]:::grey C --> B A --> D

Low Coupling (La soluzione)

Design a "stella": le classi comunicano tramite un'Astrazione (es. Interfaccia). Modificare A non rompe le altre classi.

flowchart TD
    A[Classe A
Modificata]:::good -.-> I[ Astrazione ] B[Classe B]:::grey -.-> I C[Classe C]:::grey -.-> I D[Classe D]:::grey -.-> I style I fill:#1a1a24,stroke:#666,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
Esempio 1 — Gioco

GameController dipende da IGameObserver, non da ScoreUI o SoundManager. Cambiare tutta la UI non richiede toccare il controller.

Accoppiamento Forte (Problema)
flowchart LR
    GC[GameController]:::grey --> SUI[ScoreUI]:::bad
    GC --> SM[SoundManager]:::bad
              
Basso Accoppiamento (Soluzione)
flowchart LR
    GC[GameController]:::grey -.-> I[ IGameObserver ]
    SUI[ScoreUI]:::grey -.->|implementa| I
    SM[SoundManager]:::grey -.->|implementa| I
    
    style I fill:#1a1a24,stroke:#28c840,stroke-dasharray: 5 5,color:#fff
              
Esempio 2 — Repository pattern

Un servizio dipende da IUserRepository, non da MySQLUserRepository. In test si inietta un finto repo in memoria; in produzione si usa quello reale. Il servizio non cambia mai.

Accoppiamento Forte (Problema)
                flowchart LR
                    S[UserService]:::grey --> M[MySQLRepo]:::bad
              
Basso Accoppiamento (Soluzione)
flowchart LR
    S[UserService]:::grey -.-> I[ IUserRepository ]
    M[MySQLRepo]:::grey -.->|implementa| I
    
    style I fill:#1a1a24,stroke:#28c840,stroke-dasharray: 5 5,color:#fff
              
Esempio 3 — Notifiche

Un OrderService non chiama direttamente EmailSender.send() o SmsSender.send(). Dipende da un'astrazione. Aggiungere notifiche WhatsApp non tocca il servizio.

Accoppiamento Forte (Problema)
flowchart LR
    OS[OrderService]:::grey --> ES[EmailSender]:::bad
    OS --> SMS[SmsSender]:::bad
              
Basso Accoppiamento (Soluzione)
flowchart LR
    OS[OrderService]:::grey -.-> I[ INotifier ]
    ES[EmailSender]:::grey -.->|implementa| I
    SMS[SmsSender]:::grey -.->|implementa| I
    
    style I fill:#1a1a24,stroke:#28c840,stroke-dasharray: 5 5,color:#fff
              
Esempio 4 — Logging

Le classi di business non dipendono da System.out.println() né da Log4j. Dipendono dall'interfaccia ILogger sostituibile a runtime.

Accoppiamento Forte (Problema)
flowchart LR
    B[BusinessLogic]:::grey --> L4J[Log4j]:::bad
    B --> SOUT[System.out]:::bad
              
Basso Accoppiamento (Soluzione)
flowchart LR
    B[BusinessLogic]:::grey -.-> I[ ILogger ]
    L4J[Log4jAdapter]:::grey -.->|implementa| I
    C[ConsoleAdapter]:::grey -.->|implementa| I
    
    style I fill:#1a1a24,stroke:#28c840,stroke-dasharray: 5 5,color:#fff
              

GRASP 04: High Cohesion fondamentale

Ogni classe dovrebbe avere responsabilità strettamente correlate e focalizzate. Una classe con bassa coesione fa troppe cose diverse ed è difficile da capire, mantenere e riusare.

HC · Test della coesione

L'Alta Coesione si ottiene quando una classe gestisce un numero limitato di task (compiti), oppure esiste esclusivamente per assolvere a funzionalità molto specifiche e mirate. Invece di fare decine di operazioni slegate tra loro, una classe coesa si concentra solo su task strettamente connessi al suo scopo principale.

coesione
Esempio 1 — Gioco

Item sa solo come applicarsi a un Player: apply(player). Non gestisce sessioni, non salva nulla, non notifica nessuno. Una responsabilità.

Esempio 2 — Citylogic-Engine-BYZZ (MainView Tutto-fare vs MVC)

Una classe dovrebbe avere un unico scopo e responsabilità focalizzate. Mescolare l'aggiornamento della UI con i calcoli della logica di business distrugge la coesione.

Bassa Coesione (Problema)

La classe MainView disegna la grafica, cattura l'input e calcola direttamente le tasse. Fa troppe cose slegate tra loro.

flowchart LR
    M[MainView]:::bad
    M --> GUI[Disegna Grafica]
    M --> INPUT[Cattura Input]
    M --> CALC[Calcola Tasse]
    style GUI fill:#2b2b36,stroke:#444,color:#aaa
    style INPUT fill:#2b2b36,stroke:#444,color:#aaa
    style CALC fill:#2b2b36,stroke:#444,color:#aaa
              
Alta Coesione (Soluzione MVC)

I compiti vengono divisi: MainView disegna l'interfaccia, GameController gestisce l'input e Citta calcola le tasse.

flowchart LR
    V[MainView]:::good -->|Disegna| GUI[Grafica]
    C[GameController]:::good -->|Cattura| INPUT[Input]
    M[Citta]:::good -->|Calcola| TASSE[Tasse]
    
    style GUI fill:#2b2b36,stroke:#444,color:#aaa
    style INPUT fill:#2b2b36,stroke:#444,color:#aaa
    style TASSE fill:#2b2b36,stroke:#444,color:#aaa
              
⚠️ HC + LC vanno insieme: spezzare una classe può aumentare il coupling se non si usano interfacce. I due principi si bilanciano: alta coesione interna a ogni classe, basso accoppiamento tra le classi.

GRASP 05: Controller fondamentale

Gli eventi di sistema (azioni dell'utente, richieste esterne) devono essere ricevuti da un oggetto Controller che non è parte dell'UI. Il Controller coordina la risposta senza fare la logica di business da solo.

CT · Due tipi di Controller

Facade Controller: un unico oggetto per tutti gli eventi di sistema (es. GameController per tutti gli eventi del gioco). Adatto a sistemi più semplici.

Use Case Controller: un controller per ogni caso d'uso (es. LoginController, CheckoutController). Più scalabile per applicazioni grandi.

architettura
Esempio 1 — Gioco

GameController riceve move(direction), collectItem(itemId), endGame() dall'interfaccia e orchestra la logica: valida, aggiorna il modello, notifica gli observer. Non è UI.

Esempio 2 — Web app (MVC)

In un'app web, OrderController riceve la request HTTP e chiama orderService.placeOrder(dto). Non contiene logica di business — la delega al service layer. Non è una vista — restituisce dati al framework.

Esempio 3 — ATM

ATMController riceve gli eventi fisici (carta inserita, PIN digitato, importo selezionato) e coordina CardReader, PinValidator, CashDispenser. L'utente interagisce solo con lo schermo fisico, mai con questi oggetti direttamente.

Esempio 4 — Sistema ospedaliero

AppointmentController gestisce i casi d'uso "prenota visita" e "cancella appuntamento". Separa la UI (form web) dalla logica (controllo disponibilità, invio reminder) e dalla persistenza (repository).

Sbagliato

Un pulsante UI fa direttamente player.lives-- e scoreLabel.setText(...). La logica di gioco è nell'interfaccia grafica. Impossibile testare, riusare o cambiare la UI.

Corretto

Il pulsante chiama gameController.onPlayerHit(). Il controller aggiorna il modello, il modello notifica gli observer, la UI si aggiorna da sola. Separazione netta.

Citylogic-Engine-BYZZ: Flusso del GameController

Il GameController agisce da vero "direttore d'orchestra" (Pattern Controller GRASP). La Logica Core a sinistra è indipendente, e la View a destra gestisce solo l'input/output. Il Controller fa da smistatore bidirezionale.

flowchart LR
    subgraph Model["Core Citylogic"]
        direction TB
        C[Citta]:::good
        G[Griglia]:::good
        F[EntityFactory]:::good
    end
    
    GC[GameController]:::purple
    
    subgraph View["Azioni Utente (View)"]
        direction TB
        B1[Menu Tasse]:::grey
        B2[Click Costruzione]:::grey
        B3[Fine Giorno]:::grey
    end
    
    C <--> GC
    G <--> GC
    F <--> GC
    
    GC <--> B1
    GC <--> B2
    GC <--> B3
          

GRASP 06: Polymorphism fondamentale

Quando il comportamento varia in base al tipo, usa il polimorfismo invece di if/switch. Ogni tipo gestisce la propria variazione implementando un'interfaccia comune.

PL · Polimorfismo vs. if/switch

Un if (type == "HEALTH") ... else if (type == "WEAPON") ... deve essere aggiornato ogni volta che si aggiunge un tipo. Con il polimorfismo si aggiunge una nuova classe — il codice esistente non si tocca (Open/Closed Principle).

OOP
Esempio — Calcolo Tasse in Citylogic-Engine-BYZZ

La città deve calcolare il gettito fiscale di tutti gli edifici. Ogni tipo di edificio (Residenziale, Commerciale, Industriale) ha regole di tassazione completamente diverse. Invece di far calcolare alla Città le tasse chiedendo il "tipo" dell'edificio, deleghiamo il calcolo all'edificio stesso tramite il polimorfismo (interfaccia Tassabile).

Senza polimorfismo
if (edificio.getTipo().equals("Residenziale")) {
    return calcolaTassaResidenziale();
} else if (edificio.getTipo().equals("Commerciale")) {
    return calcolaTassaCommerciale();
}

Alto rischio di refusi (es. "residenziale" con la minuscola) e forte violazione dell'Open/Closed Principle: ogni nuovo edificio richiede di modificare questo if-else.

Con polimorfismo

edificio.calcolaTassa();
Ogni implementazione di Tassabile (o sottoclasse di Edificio) sa calcolare la propria tassa in autonomia. Aggiungere l'edificio Industriale non toccherà mai il codice esistente.

Tassabile.java — Polymorphism in Citylogic
public interface Tassabile {
  double calcolaTassa();
}

public class EdificioResidenziale implements Tassabile {
  private int numeroAbitanti;
  
  @Override
  public double calcolaTassa() { 
      return numeroAbitanti * 10.5; // Logica residenziale 
  }
}

public class EdificioCommerciale implements Tassabile {
  private double fatturato;
  
  @Override
  public double calcolaTassa() { 
      return fatturato * 0.22; // Logica commerciale 
  }
}

//  La Citta calcola il totale invocando l'interfaccia (Polimorfismo puro)
double totaleTasse = 0;
for (Tassabile t : edificiTassabili) {
  totaleTasse += t.calcolaTassa(); 
}

GRASP 07: Pure Fabrication fondamentale

Classe "artificiale" che non esiste nel dominio del problema, ma viene creata per rispettare Low Coupling e High Cohesion. Risolve problemi tecnici (salvataggi, logging, comunicazione) senza sporcare le classi di dominio.

PF · Quando inventare una classe

Quando nessuna classe del dominio è il posto giusto per una responsabilità tecnica (salvare su DB, inviare email, fare cache), si inventa una classe di servizio. Non deve avere senso nel dominio reale — deve avere senso nel design.

astrazione
Esempio — Salvataggio in Citylogic-Engine-BYZZ

La classe Simulatore gestisce il tempo e le regole del gioco. Se le assegnassimo anche il compito di salvare e caricare i file su disco, diventerebbe una "God Class" inquinata da logiche tecniche (File I/O, Json, Eccezioni di scrittura). Invece, inventiamo una classe "artificiale", il SaveManager, che si fa carico unicamente di questo compito.

Senza Pure Fabrication

Il Simulatore fa tutto da solo, inquinando il dominio del gioco con responsabilità tecniche.

flowchart TD
    S["Simulatore (God Class)
+ avanzaTempo()
+ aggiornaMeteo()
+ salvaPartita()
+ caricaPartita()"]:::bad
Con Pure Fabrication

Fabbrichiamo un SaveManager. Alta coesione per entrambi, e il Simulatore rimane "puro".

flowchart LR
    S[Simulatore]:::good -->|Delega| SM[SaveManager
Pure Fabricator]:::good

GRASP 08: Indirection

Introduce un intermediario per ridurre l'accoppiamento diretto tra due classi. L'intermediario assorbe le dipendenze e consente a ciascun lato di evolversi indipendentemente.

IN · Il valore dell'intermediario

"All problems in computer science can be solved by another level of indirection." — David Wheeler. L'indirezione è la base di pattern come Observer, Adapter, Proxy, Facade, e dell'intera architettura a layer.

disaccoppiamento
Esempio 1 — Gioco

IGameObserver è il livello di indirezione tra GameSystem e ScoreUI / SoundManager. I due lati non si conoscono, comunicano solo attraverso l'interfaccia.

Esempio 2 — Adapter

La tua app usa IEmailService. Oggi l'implementazione chiama SendGrid, domani Mailgun. L'adapter è l'intermediario che traduce l'interfaccia comune nell'API specifica del provider. L'app non cambia mai.

Esempio 3 — Message Broker

Un microservizio A pubblica un evento su Kafka. Il microservizio B lo consuma. Non si conoscono direttamente. Kafka è il livello di indirezione che li disaccoppia: A e B possono scalare, aggiornarsi e fallire indipendentemente.

Esempio 4 — DNS

Il DNS è indirezione pura: il client chiede "dove trovo google.com?" e riceve un IP. Se Google cambia server, aggiorna il DNS — tutti i client continuano a funzionare senza sapere nulla del cambio.

Dipendenza diretta

Simulatore chiama MainView.aggiorna(), AudioSystem.play(), e Stats.calcola() direttamente. Aggiungere un nuovo componente richiede di modificare il Simulatore.

Con indirezione

Simulatore chiama notifyObservers(). Chi vuole reagire agli eventi implementa CityObserver e si registra. Il Simulatore non sa chi lo ascolta.

Collegamento →  Indirection e Pure Fabrication spesso coincidono: l'intermediario che si introduce è spesso una Pure Fabrication (una classe che non esiste nel dominio, ma serve per disaccoppiare).

GRASP 09: Protected Variations fondamentale

Identifica i punti del sistema dove le variazioni sono prevedibili (algoritmi, formati, provider esterni, regole di business) e proteggi le parti stabili con interfacce. Le variazioni sono libere; il nucleo stabile non si tocca mai.

PV · Cosa è stabile, cosa cambia?

Il design migliore isola ciò che cambia da ciò che rimane stabile. Ciò che è stabile (interfaccia pubblica, contratto) va protetto. Ciò che cambia (implementazione, algoritmo, fornitore) va isolato dietro un'astrazione.

stabilità
Protected Variations in CityLogic (UML)
classDiagram
    class Simulatore
    class ITassabile {
        <<interface>>
        +calcolaTasse()
    }
    class Stadio
    class Ospedale
    class Casa
    
    Simulatore ..> ITassabile : Usa
    ITassabile <|.. Stadio : Implementa
    ITassabile <|.. Ospedale : Implementa
    ITassabile <|.. Casa : Implementa
    
    style Simulatore :::blue
    style ITassabile :::purple
    style Stadio :::grey
    style Ospedale :::grey
    style Casa :::grey
              
Non protetto

Il Simulatore calcola le tasse usando lunghi if/switch sui tipi di edificio. Aggiungere un nuovo edificio (es. Stadio) costringe a modificare il cuore del simulatore, rischiando di romperlo.

Protetto

Il Simulatore delega tutto chiamando edificio.calcolaTasse() sull'interfaccia ITassabile. Le variazioni fiscali sono isolate: il simulatore è protetto al 100%.

Tutti e 9 · Riepilogo

Tabella riassuntiva

Un colpo d'occhio su tutti i principi GRASP con la domanda chiave da porsi durante il design.

Principio Categoria Domanda chiave Anti-pattern che risolve
Information Expert responsabilità Chi ha i dati per fare questa cosa? Logica sparsa fuori dai dati
Creator responsabilità Chi aggrega / usa / conosce A? Creazione sparsa ovunque
Low Coupling architettura Dipendo da un'astrazione o da un concreto? Modifiche a cascata
High Cohesion architettura Posso descrivere questa classe senza "e"? God class, classi bloated
Controller architettura Chi riceve questo evento di sistema senza essere UI? Logica nell'interfaccia grafica
Polymorphism OOP Sto usando if/switch sul tipo? Posso sostituirlo? Switch sulle varianti di tipo
Pure Fabrication astrazione Nessuna classe di dominio è il posto giusto? Responsabilità tecniche nel dominio
Indirection architettura Posso aggiungere un intermediario tra questi due? Dipendenze dirette tra moduli
Protected Variations astrazione Cosa cambierà? Come proteggo il resto? Variazioni che si propagano
Regola pratica: Low Coupling e High Cohesion sono i due principi "madre" — tutti gli altri sono modi specifici per raggiungerli. Se il design li rispetta, quasi certamente rispetta anche gli altri sette.
Design Patterns

I 7 Pattern GoF (Gang of Four) in CityLogic

Oltre ai principi GRASP (che assegnano le responsabilità), l'architettura di un buon software poggia fortemente sui Design Patterns GoF. Questi sono 23 "modelli" standardizzati che offrono soluzioni eleganti e comprovate a problemi di design ricorrenti. Ne vedremo 7, applicati direttamente al nostro motore CityLogic.

GoF 01: Factory (Creazionale) struttura

Delega la logica complessa di creazione degli oggetti a una classe specifica (la Factory). Utile quando la classe esatta da istanziare è complessa o decisa a runtime.

Factory · Creazione Centralizzata

Invece di spargere costruttori new Ospedale() o new Stadio() in giro per il codice (creando un forte accoppiamento col costruttore esatto), si chiede a una Factory di fabbricare l'oggetto.

Non protetto (Costruttori Diretti)

Il Simulatore fa new Ospedale(). Se l'Ospedale in futuro richiede nuovi parametri nel costruttore (es. livello di efficienza), il Simulatore si rompe.

classDiagram
  class Simulatore
  class Ospedale
  class Stadio
  
  Simulatore ..> Ospedale : "new Ospedale()"
  Simulatore ..> Stadio : "new Stadio()"
  
  style Simulatore :::grey
  style Ospedale :::bad
  style Stadio :::bad
              
Protetto (Factory)

Il Simulatore chiede a EdificioFactory.crea("Ospedale"). La Factory si smazza la complessità del costruttore. Il Simulatore è disaccoppiato.

classDiagram
  class Simulatore
  class EdificioFactory {
    +creaEdificio(tipo) IEdificio
  }
  class Ospedale
  
  Simulatore ..> EdificioFactory : Chiama crea()
  EdificioFactory ..> Ospedale : Istanzia
  
  style Simulatore :::grey
  style EdificioFactory :::good
  style Ospedale :::grey
              

GoF 02: Singleton (Creazionale) base

Garantisce che esista una e una sola istanza di una classe in tutto il ciclo di vita del programma, fornendo un punto di accesso globale ad essa.

Singleton · L'Unico e Solo

Nel nostro gioco, il GameSystem o il Simulatore sono Singleton. Non ha alcun senso avere due motori di simulazione che modificano le stesse tasse o lo stesso inquinamento in parallelo. Ne serve uno solo.

Attenzione (Svantaggi)
  • È a tutti gli effetti una variabile globale mascherata.
  • Rende molto difficili gli Unit Test a causa dello stato condiviso.
  • In multi-threading rischia race conditions se non si usa l'Holder Idiom.
⚠️ Nei sistemi moderni spesso si preferisce la Dependency Injection.
Vantaggi
  • Unica istanza rigorosamente controllata (tramite costruttore privato).
  • Accesso globale facile con Simulatore.getInstance().
  • Lazy initialization (viene istanziato in RAM solo al primo utilizzo effettivo).
Simulatore.java (Thread-safe)
public class Simulatore {
  // Costruttore privato! Nessuno può fare new dall'esterno.
  private Simulatore() {
    System.out.println("Motore avviato.");
  }

  // Holder Class Idiom (100% Thread-safe e Lazy)
  private static class Holder {
    private static final Simulatore INSTANCE = new Simulatore();
  }

  // L'unico modo per ottenere l'istanza globale
  public static Simulatore getInstance() {
    return Holder.INSTANCE;
  }
}

GoF 03: Observer (Comportamentale) eventi

Definisce una dipendenza uno-a-molti in puro stile Pub/Sub (Iscrizione-Notifica). Quando il Subject (soggetto) cambia stato, tutti i suoi Observer (iscritti) vengono notificati in automatico.

Observer · Chiama non chiamare, ti chiamiamo noi

Il Simulatore (Subject) non sa nulla dell'interfaccia grafica. Quando l'inquinamento sale oltre i limiti legali, il Simulatore lancia semplicemente un evento notifica(). Le finestre della UI (Observer) che si erano registrate ascoltano l'evento e fanno comparire un pop-up rosso. Accoppiamento zero!

flowchart TB
  
  C[Citta]:::good -. "Implementa" .-> T[[TickObserver]]:::purple
  G[Griglia]:::good -. "Implementa" .-> T
  M[Meteo]:::good -. "Implementa" .-> T
  E[EventoCasuale]:::good -. "Implementa" .-> T
  
  T -- "usato da" --- S[Simulatore]:::grey 
          

GoF 04: Delegation (Design Base) composizione

Più che un pattern strutturato, è il principio cardine dell'Object-Oriented Programming moderno: "Favorire la composizione sull'ereditarietà". Invece di far fare tutto a una classe, si delega il lavoro a un helper interno.

Delegation · L'arte di far lavorare gli altri

In CityLogic, se il Simulatore dovesse calcolare il PIL, le Tasse, la Felicità e il Traffico, diventerebbe una God Class illeggibile di 10.000 righe. Invece, il Simulatore delega: ha un oggetto privato TaxManager al suo interno. Quando qualcuno chiede le tasse al Simulatore, lui di nascosto risponde return this.taxManager.calcola().

GoF 05: Adapter (Strutturale) integrazione

Traduce l'interfaccia di una classe in un'altra interfaccia attesa dal client. È il tipico "Glue Code" (codice colla) usato per integrare librerie esterne che non possiamo o vogliamo modificare.

Adapter · L'adattatore di prese internazionali

CityLogic ha bisogno di sapere che tempo fa. L'architettura esige che ogni provider meteo implementi IMeteoProvider.getTemperatura(). Compriamo una libreria meteo fantastica (una .dll esterna) che però usa un metodo proprietario MeteoPro.fetchCelsius(). Invece di sporcare CityLogic cambiando le chiamate, creiamo un MeteoAdapter che implementa IMeteoProvider e al suo interno nasconde la chiamata a MeteoPro!

CityLogic Meteo Adapter (UML)
classDiagram
  class Simulatore
  class IMeteoProvider {
    <<interface>>
    +getTemperatura()
  }
  class MeteoAdapter {
    -libreriaEsterna: MeteoPro
    +getTemperatura()
  }
  class MeteoPro {
    <<Terze Parti>>
    +fetchCelsius()
  }
  
  Simulatore --> IMeteoProvider : Usa la porta standard
  IMeteoProvider <|.. MeteoAdapter : Implementa
  MeteoAdapter --> MeteoPro : Traduce e chiama
  
  style Simulatore :::grey
  style IMeteoProvider :::purple
  style MeteoAdapter :::good
  style MeteoPro :::bad
              

GoF 06: Façade (Strutturale) architettura

Fornisce un'interfaccia unificata e semplificata per un intero sottosistema molto complesso, mascherando decine di classi sotto a un unico punto di accesso (una Facciata, appunto).

Façade · Il pulsante Magico

Quando in CityLogic clicchi "Salva Partita", sotto il cofano succedono mille cose: si estraggono i dati dalla Griglia, si passano a un JSONSerializer, poi si comprimono con ZipCompressor, poi si apre una DBConnection e si scrivono su disco. Costringere il pulsante della UI a chiamare queste 4 classi distruggerebbe il Low Coupling. Invece, creiamo un SaveSystemFacade con un solo metodo pubblico: salvaGioco(). La UI chiama quello, la facciata orchestra il caos.

GoF 07: Proxy (Strutturale) performance

Fornisce un "sostituto" o un segnaposto (placeholder) per controllare l'accesso a un altro oggetto. È il pattern sacro usato per il Lazy Loading (ritardare caricamenti pesanti in RAM).

Proxy · Lo Scudo di Protezione della RAM (Virtual Proxy)

In CityLogic abbiamo un menù per caricare le partite. Se per mostrare ogni slot il motore leggesse per intero il mastodontico file JSON del salvataggio (contenente tutti gli edifici, la griglia e il traffico), il gioco si bloccherebbe e saturerebbe la RAM solo per disegnare una lista!

Invece, usiamo un SaveGameProxy. All'apertura del menù, il Proxy legge dal JSON esclusivamente l'intestazione (Nome e Data) e mostra la lista all'istante. Solo quando decidi di avviare davvero quel salvataggio, il Proxy istanzia l'oggetto pesante SaveGameReale, cedendogli il controllo.

Menu Carica Partita (Proxy in azione)
Esempio pratico: I dati visibili in questo menù (SALVATAGGIO1, 28/06/2026) sono forniti dai Proxy leggeri in Lazy Loading.