4. [TD]: Architetture a livelli
Parole chiave: architettura multistrato, Spring, iniezione di dipendenze.
4.1. Introduzione
Rivediamo ciò che abbiamo fatto:
- Nella Parte 1 dell'esercizio ELECTIONS non sono state utilizzate classi. Abbiamo realizzato una soluzione come l'avremmo realizzata in linguaggio C.
- Nella Parte 2 dell'esercizio sono state introdotte due classi:
- [VoterList], che rappresenta gli attributi (id, nome, voti, seggi, eliminato) di una lista di candidati
- [ElectionsException], una classe per le eccezioni non gestite. Questo tipo di eccezione viene utilizzato ogni volta che si verifica un errore fatale nell'applicazione elettorale. Non viene gestita, il che significa che lo sviluppatore non è tenuto a gestirla con un blocco try-catch.
Fino ad ora, il calcolo dei risultati elettorali è stato gestito da un metodo [main] della classe [MainElections]
La soluzione precedente comprende tre fasi standard:
- acquisizione dei dati, righe 17-18
- calcolo della soluzione, righe 19–20
- visualizzazione e/o salvataggio dei risultati, righe 21–22
Solo la fase 2 è realmente costante. La fase 1 può variare: i dati possono provenire dalla tastiera come negli esempi studiati, da un file di testo, da un'interfaccia grafica, da un database, dalla rete, ... Allo stesso modo, ci sono diversi modi per visualizzare i risultati nella fase 3: visualizzarli sullo schermo come negli esempi studiati, salvarli in un file, in un database, inviarli in rete, ecc.
Più in generale, un'applicazione può spesso essere modellata come tre livelli, ciascuno con un ruolo ben definito:
![]() |
Questa architettura è anche denominata "architettura a tre livelli". Il termine "a tre livelli" si riferisce solitamente a un'architettura in cui ogni livello risiede su una macchina diversa. Quando i livelli si trovano sulla stessa macchina, l'architettura diventa un'architettura "a tre strati".
- Il livello [business] contiene le regole di business dell'applicazione. Per la nostra applicazione elettorale, si tratta delle regole che calcolano i seggi conquistati dalle diverse liste una volta noti i voti ottenuti da ciascuna. Questo livello richiede dati per funzionare. Ad esempio, nell'applicazione elettorale:
- le liste, ciascuna con il proprio nome e il numero di voti
- il numero di seggi da assegnare
- la soglia elettorale al di sotto della quale una lista viene eliminata
Nel diagramma sopra, i dati possono provenire da due fonti:
- il livello di accesso ai dati o [DAO] (DAO = Data Access Object) per i dati già memorizzati in file o database. Questo potrebbe essere il caso, in questo contesto, dei nomi delle liste, del numero di seggi da assegnare e della soglia elettorale. Infatti, queste informazioni sono note prima delle elezioni stesse.
- il livello dell'interfaccia utente o [ui] (UI = User Interface) per i dati inseriti dall'utente o visualizzati all'utente. Questo potrebbe essere il caso, in questo contesto, dei voti per le liste, che sono noti solo all'ultimo momento, così come per la visualizzazione dei risultati elettorali.
- In generale, il livello [DAO] gestisce l'accesso ai dati persistenti (file, database) o ai dati non persistenti (rete, sensori, ecc.).
- Il livello [UI], d'altra parte, gestisce le interazioni con l'utente, se presente.
- I tre livelli sono resi indipendenti attraverso l'uso di interfacce Java.
- Esistono vari metodi per integrare questi livelli nell'applicazione. Useremo uno strumento chiamato "Spring". Nel diagramma, esso attraversa gli altri livelli.
Riprenderemo l'applicazione [Elections] sviluppata in precedenza per dotarla di un'architettura a tre livelli. Per farlo, esamineremo i livelli [UI, Business, DAO] uno per uno, iniziando dal livello [DAO], che gestisce i dati persistenti.
Per prima cosa, dobbiamo definire le interfacce per i diversi livelli dell'applicazione [Elections].
4.2. Le interfacce dell'applicazione [Elections]
Ricordiamo che un'interfaccia definisce un insieme di firme di metodi. Le classi che implementano l'interfaccia forniscono l'implementazione di questi metodi.
Torniamo all'architettura a 3 livelli della nostra applicazione:
![]() |
In questo tipo di architettura, spesso è l'utente a prendere l'iniziativa. Egli effettua una richiesta al punto [1] e riceve una risposta al punto [8]. Questo processo è chiamato ciclo richiesta-risposta. Prendiamo l'esempio del calcolo dei seggi conquistati nella notte delle elezioni. Ciò richiederà diversi passaggi:
- Il livello [ui] dovrà chiedere all'utente il numero di voti ricevuti da ciascuna delle liste. Per farlo, dovrà mostrare all'utente i nomi delle liste in competizione. L'utente dovrà quindi semplicemente inserire il numero di voti accanto a ciascuna lista e richiedere il calcolo dei seggi.
- Il livello [ui] non dispone dei nomi delle liste. Questi sono memorizzati nella fonte dati a destra del diagramma. Utilizzerà il percorso [2, 3, 4, 5, 6, 7] per recuperarli. L'operazione [2] è la richiesta delle liste, mentre l'operazione [7] è la risposta a tale richiesta. Una volta fatto ciò, potrà presentarli all'utente tramite [8].
- L'utente trasmetterà al livello [ui] il numero di voti ottenuti da ciascuna lista. Questa è l'operazione [1] sopra indicata. Durante questa fase, l'utente interagisce solo con il livello [ui]. È questo livello che verificherà la validità dei dati inseriti. Una volta fatto ciò, l'utente richiederà l'elenco dei seggi ottenuti da ciascuna lista.
- Il livello [ui] chiederà al livello business di calcolare i seggi. Per farlo, invierà i dati ricevuti dall'utente al livello business. Questa è l'operazione [2].
- Il livello [business] necessita di determinate informazioni per svolgere il proprio compito. Dispone già delle liste provenienti dall'operazione (b). Ha inoltre bisogno del numero di seggi da assegnare e del valore della soglia elettorale. Richiederà queste informazioni al livello [DAO] tramite il percorso [3, 4, 5, 6]. [3] è la richiesta iniziale e [6] è la risposta a tale richiesta.
- Con tutti i dati necessari, il livello [business] calcola i seggi vinti da ciascuna lista.
- Il livello [business] può ora rispondere alla richiesta del livello [ui] effettuata in (d). Questo è il percorso [7].
- Il livello [UI] formatterà questi risultati per presentarli all'utente in una forma appropriata e poi li visualizzerà. Questo è il percorso [8].
- Si può immaginare che questi risultati debbano essere memorizzati in un file o in un database. Ciò può essere fatto automaticamente. In questo caso, dopo l'operazione (f), il livello [business] chiederà al livello [DAO] di salvare i risultati. Questo sarà il percorso [3, 4, 5, 6]. Ciò può essere fatto anche solo su richiesta dell'utente. Il percorso [1-8] sarà utilizzato dal ciclo richiesta-risposta.
Da questa descrizione si può vedere che un livello utilizza le risorse del livello alla sua destra, mai quelle del livello alla sua sinistra. Consideriamo due livelli contigui:
![]() |
Il livello [A] invia richieste al livello [B]. Nei casi più semplici, un livello è implementato da una singola classe. Un'applicazione si evolve nel tempo. Pertanto, il livello [B] può avere diverse classi di implementazione [B1, B2, ...]. Se il livello [B] è il livello [DAO], può avere un'implementazione iniziale [B1] che recupera i dati da un file. Qualche anno dopo, potremmo voler memorizzare i dati in un database. Costruiremo quindi una seconda classe di implementazione [B2]. Se, nell'applicazione iniziale, il livello [A] funzionava direttamente con la classe [B1], siamo costretti a riscrivere parzialmente il codice per il livello [A]. Supponiamo, ad esempio, di aver scritto qualcosa di simile a quanto segue nel livello [A]:
- riga 1: viene creata un'istanza della classe [B1]
- riga 3: vengono richiesti i dati da questa istanza
Se ipotizziamo che la nuova classe di implementazione [B2] utilizzi metodi con la stessa firma di quelli della classe [B1], dovremo sostituire tutti i riferimenti a [B1] con [B2]. Si tratta di uno scenario molto favorevole e piuttosto improbabile se non abbiamo prestato attenzione a queste firme dei metodi. In pratica, è comune che le classi [B1] e [B2] abbiano firme di metodo diverse, il che significa che una parte significativa del livello [A] deve essere completamente riscritta.
Possiamo migliorare la situazione introducendo un'interfaccia tra i livelli [A] e [B]. Ciò significa che definiamo le firme dei metodi presentate dal livello [B] al livello [A] in un'interfaccia. Il diagramma precedente diventa quindi il seguente:
![]() |
Il livello [A] non comunica più direttamente con il livello [B], ma con la sua interfaccia [IB]. Pertanto, nel codice del livello [A], la classe di implementazione [Bi] del livello [B] compare una sola volta, al momento dell'implementazione dell'interfaccia [IB]. Una volta fatto ciò, nel codice viene utilizzata l'interfaccia [IB] e non la sua classe di implementazione. Il codice precedente diventa il seguente:
- riga 1: viene creata un'istanza [ib] che implementa l'interfaccia [IB] istanziando la classe [B1]
- riga 3: vengono richiesti i dati dall'istanza [ib]
Ora, se sostituiamo l'implementazione [B1] del livello [B] con un'implementazione [B2], e entrambe le implementazioni aderiscono alla stessa interfaccia [IB], allora è necessario modificare solo la riga 1 del livello [A], e nessun'altra riga. Questo è un vantaggio fondamentale che da solo giustifica l'uso sistematico delle interfacce tra due livelli.
Possiamo spingerci ancora oltre e rendere il livello [A] completamente indipendente dal livello [B]. Nel codice sopra riportato, la riga 1 pone un problema perché codifica in modo rigido un riferimento alla classe [B1]. Idealmente, il livello [A] dovrebbe essere in grado di utilizzare un'implementazione dell'interfaccia [IB] senza dover specificare il nome di una classe. Ciò sarebbe coerente con il nostro diagramma sopra riportato. Possiamo vedere che il livello [A] interagisce con l'interfaccia [IB] e non c'è motivo per cui debba conoscere il nome della classe che implementa questa interfaccia. Questo dettaglio non è utile al livello [A].
Il framework Spring (http://www.springframework.org) ci permette di ottenere questo risultato. L'architettura precedente si evolve come segue:
![]() |
Il livello trasversale [Spring] consentirà a un livello di ottenere, tramite configurazione, un riferimento al livello alla sua destra senza bisogno di conoscere il nome della classe che implementa quel livello. Questo nome sarà nei file di configurazione e non nel codice Java. Il codice Java per il livello [A] assumerà quindi la seguente forma:
- riga 1: un'istanza [ib] che implementa l'interfaccia [IB] del livello [B]. Questa istanza viene creata da Spring sulla base delle informazioni presenti in un file di configurazione. Spring si occuperà di creare:
- l'istanza [b] che implementa il livello [B]
- l'istanza [a] che implementa il livello [A]. Questa istanza verrà inizializzata. Al campo [ib] sopra indicato verrà assegnato il riferimento [b] dell'oggetto che implementa il livello [B]
- riga 3: i dati vengono richiesti all'istanza [ib]
Ora possiamo vedere che la classe di implementazione [B1] del livello B non compare in nessuna parte del codice del livello [A]. Quando l'implementazione [B1] viene sostituita da una nuova implementazione [B2], nulla cambierà nel codice della classe [A]. Modificheremo semplicemente i file di configurazione di Spring per istanziare [B2] invece di [B1].
La combinazione di Spring e delle interfacce Java apporta un miglioramento decisivo alla manutenzione dell'applicazione, rendendo gli strati dell'applicazione strettamente accoppiati tra loro. Questa è la soluzione che useremo per l'applicazione [Elections].
Torniamo all'architettura a tre livelli della nostra applicazione:
![]() |
In casi semplici, possiamo partire dal livello [business] per individuare le interfacce dell’applicazione. Per funzionare, essa necessita di dati:
- già disponibili in file, database o tramite la rete. Questi dati sono forniti dal livello [DAO].
- non ancora disponibili. Vengono quindi forniti dal livello [UI], che li ottiene dall’utente dell’applicazione.
Quale interfaccia dovrebbe fornire il livello [DAO] al livello [business]? Quali interazioni sono possibili tra questi due livelli? Il livello [DAO] deve fornire i seguenti dati al livello [business]:
- il numero di seggi da assegnare
- la soglia elettorale al di sotto della quale una lista viene eliminata
- i nomi delle liste
Queste informazioni sono note prima delle elezioni e possono quindi essere memorizzate. Nella direzione [business] -> [DAO], il livello [business] può chiedere al livello [DAO] di registrare i risultati elettorali, compreso il numero di seggi conquistati dalle varie liste.
Con queste informazioni, potremmo tentare una definizione iniziale dell'interfaccia del livello [DAO]:
public interface IElectionsDao {
public double getSeuilElectoral();
public int getNbSiegesAPourvoir();
public ListeElectorale[] getListesElectorales();
public void setListesElectorales(ListeElectorale[] listesElectorales);
}
- Riga 1: L'interfaccia si chiama [IElectionsDao]. Definisce quattro metodi:
- tre metodi per leggere i dati dall'origine dati: [getVotingThreshold, getNumberOfSeatsToBeFilled, getVoterLists]. Questi tre metodi consentiranno al livello [business] di ottenere i dati che caratterizzano l'elezione in corso.
- un metodo per scrivere dati nell'origine dati: [setVoterLists]. Questo metodo consentirà al livello [business] di richiedere la registrazione dei risultati che ha calcolato.
Torniamo all'architettura a tre livelli della nostra applicazione:
![]() |
Quale interfaccia dovrebbe presentare il livello [business] al livello [ui]? Esaminiamo le possibili interazioni tra questi due livelli.
- Il livello [ui] avrà il compito di chiedere all’utente di votare le varie liste in competizione. Per farlo, deve conoscere il numero di liste. Può richiedere questa informazione al livello [business], che a sua volta può richiedere la tabella delle liste in competizione al livello [dao]. Se il livello [business] dispone di questa tabella, potrebbe anche trasferirla al livello [ui]. Il livello [UI] disporrà quindi dei nomi delle liste e potrà perfezionare i messaggi all'utente chiedendo, ad esempio, "Numero di voti per la Lista A".
- Una volta che il livello [UI] ha ottenuto i voti per tutte le liste, richiederà il calcolo dei seggi al livello [business]. Il livello [business] può eseguire questo calcolo e restituire il risultato al livello [UI].
- Il livello [ui] può quindi presentare questi risultati all'utente. L'utente può anche richiedere che vengano salvati.
- Il livello [ui] potrebbe anche voler presentare all’utente informazioni aggiuntive, come la soglia elettorale o il numero di seggi da assegnare.
Con queste informazioni, potremmo tentare una definizione iniziale dell'interfaccia per il livello [ metier]:
public interface IElectionsMetier {
public ListeElectorale[] getListesElectorales();
public int getNbSiegesAPourvoir();
public double getSeuilElectoral();
public void recordResultats(ListeElectorale[] listesElectorales);
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
- Riga 1: L'interfaccia si chiama [IElectionsMetier]. Definisce i seguenti metodi:
- riga 3: un metodo [getVoterLists] che consentirà al livello [ui] di ottenere l'array delle liste in competizione;
- riga 5: il metodo [getNbSiegesAPourvoir] recupera il numero di seggi da assegnare;
- riga 7: il metodo [getElectoralThreshold] recupera la soglia elettorale;
- riga 11: un metodo [calculateSeats] (riga 36) che permetterà al livello [ui] di richiedere il calcolo dei seggi una volta noti i conteggi dei voti per le varie liste. Il parametro è l'array delle liste in lizza, senza i loro seggi e senza il booleano "eliminated". Il risultato restituito è lo stesso array, ma questa volta con i campi [seats, eliminated] inizializzati;
- Riga 9: un metodo [recordResults] che permetterà al livello [ui] di richiedere la registrazione dei risultati.
Nota: data la sua posizione, il livello [business] riutilizza alcuni dei metodi del livello [DAO] per renderli disponibili al livello [UI]. A causa di questa ridondanza, si potrebbe essere tentati di raggruppare tutto in un unico livello che combini sia la logica di business che l'accesso ai dati. Questo unico livello è talvolta chiamato modello, la M nell'acronimo MVC (Model-View-Controller). MVC è un modello di progettazione ampiamente utilizzato nelle applicazioni web.
Esaminiamo la firma del metodo [calculateSeats]:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
Come affermato in precedenza: «Il parametro è l’array delle liste in competizione, senza i loro seggi e senza il booleano ‘eliminated’. Il risultato è lo stesso array, ma questa volta con i campi [seats, eliminated]». La firma del metodo potrebbe anche essere la seguente:
public void calculerSieges(ListeElectorale[] listesElectorales);
Il parametro [voterLists] è un riferimento a un oggetto, in questo caso un array. Ogni elemento è a sua volta un riferimento a un oggetto, in questo caso di tipo [VoterList]. Il metodo [calculateSeats] modificherà i campi [seats, eliminated] di ciascuno di questi oggetti. Il metodo chiamante contiene un puntatore [voterLists] che:
- prima della chiamata, è un riferimento a un array di oggetti [VoterList] con i campi [seats, eliminated] non inizializzati;
- dopo la chiamata, è il riferimento (lo stesso) a un array di oggetti [VoterList] con i campi [seats, eliminated] inizializzati;
Allora perché utilizzare la firma:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
Quando si scrive un'interfaccia, è importante ricordare che può essere utilizzata in due contesti diversi: locale e remoto . Nel contesto locale, il metodo chiamante e il metodo chiamato vengono eseguiti nella stessa JVM (Java Virtual Machine):
![]() |
Se il livello [ui] chiama il metodo calculateSeats del livello [DAO], ha effettivamente un riferimento al parametro [VoterList[] voterLists] che passa al metodo.
Nel contesto remoto, il metodo chiamante e il metodo chiamato vengono eseguiti in JVM diverse:
![]() |
Nell'esempio sopra riportato, il livello [ui] viene eseguito nella JVM 1 e il livello [business] nella JVM 2 su due macchine diverse. I due livelli non comunicano direttamente. Tra di essi si trova un livello che chiameremo livello di comunicazione [1]. Questo è costituito da un livello di trasmissione [2] e da un livello di ricezione [3]. Generalmente lo sviluppatore non deve scrivere questi livelli di comunicazione. Essi vengono generati automaticamente da strumenti software. Il livello [business] è scritto come se fosse in esecuzione nella stessa JVM del livello [DAO]. Pertanto, non sono necessarie modifiche al codice.
Il meccanismo di comunicazione tra il livello [ui] e il livello [business] è il seguente:
- il livello [ui] chiama il metodo calculateSeats del livello [business], passando il parametro [VoterList[] voterLists1];
- questo parametro viene effettivamente passato al livello di trasmissione [2]. Questo livello trasmetterà il valore del parametro `listesElectorales1` sulla rete, non il suo riferimento. La forma esatta di questo valore dipende dal protocollo di comunicazione utilizzato;
- il livello ricevente [3] recupererà questo valore e lo utilizzerà per ricostruire un oggetto [VoterList[] voterLists2] che rispecchia il parametro iniziale inviato dal livello [business]. Ora abbiamo due oggetti identici (in termini di contenuto) in due JVM diverse: voterLists1 e voterLists2.
- Il livello ricevente passerà l'oggetto listesElectorales2 al metodo calculerSieges del livello [business], che lo salverà nel database. Dopo questa operazione, il riferimento listesElectorales2 punta a un array di oggetti [VoterList] con i campi [seats, eliminated] inizializzati. Questo non è il caso dell'oggetto listesElectorales1, a cui il livello [ui] ha un riferimento. Se vogliamo che il livello [ui] abbia un riferimento all'oggetto listesElectorales2, dobbiamo passarlo al livello [ui]. Pertanto, utilizziamo la seguente firma per il metodo [calculerSieges]:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
- Con questa firma, il metodo `calculateSeats` restituirà come risultato il riferimento a `electoralLists2`. Questo risultato viene restituito al livello ricevente [3], che aveva chiamato il livello [business]. Il livello [business] restituirà il valore (non il riferimento) di `electoralLists2` al livello mittente [2];
- Il livello di invio [2] recupererà questo valore e lo utilizzerà per ricostruire un oggetto [VoterList[] voterLists3] che rispecchia il risultato restituito dal metodo calculateSeats del livello [business].
- L'oggetto [VoterList[] voterLists3] viene restituito al metodo nel livello [UI] la cui chiamata al metodo calculateSeats del livello [DAO] aveva avviato l'intero meccanismo;
In questo processo, gli oggetti di tipo [VoterList] passeranno tra i livelli [2] e [3]:
- Quando il livello [2] trasmette il valore di un oggetto [VoterList] al livello [3], si dice che l'oggetto viene serializzato. La forma esatta di questa serializzazione dipende dal protocollo di comunicazione utilizzato;
- Quando il livello [3] recupera il valore di un oggetto [VoterList] per creare un nuovo oggetto [VoterList], si dice che l'oggetto viene deserializzato;
Affinché un oggetto possa essere sottoposto a questa serializzazione/deserializzazione, alcuni protocolli richiedono che l'oggetto implementi l'interfaccia [Serializable]. Questa interfaccia è semplicemente un indicatore; non ci sono metodi da implementare. Pertanto, la classe [VoterList] verrà ora dichiarata come segue:
public abstract class ListeElectorale implements Serializable {
private static final long serialVersionUID = 1L;
- Il campo alla riga 2 è obbligatorio. Può essere mantenuto così com'è e utilizzato per qualsiasi classe di tipo [Serializable].
4.3. La classe di eccezione
Torniamo all'interfaccia del livello [DAO]:
![]() |
public interface IElectionsDao {
public double getSeuilElectoral();
public int getNbSiegesAPourvoir();
public ListeElectorale[] getListesElectorales();
public void setListesElectorales(ListeElectorale[] listesElectorales);
}
Questi metodi operano con un database e possono incontrare vari errori, come ad esempio l'indisponibilità del database. Quando si scrive un metodo, è necessario gestire sempre i casi di errore. Questi sono tipicamente segnalati da un'eccezione. Abbiamo già incontrato la classe [ElectionsException] nella Sezione 3.3. Continueremo a utilizzarla, ma la miglioreremo come segue:
package ...;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
// exception class for the Elections application
// the exception is uncontrolled
public class ElectionsException extends RuntimeException implements Serializable {
// serial ID
private static final long serialVersionUID = 1L;
// local fields
private int code;
private List<String> erreurs;
// manufacturers
public ElectionsException() {
super();
}
public ElectionsException(int code, Throwable e) {
// parent
super(e);
// local
this.code = code;
this.erreurs = getErreursForException(e);
}
public ElectionsException(int code, String message, Throwable e) {
// parent
super(message,e);
// local
this.code = code;
this.erreurs = getErreursForException(e);
}
public ElectionsException(int code, String message) {
// parent
super(message);
// local
this.code = code;
List<String> erreurs = new ArrayList<>();
erreurs.add(message);
this.erreurs = erreurs;
}
public ElectionsException(int code, List<String> erreurs) {
// parent
super();
// local
this.code = code;
this.erreurs = erreurs;
}
// list of exception error messages
private List<String> getErreursForException(Throwable th) {
// retrieve the list of exception error messages
Throwable cause = th;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// the message is retrieved only if it is !=null and not blank
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// next cause
cause = cause.getCause();
}
return erreurs;
}
// getters and setters
...
}
- righe 16-17: il tipo [ElectionsException] incapsula:
- un codice di errore, riga 16;
- un elenco di messaggi di errore, riga 17;
La classe supporta cinque costruttori:
- riga 20: ElectionsException()
- Riga 24: ElectionsException(int code, Throwable e): il secondo parametro è di tipo [Throwable], che è la superclasse della classe [Exception]. Questo costruttore consente di incapsulare l'eccezione e insieme a un codice di errore. Il tipo [Throwable] (e quindi il tipo Exception) consente di incapsulare una o più eccezioni. L'idea è:
- intercettare un'eccezione che si verifica;
- arricchirla con un messaggio incapsulandola in una nuova eccezione;
- lanciare la nuova eccezione;
L'incapsulamento avviene alla riga 34 tramite l'istruzione [super(message, e)]. Questo processo di incapsulamento può essere ripetuto e l'eccezione iniziale può essere arricchita con messaggi diversi. Questo fenomeno è noto come stack delle eccezioni. Il metodo [private List<String> getErrorsForException(Throwable th)] consente di recuperare i vari messaggi associati alle eccezioni incapsulate:
- (continua)
- (continua)
- L'eccezione incapsulata si ottiene utilizzando il metodo Throwable [Throwable].getCause();
- il messaggio associato a un'eccezione si ottiene tramite il metodo String [Throwable].getMessage();
- (continua)
- righe 28–29: vengono costruiti i campi [code, errors];
- riga 32: public ElectionsException(int code, String message, Throwable e): questo costruttore è simile al precedente, tranne per il fatto che arricchisce l'eccezione che incapsulerà con un codice e un messaggio;
- riga 40: public ElectionsException(int code, String message): costruttore senza incapsulamento dell'eccezione;
- riga 50: public ElectionsException(int code, List<String> errors): costruttore senza incapsulamento dell'eccezione né messaggio;
La classe [ElectionsException] può essere utilizzata come segue:
dove il messaggio può essere presente o meno. Una volta creata, l'eccezione [ElectionsException] non è destinata a incapsulare nuove eccezioni. Nell'esempio sopra riportato, incapsula l'eccezione e1 e le eccezioni che e1 incapsula. Non vi sono ulteriori incapsulamenti oltre a questi.
La classe [ElectionsException] può anche essere utilizzata come segue:






