29. Esercizio pratico: versione 11
29.1. Introduzione
Nelle versioni precedenti dell'applicazione client/server per il calcolo delle imposte, il livello [logica di business] che implementa le regole di business per questo calcolo era sul lato server. Ora proponiamo di spostarlo sul lato client. Qual è il vantaggio? Parte del lavoro precedentemente svolto dal server verrà spostato sul lato client. Consideriamo uno scenario in cui un server riceve richieste da N client; N calcoli fiscali saranno eseguiti dai client. Nelle versioni precedenti, il server eseguiva questi N calcoli. Poiché non esegue più il calcolo, il server risponderà più rapidamente ai propri client e sarà quindi in grado di servirne un numero maggiore contemporaneamente.
L'architettura client/server diventa la seguente:

- il livello [aziendale] [10] è stato duplicato [12] sul client;
- un nuovo script [main2] [11] è stato aggiunto al client;
Il client web avrà due modi per calcolare l'imposta per l'elenco dei contribuenti trovato in [3]:
- utilizzare il metodo della versione precedente. Esso si avvale del livello [business] del server [10]. Lo script [main] utilizzerà questo metodo;
- è sufficiente richiedere i dati dell'autorità fiscale al server [2-4] e quindi utilizzare il livello [business] lato client [12];
Confronteremo le prestazioni dei due metodi.
29.2. Il server web
La struttura delle directory del server web sarà la seguente:

- La directory [http-servers/06] viene inizialmente creata copiando la directory [http-servers/05]. Manterremo infatti le funzionalità della precedente versione 10. Ci limiteremo ad aggiungervi una nuova funzionalità. Ciò è implementato dalla presenza di un nuovo controller [get_admindata_controller] [1]. L'altro controller [calculate_tax_controller] non è altro che il vecchio [index_controller] che è stato rinominato;
29.3. Configurazione
Il server offrirà due URL di servizio:
- [/calculate-tax] per calcolare l'imposta per un elenco di contribuenti passato nel corpo di una richiesta POST. Corrisponde quindi all'URL [/] della precedente versione 10;
- [/get-admindata] restituisce la stringa JSON dei dati amministrativi fiscali;
La configurazione [config] associa ciascuno di questi URL al controller che lo gestisce:
29.4. Lo script principale [main]
Lo script principale [main] ristruttura lo script [main] della versione precedente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
- righe 88–93: la funzione [calculate_tax] gestisce l'URL [/calculate-tax];
- righe 95–100: la funzione [get_admindata] gestisce l'URL [/get-admindata];
- Queste due funzioni non fanno nulla da sole. Passano immediatamente il controllo al controller principale [main_controller] nelle righe 37–86;
- righe 37–86: il controller principale [main_controller] non è altro che la funzione [index] della versione precedente, con una piccola differenza: mentre la funzione [index] gestiva un solo URL, qui [main_controller] ne gestisce due. Deve quindi farli elaborare da uno dei due controller [calculate_tax_controller, get_admin_data_controller];
- Righe 39–40: Recuperiamo l'azione richiesta [calculate_tax] o [get_admindata]. Queste informazioni si trovano nel percorso dell'URL [request.path]. A seconda dei casi, [request.path] è [/get-admindata] oppure [/calculate_tax]. La suddivisione alla riga 40 produrrà due elementi:
- la stringa vuota per la parte che precede il /;
- il nome dell'azione richiesta per la parte che segue il /;
- righe 62-63: una volta recuperata l'azione dell'URL, sappiamo quale controller utilizzare per gestire l'URL. Questa informazione si trova nella configurazione [config];
29.5. Controller
Il [calculate_tax_controller] non è altro che l'[index_controller] della versione precedente.
Il controller [get_admindata_controller] è il seguente:
- L'URL [/get-admindata] deve restituire la stringa JSON dei dati dell'amministrazione fiscale;
- riga 6: questi dati sono stati recuperati dallo script principale [main] e inseriti nel dizionario [config] come oggetto [AdminData]. Restituiamo il dizionario di questo oggetto;
29.6. Test Postman
Avviamo il server web, il DBMS e il server di posta [hMailServer]. Quindi, utilizzando un client Postman, calcoliamo l'imposta per diversi contribuenti:

