Salta al contenuto
Note per Studenti Architettura del software - decisioni, organizzazione e stili

Architettura del software - decisioni, organizzazione e stili

In questa pagina 5

Nota sulle fonti. Il materiale del corso su questo argomento è ridotto (il programma lo elenca, le slide e la dispensa lo toccano solo di passaggio con MVC, publish-subscribe e client-server). La nota è quindi una sintesi concisa dei concetti standard di architettura del software, collegata agli esempi del corso e alle note sugli stessi stili in altre materie.

Definizione (architettura del software). L'organizzazione fondamentale di un sistema: i suoi componenti (sottosistemi, moduli), le relazioni tra loro e con l'ambiente, la loro distribuzione sulla rete e i principi che ne guidano il progetto e l'evoluzione.

Nel processo di sviluppo (Processi di sviluppo - modelli plan-based, metodi agili e ScrumUn processo software è un insieme strutturato di attività (specifica, progetto e realizzazione, validazione, evoluzione) e si descrive con attività, prodotti, ruoli e condizioni; i processi plan-driven pianificano tutto in anticipo e misurano il progresso sul piano (cascata, modello a V con i livelli di test corrispondenti, sviluppo incrementale pianificato, integrazione e configurazione di componenti, con prototipi e consegna incrementale per gestire il cambiamento, e miglioramento del processo con i livelli di maturità SEI), mentre i metodi agili pianificano in modo incrementale e accolgono il cambiamento (manifesto agile, XP con i suoi valori e le sue dodici pratiche, storie dell'utente e story point); Scrum organizza il lavoro in sprint con tre ruoli (Product Owner, Scrum Master, team), eventi (planning, daily scrum, review, retrospective) e artefatti (product backlog, sprint backlog, incremento), stimando con il planning poker; non c'è un processo giusto, ma priorità diverse (stabilità dei requisiti, criticità, dimensione e distribuzione del team).Processi di sviluppo - modelli plan-based, metodi agili e Scrum →) il progetto architetturale è la prima fase del progetto: collega i requisiti (Ingegneria dei requisiti - requisiti funzionali e non funzionaliUn requisito è la descrizione di un servizio o di un vincolo del sistema, scritta ad astrazione diversa per chi decide (requisiti utente, in linguaggio naturale) e per chi realizza (requisiti di sistema, strutturati e dettagliati); si dividono in funzionali (servizi e comportamento), non funzionali (proprietà e vincoli: prodotto, organizzativi, esterni, da scrivere con una misura verificabile) e di dominio; il processo di ingegneria dei requisiti è iterativo (elicitazione con interviste, etnografia, storie e scenari; analisi e negoziazione; specifica; validazione; gestione del cambiamento con identificatori e tracciabilità) e produce il documento dei requisiti, che dice cosa fa il sistema e non come; i modelli UML (contesto, casi d'uso, sequenza, classi, stati) aiutano a chiarire i requisiti.Ingegneria dei requisiti - requisiti funzionali e non funzionali →) al progetto dettagliato dei componenti, e i design pattern (Design pattern - introduzione e catalogo GoFUn design pattern è una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, con un nome (che arricchisce il vocabolario dei progettisti), un problema, una soluzione (struttura e partecipanti) e le conseguenze; il catalogo GoF (Gamma, Helm, Johnson, Vlissides) ha 23 pattern in tre famiglie: 5 creazionali (come si creano gli oggetti), 7 strutturali (come si compongono classi e oggetti), 11 comportamentali (come collaborano e si ripartiscono le responsabilità); dietro i pattern ci sono principi come programmare verso un'interfaccia, incapsulare ciò che varia, preferire la composizione all'ereditarietà, aperto/chiuso e inversione delle dipendenze; un pattern si sceglie dal problema, non dal nome, e usarlo senza bisogno complica il progetto.Design pattern - introduzione e catalogo GoF →) lavorano a un livello più basso, dentro i singoli componenti. Perché si fa un'architettura esplicita:

  • comunicazione tra gli stakeholder (è il modello più astratto da discutere);
  • analisi del sistema: le decisioni architetturali determinano prestazioni, affidabilità, manutenibilità;
  • riuso su larga scala: si riusano interi stili e sistemi, non solo classi.

Decisioni di progettazione architetturale

Non esiste una ricetta: l'architetto risponde a domande, in parte in cooperazione con i requisiti.

  • Esiste un'architettura di riferimento per questo tipo di applicazione? Come si distribuisce il sistema (un nodo, client e server, cloud)?
  • Quali stili si usano (vedi sotto)? Come si decompone in componenti e come questi comunicano (chiamate, eventi, dati condivisi)?
  • Quale strategia di controllo: centralizzato (un componente chiama gli altri) o basato su eventi?
  • Come si valuta l'architettura rispetto ai requisiti non funzionali, e come si documenta?

Le decisioni principali vengono dai requisiti non funzionali:

requisito scelta architetturale tipica
prestazioni componenti critici grandi e vicini (poche comunicazioni), dati locali, replica
sicurezza struttura a livelli con i dati critici nei livelli più interni, controlli ad ogni confine
sicurezza di funzionamento (safety) funzioni critiche in pochi componenti piccoli e verificabili
disponibilità componenti ridondanti, sostituzione senza fermare il sistema
manutenibilità componenti piccoli, autonomi, con interfacce stabili; niente dati condivisi in modo implicito

Le richieste sono in conflitto: componenti grandi e vicini aiutano le prestazioni ma peggiorano la manutenibilità; più livelli aiutano la sicurezza ma rallentano. L'architetto sceglie un compromesso e lo documenta.

Viste

Un solo diagramma non basta. Il modello "4+1" propone:

Organizzazione del sistema: stili architetturali

Definizione (stile architetturale). Uno schema ricorrente di organizzazione di un sistema, con tipi di componenti, tipi di collegamenti tra componenti e vincoli sul loro uso. È ai componenti ciò che un design pattern è alle classi, ma a scala maggiore.

stile componenti e relazioni quando conviene svantaggi
a livelli (layered) pile di livelli: ognuno usa solo il livello sotto (presentazione, logica applicativa, dati) sviluppo per strati e per gruppi diversi, sostituibilità dei livelli prestazioni (ogni richiesta attraversa i livelli); separazione netta non sempre possibile
MVC (model-view-controller) model (dati e logica), view (presentazione), controller (riceve gli eventi e li smista) interfacce con più viste degli stessi dati, web (JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →) complessità per applicazioni semplici
repository molti componenti che comunicano solo tramite un archivio dati condiviso grandi quantità di dati, strumenti diversi sugli stessi dati (database, IDE) il repository è collo di bottiglia e punto unico di guasto
client-server server che offrono servizi, client che li richiedono tramite rete servizi condivisi, dati centralizzati (Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →) il server è punto di guasto; prestazioni dipendono dalla rete
pipe and filter filtri indipendenti che trasformano un flusso di dati, collegati da pipe elaborazioni di dati in sequenza (compilatori, elaborazione di file) formato di scambio comune; poco adatto a sistemi interattivi
publish-subscribe (a eventi) i componenti pubblicano eventi, altri si iscrivono; non si conoscono sistemi distribuiti, interfacce, integrazione (Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribeIl modello ISO/OSI (7 livelli) e la pila TCP/IP (4 livelli) dividono la comunicazione in strati; ogni livello riceve una SDU dal livello superiore, aggiunge la sua intestazione e produce una PDU; il confine fra due livelli adiacenti e' il SAP, identificato da un indirizzo (EtherType per Ethernet, numero di protocollo per IP, numero di porta per TCP e UDP) e per un programma C il SAP del trasporto e' l'API delle socket; il data plane inoltra i pacchetti seguendo le tabelle, il control plane le costruisce con i protocolli di instradamento; client/server funziona come una chiamata di funzione (richiesta e risposta), peer-to-peer mette tutti i nodi alla pari, publish/subscribe funziona come un interrupt: un broker inoltra gli eventi agli iscritti, con disaccoppiamento nello spazio, nel tempo e nella sincronizzazione.Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribe →) ordine e consegna degli eventi difficili da prevedere
microservizi tanti servizi piccoli e autonomi, ciascuno con i suoi dati, che comunicano in rete (HTTP o messaggi, vedi Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →) sistemi grandi, scalabilità e rilascio indipendenti per componente complessità di rete e di gestione dei dati distribuiti

L'esempio a livelli è nel programma sotto (biblioteca): la presentazione conosce solo il servizio, il servizio conosce solo l'interfaccia CatalogoRepository, e l'implementazione dei dati (CatalogoInMemoria) si può sostituire senza toccare il resto. L'unico punto che conosce le classi concrete è main. Questa è l'inversione delle dipendenze (si dipende dalle interfacce, non dalle classi concrete, vedi Design pattern - introduzione e catalogo GoFUn design pattern è una soluzione generale, riusabile e collaudata a un problema ricorrente di progettazione in un certo contesto, con un nome (che arricchisce il vocabolario dei progettisti), un problema, una soluzione (struttura e partecipanti) e le conseguenze; il catalogo GoF (Gamma, Helm, Johnson, Vlissides) ha 23 pattern in tre famiglie: 5 creazionali (come si creano gli oggetti), 7 strutturali (come si compongono classi e oggetti), 11 comportamentali (come collaborano e si ripartiscono le responsabilità); dietro i pattern ci sono principi come programmare verso un'interfaccia, incapsulare ciò che varia, preferire la composizione all'ereditarietà, aperto/chiuso e inversione delle dipendenze; un pattern si sceglie dal problema, non dal nome, e usarlo senza bisogno complica il progetto.Design pattern - introduzione e catalogo GoF →) applicata a livello di architettura.

java
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/* ===== Livello DOMINIO (dipende da nessuno) ===== */
class Documento {
    final String titolo; int copie;
    Documento(String titolo, int copie) { this.titolo = titolo; this.copie = copie; }
}

/* ===== Livello DATI: l'interfaccia sta SOPRA, l'implementazione sotto (Dependency Inversion) ===== */
interface CatalogoRepository {
    Optional<Documento> trova(String titolo);
    List<Documento> tutti();
}

class CatalogoInMemoria implements CatalogoRepository {      // domani: CatalogoSql, senza toccare il resto
    private final List<Documento> dati = new ArrayList<>(List.of(new Documento("rosa", 1), new Documento("pendolo", 0)));
    public Optional<Documento> trova(String titolo) { return dati.stream().filter(d -> d.titolo.equals(titolo)).findFirst(); }
    public List<Documento> tutti() { return dati; }
}

/* ===== Livello LOGICA DI BUSINESS: usa solo l'interfaccia del livello sotto ===== */
class PrestitoService {
    private final CatalogoRepository repo;
    PrestitoService(CatalogoRepository repo) { this.repo = repo; }

    String presta(String titolo) {
        Optional<Documento> d = repo.trova(titolo);
        if (d.isEmpty()) return "sconosciuto";
        if (d.get().copie == 0) return "non disponibile";
        d.get().copie--;
        return "ok";
    }
}

/* ===== Livello PRESENTAZIONE: parla solo con il livello sotto ===== */
class Console {
    private final PrestitoService servizio;
    Console(PrestitoService servizio) { this.servizio = servizio; }
    void comando(String riga) {
        String[] p = riga.split(" ");
        if (p[0].equals("presta")) System.out.println("> " + riga + " -> " + servizio.presta(p[1]));
        else System.out.println("> " + riga + " -> comando non valido");
    }
}

public class Livelli {
    public static void main(String[] args) {
        // l'unico punto che conosce le classi concrete di tutti i livelli (composizione dell'applicazione)
        Console c = new Console(new PrestitoService(new CatalogoInMemoria()));
        c.comando("presta rosa");
        c.comando("presta rosa");
        c.comando("presta pendolo");
        c.comando("presta xyz");
        c.comando("elimina rosa");
    }
}
> presta rosa -> ok
> presta rosa -> non disponibile
> presta pendolo -> non disponibile
> presta xyz -> sconosciuto
> elimina rosa -> comando non valido

Per lo stile pipe and filter, ogni filtro del programma ha un solo compito (scartare le righe inutili, controllarne il formato, calcolare la multa) e conosce solo il formato dei propri ingressi e uscite; la catena di stream() è la pipe.

java
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/** Pipe and filter: ogni filtro e' indipendente e conosce solo il formato dei dati in ingresso e uscita. */
public class PipeFiltri {
    // formato: "utente;titolo;giorniRitardo"
    static final List<String> ingresso = List.of(
            "anna;rosa;0", "luca;pendolo;12", "anna;baudolino;3", "", "# commento", "piero;rosa;x", "luca;rosa;40");

    record Riga(String utente, String titolo, int ritardo) { }

    // filtri
    static boolean utile(String s) { return !s.isBlank() && !s.startsWith("#"); }           // scarta righe vuote e commenti
    static Riga analizza(String s) { String[] p = s.split(";"); return new Riga(p[0], p[1], Integer.parseInt(p[2])); }
    static boolean inRitardo(Riga r) { return r.ritardo() > 0; }
    static String multa(Riga r) { return String.format(java.util.Locale.ROOT, "%s: %.2f euro (%s)", r.utente(), Math.min(0.10 * r.ritardo(), 3.0), r.titolo()); }

    public static void main(String[] args) {
        // la "pipe" e' la catena di stage dello stream
        List<String> uscita = ingresso.stream()
                .filter(PipeFiltri::utile)
                .filter(s -> s.split(";")[2].matches("\\d+"))                               // scarta le righe malformate
                .map(PipeFiltri::analizza)
                .filter(PipeFiltri::inRitardo)
                .map(PipeFiltri::multa)
                .collect(Collectors.toList());
        uscita.forEach(System.out::println);

        // lo stesso concetto, composto esplicitamente: filtri come funzioni
        Function<String, String> maiuscole = String::toUpperCase;
        Function<String, String> senzaSpazi = s -> s.replace(" ", "");
        System.out.println(maiuscole.andThen(senzaSpazi).apply("il nome della rosa"));
    }
}
luca: 1.20 euro (pendolo)
anna: 0.30 euro (baudolino)
luca: 3.00 euro (rosa)
ILNOMEDELLAROSA

Quasi mai un sistema reale usa un solo stile: un'applicazione web è a livelli (browser, server, database), il livello server può usare MVC, le componenti di notifica publish-subscribe, e l'integrazione con altri sistemi client-server o REST.

Stili di modularizzazione

La modularizzazione è la scomposizione in parti con confini ben definiti. Principi:

Modi di scomporre un sistema:

In Java i moduli si realizzano con i pacchetti (package, visibilità package-private), i moduli della piattaforma (module-info.java, che dichiara quali pacchetti si esportano) e i file jar; gli strumenti di build gestiscono le dipendenze tra moduli (Strumenti di sviluppo - git, Maven e DockerTre strumenti rendono un progetto web riproducibile e condivisibile. Git tiene la storia del codice: tre aree (cartella di lavoro, area di staging, repository), il commit è un'istantanea con un hash, il branch è un puntatore mobile a un commit, HEAD indica dove si è; il merge fonde due rami (fast-forward se non c'è divergenza, altrimenti un commit di merge) e può produrre conflitti, che si risolvono a mano nei marcatori di conflitto, poi add e commit; con un remoto si usano clone, fetch, pull (fetch + merge) e push, e un push rifiutato (fetch first) richiede prima un pull; in gruppo si lavora su rami di funzionalità con commit piccoli e main sempre funzionante. Maven descrive il progetto nel pom.xml (coordinate groupId:artifactId:version, packaging war, dipendenze con scope compile, provided, runtime, test), impone la struttura src/main/java, src/main/webapp, src/test, e il ciclo di vita validate, compile, test, package, verify, install, deploy: ogni fase esegue prima tutte le precedenti, mvn clean package produce target/aule.war. Docker confeziona l'applicazione: un'immagine (strati di sola lettura, costruita da un Dockerfile) è il modello, un container è un'istanza in esecuzione; per l'esempio un Dockerfile a due fasi compila il war con Maven e lo copia in Tomcat, e docker compose avvia insieme Tomcat e PostgreSQL con un volume per i dati.Strumenti di sviluppo - git, Maven e Docker →).

Strategie di controllo

Dall'architettura ai pattern

I pattern GoF realizzano nel piccolo le stesse idee: Facade è il confine di un sottosistema, Observer è il publish-subscribe a livello di oggetti, Proxy e Adapter nascondono componenti remoti o incompatibili, Abstract Factory sceglie una famiglia di implementazioni in un punto solo. Il Singleton serve per un componente unico (configurazione, pool di connessioni), da trattare con attenzione nelle applicazioni concorrenti.

Domande d'esame

1. Un sistema deve rispondere in meno di 100 ms con migliaia di richieste al secondo ed essere facile da modificare. Quali conflitti ci sono e come si risolvono? Traccia. Le prestazioni spingono verso pochi componenti grandi e vicini, dati locali e replicati; la manutenibilità verso componenti piccoli e autonomi con interfacce stabili. Compromesso: stile a livelli con pochi livelli, cache dietro l'interfaccia del repository, confini dei moduli sulle parti che cambiano meno; si misura l'effetto con test di prestazione e si documenta la scelta.

2. Confrontare lo stile a livelli e il publish-subscribe: componenti, accoppiamento, quando usarli. Traccia. Livelli: ogni livello usa solo quello sotto tramite interfaccia, accoppiamento verticale, adatto a separare presentazione, logica e dati. Publish-subscribe: i componenti comunicano tramite eventi senza conoscersi (accoppiamento minimo), adatto a notifiche e sistemi distribuiti, ma con flusso di controllo meno prevedibile e consegna da garantire.

3. Che cosa significa "dipendere dalle astrazioni" in un'architettura a livelli? Fare un esempio in Java. Traccia. Il livello superiore usa un'interfaccia (CatalogoRepository) e non la classe concreta (CatalogoInMemoria); l'implementazione si fornisce dall'esterno (in main, o con un contenitore di dependency injection). Cambiare la sorgente dati non richiede di modificare né ricompilare i livelli superiori.

Versione ripasso

  • Architettura: componenti (sottosistemi, moduli), relazioni, distribuzione, principi guida. È il passaggio tra requisiti e progetto dettagliato; i pattern GoF lavorano dentro i componenti. Serve per comunicare, analizzare (prestazioni, affidabilità, manutenibilità) e riusare su larga scala. Fonti del corso scarse: sintesi di concetti standard.
  • Decisioni: architettura di riferimento? distribuzione? stili? decomposizione e comunicazione? controllo centralizzato o a eventi? valutazione e documentazione?
requisito scelta
prestazioni componenti critici grandi e vicini, dati locali
sicurezza livelli, dati critici all'interno, controlli ai confini
safety funzioni critiche in pochi componenti piccoli
disponibilità ridondanza
manutenibilità componenti piccoli e autonomi, interfacce stabili

I requisiti entrano in conflitto (prestazioni contro manutenibilità): si sceglie un compromesso e lo si documenta.

java
interface CatalogoRepository { Optional<Documento> trova(String titolo); List<Documento> tutti(); }
class PrestitoService {
    private final CatalogoRepository repo;
    PrestitoService(CatalogoRepository repo) { this.repo = repo; }   // dipende dall'interfaccia
}
Console c = new Console(new PrestitoService(new CatalogoInMemoria()));   // l'unico punto che sceglie

Teoria collegata