20. Programmazione asincrona con RxJava
Lettura consigliata: [Introduzione a RxJava. Applicazione agli ambienti Swing e Android.]
In questo capitolo riprendiamo il Capitolo 17.6, in cui abbiamo realizzato un'applicazione client/server con la seguente architettura:
![]() |
Alcune azioni dell'utente sull'interfaccia Swing in [1] innescano azioni che arrivano fino al database in [3] tramite una rete HTTP [2]. Per questo motivo, la risposta all'azione dell'utente potrebbe richiedere più o meno tempo per arrivare. Sarebbe utile includere un indicatore di caricamento sull'interfaccia utente con un'opzione per annullare l'operazione avviata se richiede troppo tempo. Nel Capitolo 17.6, ogni azione dell'utente che richiede lo scambio di dati con il server è sincrona. Il gestore di eventi eseguito dal codice non termina finché non viene ricevuta la risposta. Durante questo periodo, l’interfaccia grafica è bloccata: non risponde alle nuove azioni dell’utente. Queste vengono semplicemente messe in coda per essere elaborate una volta terminato il gestore di eventi attualmente in esecuzione. Pertanto, se venisse visualizzato un pulsante di annullamento, l’utente potrebbe cliccarci sopra, ma non accadrebbe nulla finché l’operazione corrente non fosse terminata. Il pulsante di annullamento non servirebbe quindi a nulla.
Affinché il clic sul pulsante di annullamento abbia effetto, l'operazione corrente deve essere completata. Per ottenere ciò, è necessario avviare l'operazione potenzialmente di lunga durata in modo asincrono:
- il gestore di eventi avvia l'operazione di lunga durata ma non attende il suo risultato e restituisce il controllo al thread dell'interfaccia utente che gestisce gli eventi dell'interfaccia grafica. L'operazione di lunga durata viene avviata su un thread diverso dal thread dell'interfaccia utente, il che impedisce a quest'ultimo di essere bloccato;
- Se l'utente fa clic sul pulsante Annulla prima che l'operazione di lunga durata sia completata, il thread UI inattivo può gestire questo evento. L'operazione di lunga durata può quindi essere abbandonata ignorandone il risultato;
- Se l'operazione di lunga durata non è stata annullata, l'arrivo della risposta attiverà un evento nel thread dell'interfaccia utente. Se il thread dell'interfaccia utente è inattivo, eseguirà quindi il codice associato a questo evento, che elaborerà la risposta;
L'interfaccia utente funzionerà come prima. Se i tempi di risposta del server sono rapidi, l'utente non noterà alcuna differenza. Se invece sono evidenti, all'utente verrà mostrato un pulsante di annullamento e avrà la possibilità di interrompere l'operazione in corso.
La libreria [Rx] consente la programmazione asincrona. Il suo principale vantaggio risiede nel fatto che è stata portata su numerosi ambienti (Java, .NET, JS, ecc.) e che la competenza in un ambiente può essere facilmente trasferita a un altro. Qui faremo riferimento al Capitolo 2 del documento [Introduzione a RxJava. Applicazione agli ambienti Swing e Android]. Si invita il lettore a leggerlo. Nelle sezioni seguenti, utilizzeremo il codice degli esempi presenti in questo capitolo.
Svilupperemo l'architettura dell'applicazione come segue:
![]() |
- In [1], inseriamo un livello [RxJava] tra il livello [Swing] e il livello [logica di business]. I metodi di quest'ultimo saranno ora chiamati in modo asincrono;
Procederemo in diversi passaggi:
- Fase 1: Il livello [logica di business, DAO] presenta attualmente un'interfaccia sincrona verso il livello [UI]. Lo trasformeremo in un livello asincrono [RxJava, logica di business, DAO];
- Fase 2: convertiremo l'applicazione console sincrona in un'applicazione che rimane sincrona ma utilizza l'interfaccia asincrona [RxJava, business, DAO];
- Fase 3: convertiremo l'applicazione Swing sincrona in un'applicazione Swing asincrona;
20.1. Fase 1
Stiamo convertendo l'attuale livello sincrono [logica di business, DAO] in un livello asincrono [RxJava, logica di business, DAO].
20.1.1. Creazione
Partiamo dal progetto Maven del Capitolo 17.4, che apriamo in NetBeans:
![]() | ![]() |
Duplichiamo questo progetto [1] (copia/incolla) in un nuovo progetto [elections-rxjava-business-dao-security-webjson] [2].
20.1.2. Configurazione di Maven
Aggiorniamo il file [pom.xml] del nuovo progetto per aggiungere la dipendenza dalla libreria [RxJava]:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-metier-dao-security-rxjava-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client jUnit du serveur web / jSON</description>
<name>elections-metier-dao-security-rxjava-webjson</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- log library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.reactivex/rxjava -->
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- righe 65–70: abbiamo aggiunto la dipendenza dalla libreria RxJava;
20.1.3. Implementazione asincrona del livello [business]
![]() |
Per implementare il livello [RxJava, business], aggiungiamo al progetto un'interfaccia asincrona [IRxElectionsMetier] [1] e la sua implementazione [RxElectionsMetier] [2]:
![]() |
L'interfaccia [IRxElectionsMetier] è l'interfaccia asincrona per il livello [RxJava, business]. Il suo codice è il seguente:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import rx.Observable;
public interface IRxElectionsMetier {
// authentication
Observable<Void> authenticate(User user);
// get the lists in competition
Observable<ListeElectorale[]> getListesElectorales(User user);
// the number of seats to be filled
Observable<Integer> getNbSiegesAPourvoir(User user);
// the electoral threshold
Observable<Double> getSeuilElectoral(User user);
// recording results
Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales);
// calculating seats
Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales);
}
L'interfaccia [IRxElectionsMetier] eredita i metodi dall'interfaccia [IElectionsMetier], ma mentre un metodo M nell'interfaccia [IElectionsMetier] restituiva un risultato di tipo T, il metodo M nell'interfaccia [IRxElectionsMetier] restituisce un risultato di tipo Observable<T>. Il tipo [Observable] è fornito dalla libreria RxJava. Un tipo Observable<T> fornisce il metodo [subscribe], che recupera il tipo T in modo asincrono. A questo metodo sono associati tre eventi:
- onSuccess(T result), che notifica che è disponibile un risultato di tipo T. L'operazione asincrona può restituire più risultati;
- onError(Throwable th), che notifica che l'operazione asincrona ha riscontrato un errore;
- onCompleted(), che notifica che l'operazione asincrona è stata completata;
Fino a quando non viene chiamato il metodo [Observable.subscribe], l'operazione asincrona associata all'observable non viene avviata. Il codice che chiama un metodo M dell'interfaccia [IRxElectionsMetier] non ottiene il risultato atteso T, ma un tipo Observable<T> che gli consentirà in seguito di ottenere il risultato T chiamando il metodo [Observable.subscribe].
L'implementazione [RxElectionsMetier] dell'interfaccia [IRxElectionsMetier] è la seguente:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;
@Component
public class RxElectionsMetier implements IRxElectionsMetier {
@Autowired
private IElectionsMetier metier;
@Override
public Observable<Void> authenticate(User user) {
...
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getListesElectorales(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
...
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
...
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
...
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
...
}
}
- righe 12-13: iniezione Spring del livello business sincrono;
- righe 20-34: commenteremo il metodo [getVoterLists], che invece di restituire un tipo [VoterList[]] restituisce un tipo [Observable<VoterList[]>];
- righe 22-32: il metodo statico [Observable.create] consente di creare un Observable da un tipo [Subscriber]. Il tipo [Subscriber] rappresenta un sottoscrittore dei flussi di risultati prodotti dal processo osservato (l'Observable). Fornisce tre metodi:
- [Subscriber.onNext] (riga 25) per ricevere un risultato dal processo osservato;
- [Subscriber.onError] (riga 30) per ricevere un'eccezione dal processo osservato. Dopo un'eccezione, il tipo [Observable] non emette più risultati;
- [Subscriber.onCompleted] (riga 27) per ricevere il segnale di fine emissione dal processo osservato. Qui, il processo osservato emette un solo elemento. Si noti che questo segnale non viene emesso se si verifica un'eccezione. Questo è il comportamento predefinito degli Observable: l'emissione di un'eccezione segnala anche la fine delle emissioni. Gli abbonati ne sono consapevoli;
- righe 22–34: il metodo [Observable.create] accetta un tipo [Observable.OnSubscribe] come parametro. Questo tipo è un'interfaccia funzionale. Questo concetto è stato introdotto con Java 8 e si riferisce a un'interfaccia con un unico metodo. Qui, l'unico metodo dell'interfaccia [Observable.OnSubscribe] è il seguente:
Per implementare un'interfaccia funzionale a metodo singolo m(param1, param2, ..., paramn), è possibile utilizzare la seguente sintassi semplificata:
Questo è ciò che viene fatto nelle righe 22–34:
- [subscriber] è il parametro del metodo [Observable.OnSubscribe.call];
- righe 23–32: il codice che vogliamo fornire al metodo [call];
- riga 25: richiediamo le liste elettorali in modo sincrono dal livello [business] iniettato alla riga 13. Dovremo quindi attendere il risultato. Una volta ricevuto, viene passato al metodo [onNext] del subscriber;
- riga 28: in caso di errore, l'eccezione viene passata al metodo [onError] del subscriber;
- riga 31: attendiamo un singolo risultato. Una volta ottenuto (le liste elettorali o un'eccezione), notifichiamo al sottoscrittore che il processo osservato ha terminato di emettere risultati;
È importante ricordare che il metodo [RxElectionsMetier] restituisce un tipo Observable<VoterList[]> e non il tipo VoterList[] stesso. Il codice chiamante deve chiamare il metodo Observable<VoterList[]>.subscribe affinché il codice nelle righe 23–33 venga eseguito e restituisca le liste degli elettori tramite la riga 25.
Il codice per gli altri metodi è simile:
package elections.security.client.metier;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.Observable;
@Component
public class RxElectionsMetier implements IRxElectionsMetier {
@Autowired
private IElectionsMetier metier;
@Override
public Observable<Void> authenticate(User user) {
return Observable.create(subscriber -> {
try {
// synchronous method call
metier.authenticate(user);
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> getListesElectorales(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getListesElectorales(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Integer> getNbSiegesAPourvoir(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getNbSiegesAPourvoir(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Double> getSeuilElectoral(User user) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.getSeuilElectoral(user));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<Void> recordResultats(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// synchronous method call
metier.recordResultats(user, listesElectorales);
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
@Override
public Observable<ListeElectorale[]> calculerSieges(User user, ListeElectorale[] listesElectorales) {
return Observable.create(subscriber -> {
try {
// call synchronous method then reply to subscriber
subscriber.onNext(metier.calculerSieges(user, listesElectorales));
// we signal the end of the observable
subscriber.onCompleted();
} catch (Exception e) {
// we forward the exception
subscriber.onError(e);
}
});
}
}
- righe 20 e 81: il metodo [onNext] del sottoscrittore non viene chiamato perché il sottoscrittore non si aspetta alcun risultato;
20.1.4. Test JUnit per il livello [business]
![]() |
20.1.4.1. Test01
Riprendiamo il test unitario [Test01] discusso nella Sezione 17.4.4. Esso era stato progettato per effettuare chiamate sincrone all'interfaccia [IElectionsMetier]. Lo modifichiamo in modo che effettui chiamate sincrone alla nuova interfaccia [IRxElectionsMetier]. È infatti possibile effettuare chiamate sincrone a un'interfaccia RxJava asincrona. Il codice diventa il seguente:
package elections.security.client.metier.junit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import elections.security.client.config.MetierConfig;
import elections.security.client.entities.ElectionsException;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import rx.observables.BlockingObservable;
@SpringApplicationConfiguration(classes = MetierConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// layer [electionsMetier]
@Autowired
private IRxElectionsMetier electionsMetier;
// mapper jSON
private final ObjectMapper mapper = new ObjectMapper();
// users
static private User admin;
static private User user;
static private User unknown;
@BeforeClass
public static void initTest() {
admin = new User("admin", "admin");
user = new User("user", "user");
unknown = new User("x", "y");
}
@Test()
public void checkUserUser() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(user)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("403 Forbidden", se.getErreurs().get(0));
}
@Test()
public void checkUserUnknown() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(unknown)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("401 Unauthorized", se.getErreurs().get(0));
}
@Test()
public void checkUserAdmin() {
ElectionsException se = null;
try {
BlockingObservable.from(electionsMetier.authenticate(admin)).firstOrDefault(null);
} catch (ElectionsException e) {
se = e;
}
Assert.assertNull(se);
}
/**
* vérification 1 : méthode de calcul des sièges on fixe en dur les listes
*/
@Test
public void calculSieges1() {
// create the table of 7 candidate lists
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// the seats for each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// check results
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
/**
* vérification 2 : méthode de calcul des sièges on demande les listes à la couche [metier] puis on fixe en dur les
* voix
*/
@Test
public void calculSieges2() {
// create the table of 7 candidate lists
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// the voices are hard-fixed
listes[0].setVoix(32000);
listes[1].setVoix(25000);
listes[2].setVoix(16000);
listes[3].setVoix(12000);
listes[4].setVoix(8000);
listes[5].setVoix(4500);
listes[6].setVoix(2500);
// the seats obtained by each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// check results
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
/**
* vérification 3 méthode de calcul des sièges on provoque une exception
*/
@Test(expected = ElectionsException.class)
public void calculSieges3() {
// we create a table of 24 candidate lists, each with 1 vote
ListeElectorale[] listes = new ListeElectorale[25];
// all 25 lists will have the same number of votes (4%)
for (int i = 0; i < listes.length; i++) {
listes[i] = new ListeElectorale("Liste" + (i + 1), 1, 0, false);
}
// calculation of seats - normally there should be a ElectionsException
// with an electoral threshold of 5%
BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
}
/**
* enregistrement des résultats de l'élection
*
* @throws JsonProcessingException
*/
@Test
public void ecritureResultatsElections() throws JsonProcessingException {
// create the table of 7 candidate lists
ListeElectorale[] listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// the voices are hard-fixed
listes[0].setVoix(32000);
listes[1].setVoix(25000);
listes[2].setVoix(16000);
listes[3].setVoix(12000);
listes[4].setVoix(8000);
listes[5].setVoix(4500);
listes[6].setVoix(2500);
// the seats obtained by each list are calculated
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// display results
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
// results are entered into the database
BlockingObservable.from(electionsMetier.recordResultats(admin, listes)).firstOrDefault(null);
// check results
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
// display results
for (int i = 0; i < listes.length; i++) {
System.out.println(mapper.writeValueAsString(listes[i]));
}
Assert.assertEquals(2, listes[0].getSieges());
Assert.assertFalse(listes[0].isElimine());
Assert.assertEquals(2, listes[1].getSieges());
Assert.assertFalse(listes[1].isElimine());
Assert.assertEquals(1, listes[2].getSieges());
Assert.assertFalse(listes[2].isElimine());
Assert.assertEquals(1, listes[3].getSieges());
Assert.assertFalse(listes[3].isElimine());
Assert.assertEquals(0, listes[4].getSieges());
Assert.assertFalse(listes[4].isElimine());
Assert.assertEquals(0, listes[5].getSieges());
Assert.assertTrue(listes[5].isElimine());
Assert.assertEquals(0, listes[6].getSieges());
Assert.assertTrue(listes[6].isElimine());
}
}
Esaminiamo le modifiche:
- riga 48: il metodo statico [BlockingObservable.from(Observable).first]:
- si abbona all'observable passato come parametro a [from];
- innesca l'esecuzione del codice associato all'osservabile;
- attende di ricevere il primo risultato. Si tratta quindi di un'operazione sincrona;
Qui usiamo il metodo [firstOrDefault(null)] perché l'osservabile [metier.authenticate] non restituisce un risultato quando viene eseguito. Il risultato del metodo [firstOrDefault(null)] sarà quindi null, un valore che qui non viene utilizzato;
Ripetiamo questo schema nel resto del codice ogni volta che vogliamo chiamare il livello [business].
Il test unitario [Test01] deve essere superato:
![]() |
Compito: verificare che il test [Test01] sia superato.
20.1.4.2. Test02
Modifichiamo il test [Test01] in modo che ora verifichi l'interfaccia asincrona [IRxElectionsMetier] effettuando chiamate asincrone ai suoi metodi.
Esaminiamo un test iniziale:
// thread synchronization semaphore
private CountDownLatch latch;
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// 1" semaphore
latch = new CountDownLatch((1));
// asynchronous operation
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// waiting for semaphore
latch.await();
// checking results
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- Riga 2: Un semaforo è uno strumento utilizzato per sincronizzare i thread tra loro. I thread sono flussi di esecuzione che si svolgono in parallelo. Per eseguire l'attività T1, il thread [Thread1] potrebbe aver bisogno che l'attività T2, eseguita dal thread [Thread2], sia completata. Attende quindi che il thread [Thread2] gli invii un segnale che indichi che l'attività T2 è terminata. Esistono vari modi per gestire questa sincronizzazione tra due thread. Il metodo utilizzato qui è il seguente:
- riga 10: il thread [Thread1] crea un semaforo con valore 1;
- riga 12: il thread [Thread1] crea e avvia un thread [Thread2]. Ciò si ottiene utilizzando la sintassi:
electionsMetier.authenticate(user).subscribeOn(Schedulers.io())
Il metodo [Observable.subscribeOn] imposta il thread su cui verrà eseguito il processo osservato. Il parametro di [subscribeOn] è un pool di thread. La libreria RxJava fornisce diversi pool adatti a diverse situazioni. Il pool [Schedulers.io()] è quello raccomandato per le operazioni di rete;
- (continua)
- righe 12-13: l'operazione
electionsMetier.authenticate(user).subscribeOn(Schedulers.io()).subscribe(...)
esegue l'operazione sincrona incapsulata nell'osservabile [authenticate(user)]. Tuttavia, poiché questa operazione sincrona viene avviata su un thread diverso da [Thread1], quest'ultimo non attende la risposta dal metodo [subscribe] e passa all'istruzione successiva;
- (continua)
- riga 23: il thread [Thread1] si mette in pausa e attende che il semaforo passi a 0 (attualmente è a 1);
- righe 13–21: il metodo [subscribe] accetta tre funzioni lambda come parametri:
- la prima [(result)->{...}] viene chiamata ogni volta che l'osservabile [authenticate(user)] emette un risultato [result]. Qui abbiamo un osservabile [authenticate(user)] che esegue un'operazione ma non emette alcun risultato. La lambda [(result)->{}] non verrà quindi mai chiamata. Ecco perché il suo codice è vuoto in questo caso [{}];
- la seconda [(th)->{...}] accetta un tipo [Throwable] come parametro. Viene chiamata quando l'esecuzione dell'osservabile incontra un'eccezione. Qui, gestiamo il parametro [Throwable th] come segue:
- riga 16: lo memorizziamo in un campo della classe di test di tipo [ElectionsException] perché l'osservabile eseguito genera solo questo tipo di eccezione;
- riga 17: impostiamo il semaforo a 0 per indicare che il thread [Thread2] ha terminato il suo lavoro;
- il terzo [()->{...}] viene chiamato quando l'osservabile non ha più elementi da emettere. Gestiamo questo evento come segue:
- riga 20: impostiamo il semaforo a 0 per indicare che il thread [Thread2] ha terminato il suo lavoro;
Si noti che il terzo lambda non viene chiamato se si verifica un'eccezione. Questo è il motivo per cui abbiamo dovuto impostare il semaforo a 0 anche alla riga 17;
- riga 25: quando raggiungiamo questa riga, l'osservabile ha terminato il suo lavoro. Possiamo quindi eseguire gli stessi controlli del test [Test01];
Esaminiamo un altro test:
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// create the table of 7 candidate lists
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// 1" semaphore
latch = new CountDownLatch((1));
// asynchronous operation
// the seats for each list are calculated
electionsMetier.calculerSieges(admin, listes).subscribeOn(Schedulers.io())
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
latch.countDown();
},
() -> {
latch.countDown();
});
// waiting for semaphore
latch.await();
// check results
Assert.assertNull(calculSieges1Exception);
Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
Assert.assertFalse(listesCalculSieges1[0].isElimine());
Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
Assert.assertFalse(listesCalculSieges1[1].isElimine());
Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
Assert.assertFalse(listesCalculSieges1[2].isElimine());
Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
Assert.assertFalse(listesCalculSieges1[3].isElimine());
Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
Assert.assertFalse(listesCalculSieges1[4].isElimine());
Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
Assert.assertTrue(listesCalculSieges1[5].isElimine());
Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
Assert.assertTrue(listesCalculSieges1[6].isElimine());
}
- righe 20–30: esecuzione asincrona dell'osservabile [electionsMetier.calculateSeats(admin, lists)];
- righe 21–23: l'esecuzione dell'osservabile restituisce un tipo [VoterList[]], che viene memorizzato in un campo della classe di test, riga 3;
- righe 34–48: questi controlli sono quelli del test [Test01], a cui abbiamo aggiunto il controllo alla riga 34 che garantisce che non si sia verificata alcuna eccezione;
L'intero test [Test02] è disponibile nei materiali del corso.
Compito: eseguire il test [Test02] e verificare che venga superato.
20.1.4.3. Test03
Il test [Test03] fa la stessa cosa del test [Test01]: verifica l'interfaccia [IRxElectionsMetier] utilizzando chiamate sincrone a tale interfaccia. È una copia del test [Test02] con due differenze:
- gli osservabili non vengono più eseguiti in un thread diverso da quello che esegue i test. Quando il thread [Thread1] esegue il metodo [subscribe] di un osservabile, avvia un'operazione HTTP verso il server sempre sul thread [Thread1]. L'intero metodo [subscribe] diventa quindi sincrono;
- poiché ora c'è un solo thread, la sincronizzazione dei thread diventa superflua e il semaforo scompare;
Ecco due esempi di test:
// -----------------------------------
private ElectionsException checkUserUserException;
@Test()
public void checkUserUser() throws InterruptedException {
// synchronous operation
electionsMetier.authenticate(user)
.subscribe((result) -> {
},
(th) -> {
checkUserUserException = (ElectionsException) th;
},
() -> {
});
// checking results
Assert.assertNotNull(checkUserUserException);
Assert.assertEquals("403 Forbidden", checkUserUserException.getErreurs().get(0));
}
- riga 7: per impostazione predefinita, il metodo [electionsMetier.authenticate(user).subscribe] viene eseguito nel thread del codice chiamante. Si tratta quindi di un'operazione sincrona;
// -----------------------------------
private ElectionsException calculSieges1Exception;
private ListeElectorale[] listesCalculSieges1;
@Test
public void calculSieges1() throws InterruptedException {
// create the table of 7 candidate lists
ListeElectorale[] listes = new ListeElectorale[7];
listes[0] = new ListeElectorale("A", 32000, 0, false);
listes[1] = new ListeElectorale("B", 25000, 0, false);
listes[2] = new ListeElectorale("C", 16000, 0, false);
listes[3] = new ListeElectorale("D", 12000, 0, false);
listes[4] = new ListeElectorale("E", 8000, 0, false);
listes[5] = new ListeElectorale("F", 4500, 0, false);
listes[6] = new ListeElectorale("G", 2500, 0, false);
// synchronous operation
// the seats for each list are calculated
electionsMetier.calculerSieges(admin, listes)
.subscribe((result) -> {
listesCalculSieges1 = result;
},
(th) -> {
calculSieges1Exception = (ElectionsException) th;
},
() -> {
});
// check results
Assert.assertNull(calculSieges1Exception);
Assert.assertEquals(2, listesCalculSieges1[0].getSieges());
Assert.assertFalse(listesCalculSieges1[0].isElimine());
Assert.assertEquals(2, listesCalculSieges1[1].getSieges());
Assert.assertFalse(listesCalculSieges1[1].isElimine());
Assert.assertEquals(1, listesCalculSieges1[2].getSieges());
Assert.assertFalse(listesCalculSieges1[2].isElimine());
Assert.assertEquals(1, listesCalculSieges1[3].getSieges());
Assert.assertFalse(listesCalculSieges1[3].isElimine());
Assert.assertEquals(0, listesCalculSieges1[4].getSieges());
Assert.assertFalse(listesCalculSieges1[4].isElimine());
Assert.assertEquals(0, listesCalculSieges1[5].getSieges());
Assert.assertTrue(listesCalculSieges1[5].isElimine());
Assert.assertEquals(0, listesCalculSieges1[6].getSieges());
Assert.assertTrue(listesCalculSieges1[6].isElimine());
}
Compito: Esegui il test [Test03] e verifica che sia superato.
20.2. Fase 2
Ora convertiremo l'applicazione console sincrona del Capitolo 17.5 in un'applicazione che rimane sincrona ma utilizza l'interfaccia asincrona [RxJava, logica di business, DAO];
![]() |
Partiamo dal progetto [elections-console-business-dao-security-webjson] [1] del Capitolo 17.5, che duplichiamo in un nuovo progetto [elections-console-rxjava-business-dao-security-webjson] [2]:
![]() | ![]() |
- In [3-4], nel nuovo progetto, rimuoviamo la dipendenza dal vecchio livello [business] sincrono;
![]() | ![]() |
- In [5-9], aggiungiamo una dipendenza dal nuovo livello [aziendale] asincrono;
![]() | ![]() |
- nei [10-14], rinominiamo la classe [ElectionsConsole] in [ElectionsConsole01];
Allo stesso modo, rinominiamo la classe [BootElectionsConsole] in [BootElectionsConsole01]:
![]() |
Il codice attuale della classe [BootElectionsConsole01] è il seguente:
package elections.security.client.boot;
import elections.security.client.console.IElectionsUI;
public class BootElectionsConsole01 extends AbstractBootElections{
public static void main(String[] arguments) {
new BootElectionsConsole01().run();
}
@Override
protected IElectionsUI getUI() {
return ctx.getBean("electionsConsole",IElectionsUI.class);
}
}
- Riga 13: Poiché abbiamo cambiato il nome della classe da [ElectionsConsole] a [ElectionsConsole01], ora dobbiamo scrivere:
return ctx.getBean("electionsConsole01",IElectionsUI.class);
Torniamo al codice della classe [ElectionsConsole01]:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// competing lists
ListeElectorale[] listes;
// data entry
try (Scanner clavier = new Scanner(System.in)) {
// lists in competition are requested from the [metier] layer
listes = electionsMetier.getListesElectorales(admin);
...
// we calculate the number of seats
listes=electionsMetier.calculerSieges(admin,listes);
// we record the results
electionsMetier.recordResultats(admin,listes);
...
}
Se seguiamo l'esempio del test [Test01] nella sezione 20.1.4.1, le righe 5, 17, 20 e 22 cambieranno come segue:
@Component
public class ElectionsConsole01 implements IElectionsUI {
@Autowired
private IRxElectionsMetier electionsMetier;
@Autowired
private User admin;
@Override
public void run() {
// competing lists
ListeElectorale[] listes;
// data entry
try (Scanner clavier = new Scanner(System.in)) {
// lists in competition are requested from the [metier] layer
listes = BlockingObservable.from(electionsMetier.getListesElectorales(admin)).first();
...
// we calculate the number of seats
listes = BlockingObservable.from(electionsMetier.calculerSieges(admin, listes)).first();
// we record the results
BlockingObservable.from(electionsMetier.recordResultats(admin, listes));
...
}
Compito: Configurare il progetto per eseguire la classe [BootElectionsConsole01] con i tre parametri [SS, Ore lavorate, Giorni lavorati] e verificare che l'esecuzione del progetto così configurato produca i risultati attesi.
Compito: configurare il progetto per eseguire la coppia [BootElectionsConsole02, ElectionsConsole02], dove la classe [ElectionsConsole02] è stata scritta seguendo il modello del test [Test02] nella Sezione 20.1.4.2.
Compito: configurare il progetto per eseguire la coppia [BootElectionsConsole03, ElectionsConsole03], dove la classe [ElectionsConsole03] è stata scritta seguendo il modello del test [Test03] nella Sezione 20.1.4.3.
20.3. Fase 3
Procederemo ora al porting dell'applicazione Swing in un ambiente asincrono.
![]() |
Iniziamo duplicando il progetto [elections-swing-metier-dao-security-webjson] [1] del Capitolo 17.6 in un nuovo progetto [elections-swing-rxjava-metier-dao-security-webjson] [2]:
![]() | ![]() |
- in [3, 4], rimuoviamo la dipendenza dal livello sincrono [console];
![]() | ![]() |
- in [5-9], aggiungiamo una dipendenza dal livello console asincrono;
Il livello [swing] effettuerà vere e proprie chiamate asincrone al livello [business]. Quando si chiama un metodo su quest'ultimo, ci saranno due thread:
- il thread dell'interfaccia utente, che gestisce gli eventi;
- un thread I/O che eseguirà la chiamata HTTP al server;
Durante la chiamata asincrona, dovremmo visualizzare un'immagine di caricamento e un pulsante di annullamento. Non lo faremo qui, e ti verrà suggerito come miglioramento dell'applicazione. Le modifiche vengono apportate nelle due classi che effettuano le chiamate al livello [business]:
![]() |
20.3.1. Configurazione Maven
In questa sede utilizzeremo la libreria [RxSwing], che estende la libreria [RxJava] con funzionalità disponibili solo in un ambiente Swing. A tal fine, modifichiamo il file [pom.xml] come segue:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.elections</groupId>
<artifactId>elections-swing-rxjava-metier-dao-security-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>elections-swing-rxjava-metier-dao-security-webjson</name>
<description>couche swing asynchrone du client web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- RxSwing -->
<!-- https://mvnrepository.com/artifact/io.reactivex/rxswing -->
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxswing</artifactId>
<version>0.27.0</version>
</dependency>
<!-- lower layers -->
<dependency>
<groupId>istia.st.elections</groupId>
<artifactId>elections-console-rxjava-metier-dao-security-webjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
20.3.2. La classe [ElectionsConnectForm]
In un'operazione asincrona, la classe [ElectionsConnectForm] diventa la seguente:
package elections.security.client.swing;
import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.User;
import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.SwingUtilities;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;
@Component
public class ElectionsConnectForm extends AbstractElectionsConnectForm implements IElectionsUI {
private static final long serialVersionUID = 1L;
// reference to the asynchronous [business] layer
@Autowired
private IRxElectionsMetier metier;
// logged-in user
private User user;
// main form
@Autowired
private ElectionsMainForm electionsMainForm;
// session UI
@Autowired
private UiSession uiSession;
@Override
protected void doConnect() {
if (isPageValid()) {
// user authentication
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(
// there is no answer
(result) -> {
},
// exception management
(th) -> {
// we note the error
String info = getInfoForException("Les erreurs suivantes se sont produites :", th);
// display info
jTextPaneErreurs.setText(info);
jTextPaneErreurs.setCaretPosition(0);
},
// authentication is complete
() -> {
// the user is stored in the session
uiSession.setUser(user);
// connection view is hidden
setVisible(false);
// the main view is displayed
electionsMainForm.run();
});
}
}
// initializations
@Override
protected void init() {
...
}
@Override
public void run() {
// the graphical interface is displayed
SwingUtilities.invokeLater(new Runnable() {
public void run() {
init();
setVisible(true);
}
});
}
private boolean isPageValid() {
...
}
private String getInfoForException(String message, Throwable ex) {
...
}
}
- Righe 36–63: Il metodo [doConnect] viene eseguito quando l'utente tocca l'opzione di menu [Connetti]:
![]() |
È tutto nella riga 40:
metier.authenticate(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance()).subscribe(...)
- Il processo osservato è [metier.authenticate(user)];
- verrà eseguito su un thread I/O prelevato dal pool [Schedulers.io()];
- sarà osservato nel thread dell'interfaccia utente, quello che gestisce gli eventi dell'interfaccia Swing [observeOn(SwingScheduler.getInstance())]. Questo thread viene ottenuto tramite il metodo [SwingScheduler.getInstance()], dove [SwingScheduler] è una classe fornita dalla libreria [RxSwing]. Questo è obbligatorio. Quando si ottiene il risultato dell'operazione asincrona, spesso viene utilizzato per modificare elementi dell'interfaccia Swing. Tuttavia, l'interfaccia Swing può essere modificata solo nel thread UI; in caso contrario, viene generata un'eccezione. Le righe 41–61 devono quindi essere eseguite nel thread UI. Ciò è garantito qui dal metodo [observeOn(SwingScheduler.getInstance())];
Commentiamo il resto del codice:
- righe 42–43: queste righe servono a rispettare la sintassi del metodo [subscribe]. Non verranno mai eseguite perché il processo [metier.authenticate(user)] non restituisce alcun risultato;
- righe 35–52: quando viene ricevuta un'eccezione, questa viene visualizzata;
- righe 54-61: vengono eseguite quando il processo [metier.authenticate(user)] segnala la fine delle sue emissioni;
20.3.3. La classe [ElectionsMainForm]
![]() |
20.3.3.1. Inizializzazione dell'interfaccia utente grafica
package elections.security.client.swing;
import elections.security.client.console.IElectionsUI;
import elections.security.client.entities.ListeElectorale;
import elections.security.client.entities.User;
import elections.security.client.metier.IRxElectionsMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rx.schedulers.Schedulers;
import rx.schedulers.SwingScheduler;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
@Component
public class ElectionsMainForm extends AbstractElectionsMainForm implements IElectionsUI {
private static final long serialVersionUID = 1L;
// reference to the asynchronous [business] layer
@Autowired
private IRxElectionsMetier metier;
// session UI
@Autowired
private UiSession uiSession;
// logged-in user
private User user;
// list templates JList
private DefaultListModel<String> modèleNomsVoix = null;
private DefaultListModel<String> modèleRésultats = null;
// competing lists
private ListeElectorale[] listes;
// user-entered lists
private final List<ListeElectorale> listesSaisies = new ArrayList<>();
private ListeElectorale[] tListesSaisies;
// initializations
@Override
protected void init() {
// generation of components by the parent class
super.init();
// form status
Utilitaires.setEnabled(new JLabel[]{jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer}, false);
Utilitaires.setEnabled(
new JMenuItem[]{jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer}, false);
// center window
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Dimension frameSize = getSize();
if (frameSize.height > screenSize.height) {
frameSize.height = screenSize.height;
}
if (frameSize.width > screenSize.width) {
frameSize.width = screenSize.width;
}
setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
// logged-in user
user = uiSession.getUser();
// local initializations
modèleNomsVoix = new DefaultListModel<>();
jListNomsVoix.setModel(modèleNomsVoix);
modèleRésultats = new DefaultListModel<>();
jListResultats.setModel(modèleRésultats);
// lists are requested from the [business] layer
metier.getListesElectorales(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
listesElectorales -> {
// memorize lists
listes = listesElectorales;
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
// next step
doInitStep2();
});
}
...
- riga 46: il metodo [init] viene eseguito quando la finestra associata sta per essere visualizzata. Il suo scopo è quello di inizializzare i componenti [1-3] riportati di seguito:
![]() |
- righe 71–85: le liste dei candidati vengono richieste in modo asincrono (componente [1]);
- riga 71: il processo osservato è [metier.getListesElectorales(user)]. Viene eseguito su un thread I/O [subscribeOn(Schedulers.io())] e osservato sul thread dell'interfaccia utente [observeOn(SwingScheduler.getInstance())];
- righe 74–77: il risultato restituito dal processo osservato viene memorizzato nel campo [listes] alla riga 38;
- riga 79: qualsiasi eccezione viene gestita dal seguente metodo:
private void showException(Throwable th) {
// exception is displayed
jTextPaneMessages.setText(getInfoForException("Les erreurs suivantes se sont produites : ", th));
jTextPaneMessages.setCaretPosition(0);
}
- Righe 81–84: Al termine del processo osservato, vengono eseguite le righe 81–84. Queste righe non vengono eseguite se si è verificata un'eccezione. Il metodo [doInitStep2] esegue la fase 2 dell'inizializzazione come segue:
private void doInitStep2() {
// associate list names with the jComboBoxNomsListes combo
for (int i = 0; i < listes.length; i++) {
jComboBoxNomsListes.addItem(String.format("%s - %s", listes[i].getId(), listes[i].getNom()));
}
// number of seats to be filled
metier.getNbSiegesAPourvoir(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
nbSiegesAPourvoir -> {
// initialize the label linked to this information
jLabelSAP.setText(jLabelSAP.getText() + nbSiegesAPourvoir);
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
// next step
doInitStep3();
});
}
- righe 3–5: utilizziamo il risultato del passaggio precedente per popolare l'elenco a discesa con i nomi delle liste dei candidati;
- righe 7–20: richiediamo il numero di seggi da assegnare in modo asincrono;
- riga 7: il processo osservato è [metier.getNbSiegesAPourvoir(user)]. Viene eseguito su un thread I/O [subscribeOn(Schedulers.io())] e osservato sul thread dell'interfaccia utente [observeOn(SwingScheduler.getInstance())];
- righe 10–13: il risultato restituito dal processo viene utilizzato per aggiornare l'interfaccia utente grafica;
- riga 15: vengono visualizzate eventuali eccezioni;
- righe 17–20: alla ricezione del segnale di fine dall'osservabile, si procede al passo 3 del processo di inizializzazione;
Il passaggio 3 dell'inizializzazione è gestito dal seguente codice:
private void doInitStep3() {
// electoral threshold
metier.getSeuilElectoral(user).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// answer
seuilElectoral -> {
// initialize the label linked to this information
jLabelSE.setText(jLabelSE.getText() + seuilElectoral);
},
// exception
(th) -> showException(th),
// observable purpose
() -> {
});
}
- righe 3-4: la soglia elettorale viene richiesta in modo asincrono;
- riga 3: il processo osservato è [business.getVotingThreshold(user)]. Viene eseguito su un thread I/O [subscribeOn(Schedulers.io())] e osservato sul thread dell'interfaccia utente [observeOn(SwingScheduler.getInstance())];
- righe 6-9: il risultato restituito dal processo viene utilizzato per aggiornare la GUI;
- riga 11: vengono visualizzate eventuali eccezioni;
- righe 13–14: alla ricezione del segnale di fine dall'osservabile, non viene intrapresa alcuna azione: il processo di inizializzazione della GUI è completo;
20.3.3.2. Calcolo dei seggi vinti dalle varie liste
Il metodo [doCalculer] è responsabile del calcolo del numero di seggi conquistati dalle varie liste:
@Override
protected void doCalculer() {
tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
// calcul des sièges
String info = null;
metier.calculerSieges(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// traitement résultat
result -> consumeResultSieges(result),
// traitement exception
th -> showException(th),
// fin observable
() -> {
}
);
}
- righe 6–15: i posti ottenuti dalle varie liste vengono calcolati in modo asincrono;
- riga 6: il processo osservato è [metier.calculateSeats(user, tListsEntered)]. Viene eseguito su un thread I/O [subscribeOn(Schedulers.io())] e osservato sul thread dell'interfaccia utente [observeOn(SwingScheduler.getInstance())];
- riga 9: il risultato restituito dal processo viene utilizzato dal metodo [consumeResultSeats];
- riga 11: vengono visualizzate eventuali eccezioni;
- righe 13–14: una volta ricevuto il segnale di fine dall'osservabile, non viene intrapresa alcuna azione;
Riga 9: il metodo [consumeResultSieges] elabora il risultato restituito dal processo osservato, aggiornando gli elenchi dei candidati con i campi [seats, eliminated]:
private void consumeResultSieges(ListeElectorale[] tListesSaisies) {
// the result is stored
this.tListesSaisies = tListesSaisies;
// display of results
modèleRésultats.clear();
for (int i = 0; i < tListesSaisies.length; i++) {
modèleRésultats.addElement(tListesSaisies[i].toString());
}
// maj state form
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, true);
Utilitaires.setEnabled(new JLabel[]{jLabelCalculer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, true);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemCalculer}, false);
jTextPaneMessages.setText("Calcul terminé");
}
- righe 4-14: il risultato viene utilizzato per aggiornare l'interfaccia grafica;
20.3.3.3. Registrazione dei risultati elettorali
I risultati elettorali vengono salvati utilizzando il seguente metodo [doEnregistrer]:
@Override
protected void doEnregistrer() {
// on demande l'enregistrement à la couche [métier]
metier.recordResultats(user, tListesSaisies).subscribeOn(Schedulers.io()).observeOn(SwingScheduler.getInstance())
.subscribe(
// traitement du résultat - il n'y en a pas ici
(param) -> {
},
// traitement de l'exception
(th) -> showException(th),
// fin observable
() -> {
// maj du formulaire
Utilitaires.setEnabled(new JLabel[]{jLabelEnregistrer}, false);
Utilitaires.setEnabled(new JMenuItem[]{jMenuItemEnregistrer}, false);
jTextPaneMessages.setText("Enregistrement des résultats réalisé");
}
);
}
- righe 4–17: i risultati delle elezioni vengono salvati in modo asincrono;
- riga 4: il processo osservato è [business.recordResults(user, tEnteredLists)]. Viene eseguito su un thread I/O [subscribeOn(Schedulers.io())] e osservato sul thread dell'interfaccia utente [observeOn(SwingScheduler.getInstance())];
- righe 7–8: queste righe non verranno mai eseguite perché il processo osservato non restituisce un risultato;
- riga 10: viene visualizzata qualsiasi eccezione;
- righe 14–16: alla ricezione del segnale di fine dall'osservabile, la GUI viene aggiornata;
Compito: Verificare che l'applicazione Swing funzioni. Quindi modificare la GUI e il codice in modo che durante un'operazione asincrona con il server web/JSON, appaia un'immagine di caricamento insieme a un'opzione per annullare l'operazione in corso.