Nella console di Postman, il dialogo client/server è il seguente:
POST /calculate-tax HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 5e71461a-fec8-4315-85e8-41721de939e5
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
…
{
"marié": "oui",
"enfants": 3,
"salaire": 200000
}
]
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:02:07 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347…]}}
Ora inviamo una richiesta GET all'URL [/get-admindata]:

Il dialogo client/server nella console Postman è il seguente:
GET /get-admindata HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 4af342c4-7ecb-4ab2-9e12-d653f81da424
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 596
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:07:24 GMT
{"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 93749.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
29.7. Il client web


La cartella [http-clients/06] viene inizialmente creata copiando la cartella [http-clients/05]. Il lavoro di modifica consiste essenzialmente in:
- modificare la configurazione [config_layers] in modo che ora includa un livello [business]. In precedenza, aveva solo un livello [DAO];
- aggiungere un nuovo metodo al livello [dao];
- scrivere uno script [main2] che si baserà sul livello [business] del client per calcolare le imposte dei contribuenti;
29.7.1. Configurazione del livello client
La configurazione dei livelli avviene in due punti:
- nella configurazione [config], che deve includere la cartella contenente l'implementazione del livello [business] nelle dipendenze del client. Questa cartella era già inclusa nelle dipendenze:
absolute_dependencies = [
# dossiers du projet
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Constantes, tranches
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# scripts de configuration
script_dir,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
Quindi è necessario modificare il file [config_layers]:
- righe 4–6: istanziazione del livello [business];
- righe 13-16: il livello [business] viene restituito nel dizionario dei livelli;
29.7.2. Implementazione del livello [dao]

Il livello [dao] implementerà la seguente interfaccia [InterfaceImpôtsDaoWithHttpClient]:
- riga 5: l'interfaccia [InterfaceImpôtsDaoWithHttpClient] eredita dalla classe astratta [AbstractImpôtsDao], che gestisce l'accesso al file system del client. Si noti che ha un metodo astratto [get_admindata];
- righe 7–10: il metodo [calculate_tax_in_bulk_mode] che abbiamo definito nella versione precedente consente il calcolo delle imposte per un elenco di contribuenti;
Questa interfaccia è implementata dalla seguente classe [ImpôtsDaoWithHttpClient]:
- riga 13: la classe [TaxDaoWithHttpClient] implementa l'interfaccia [TaxDaoWithHttpClientInterface]. Deriva quindi dalla classe [AbstractTaxDao];
- righe 65–66: il metodo [calculate_tax_in_bulk_mode] discusso nella versione precedente;
- righe 29–62: il metodo [get_admindata], che la classe padre [AbstractImpôtsDao] ha dichiarato come astratto. È quindi implementato nella classe figlia;
- righe 33–35: viene determinato l'URL del servizio web che il metodo [get-admindata] deve interrogare. Questi URL di servizio sono definiti nella configurazione [config] del client:
# le serveur de calcul de l'impôt
"server": {
"urlServer": "http://127.0.0.1:5000",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculate-tax",
"get-admindata": "/get-admindata"
}
},
- (continua)
- righe 9–12: i due URL del server web;
- righe 37–44: l'URL del servizio viene interrogato in modo sincrono;
- righe 46–42: se la configurazione lo richiede, la risposta del server viene registrata;
- riga 57: sappiamo che il server ha inviato una stringa JSON di un dizionario;
- righe 58–60: se lo stato HTTP della risposta non è 200, viene generata un'eccezione;
- righe 61-62: viene restituito l'oggetto [AdminData] che incapsula i dati amministrativi fiscali inviati dal server;
29.8. Gli script [main, main2]
Lo script [main] è quello della versione precedente. Utilizza il metodo [calculate_tax_in_bulk_mode] del livello [dao] e quindi utilizza il livello [business] del server;
Lo script [main2] fa la stessa cosa dello script [main] ma utilizza il livello [business] del client:
- righe 26-27: recupero dei dati dal server dell'agenzia delle entrate;
- righe 28-31: quindi l'imposta dei contribuenti viene calcolata localmente;
29.9. Test del client
In ciascuno degli script [main, main2], registriamo l'inizio e la fine dello script. Questo ci permette di calcolare il tempo di esecuzione dello script. Facciamo alcune previsioni:
- lo script [main] della versione precedente:
- crea N thread che vengono eseguiti simultaneamente;
- ogni thread elabora un gruppo di contribuenti per i quali calcola l'imposta tramite una singola richiesta al server;
- poiché i N thread vengono eseguiti contemporaneamente, la richiesta N+1 viene inviata prima che la richiesta N abbia ricevuto la propria risposta. Pertanto, le N richieste comportano un costo maggiore rispetto a una singola richiesta, ma probabilmente non di molto. Sul server vengono inoltre eseguiti 11 calcoli aziendali (il numero dei contribuenti);
- lo script [main2] in questa versione:
- effettua una singola richiesta al server;
- esegue 11 calcoli aziendali localmente sul client;
I calcoli aziendali richiederanno lo stesso tempo sia che vengano eseguiti sul server che sul client. La differenza risiederà quindi nelle richieste. Possiamo quindi aspettarci che il tempo di esecuzione di [main] sia leggermente più lungo di quello di [main2].
Avviamo il server versione 11, il DBMS e il server di posta [hMailServer]. Sul lato server, impostiamo il parametro [sleep_time] a zero in modo che entrambi i test vengano eseguiti nelle stesse condizioni.
Esecuzione 1 [main]
L'esecuzione di [main] produce i seguenti log:
2020-07-29 14:35:50.016079, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:35:50.016079, Thread-1 : début du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.016079, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.016079, Thread-3 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.016079, Thread-4 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.024426, Thread-5 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.050473, Thread-1 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.050473, Thread-1 : fin du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.050473, Thread-3 : {"réponse": {"results": [{"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-3 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-5 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-5 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-2 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.051214, Thread-4 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-4 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, MainThread : fin du calcul de l'impôt des contribuables
Il tempo di esecuzione è stato di [051214-016079] nanosecondi (riga 17 – riga 1), ovvero 35 millisecondi e 135 nanosecondi.
Possiamo notare che tra la prima richiesta effettuata al server e l'ultima risposta ricevuta dal client, la durata è la stessa [051214-016079] (riga 15 – riga 1), ovvero 35 millisecondi e 135 nanosecondi.
Esecuzione 2 [main2]
L'esecuzione di [main2] produce i seguenti log:
2020-07-29 14:41:03.303520, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:41:03.345084, MainThread : {"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
2020-07-29 14:41:03.349975, MainThread : fin du calcul de l'impôt des contribuables
Il tempo di esecuzione è stato di [349975-303520] nanosecondi (riga 3 - riga 1), ovvero 46 millisecondi e 455 nanosecondi. Contrariamente alle aspettative, [main] è più veloce di [main2].
Vediamo che la singola richiesta da [main2] ha richiesto [345084-303520] (riga 2 – riga 1), ovvero 41 millisecondi e 564 nanosecondi. Il calcolo delle imposte ha quindi richiesto [349975-345084] (riga 3 – riga 2), ovvero 4 millisecondi e 91 nanosecondi. È la richiesta HTTP a determinare il tempo di esecuzione. Sorprendentemente, vediamo qui che la singola richiesta da [main2] ha richiesto più tempo [41 millisecondi] rispetto alle quattro richieste simultanee da [main] [35 millisecondi].
Sul lato server, i log sono i seguenti:
2020-07-29 14:35:27.047721, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:27.140927, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:28.790716, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:28.847518, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:50.039178, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.039178, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.043220, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.045796, Thread-3 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}, {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}, {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata' [GET]>
2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0, 13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0, 'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0, 'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0, 'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1, 'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
- riga 5: la prima richiesta dal client [main];
- riga 14: l'ultima risposta al client [main]. Tra le due ci sono 6 millisecondi e 647 nanosecondi;
- righe 15–16: la singola richiesta dal client [main2]. La risposta è istantanea;