29. Exercício prático: versão 11
29.1. Introdução
Nas versões anteriores da aplicação de cálculo de impostos cliente/servidor, a camada de [lógica de negócio] que implementa as regras de negócio para este cálculo estava no lado do servidor. Propomos agora movê-la para o lado do cliente. Qual é a vantagem? Parte do trabalho anteriormente realizado pelo servidor será transferido para o lado do cliente. Considere um cenário em que um servidor é consultado por N clientes; N cálculos fiscais serão realizados pelos clientes. Nas versões anteriores, o servidor realizava esses N cálculos. Como já não realiza o cálculo, o servidor responderá mais rapidamente aos seus clientes e, portanto, poderá atender a um maior número deles simultaneamente.
A arquitetura cliente/servidor passa a ser a seguinte:

- a camada [de negócios] [10] foi duplicada [12] no cliente;
- foi adicionado um novo script [main2] [11] ao cliente;
O cliente web terá duas formas de calcular o imposto para a lista de contribuintes encontrada em [3]:
- utilize o método da versão anterior. Este utiliza a camada [de negócios] do servidor [10]. O script [principal] utilizará este método;
- basta solicitar os dados da autoridade fiscal ao servidor [2-4] e, em seguida, utilizar a camada [de negócios] do lado do cliente [12];
Iremos comparar o desempenho dos dois métodos.
29.2. O servidor web
A estrutura de diretórios do servidor web será a seguinte:

- O diretório [http-servers/06] é inicialmente criado através da cópia do diretório [http-servers/05]. Iremos, de facto, manter as funcionalidades da versão 10 anterior. Iremos simplesmente adicionar-lhe uma nova funcionalidade. Isto é implementado através da presença de um novo controlador [get_admindata_controller] [1]. O outro controlador [calculate_tax_controller] não é outro senão o antigo [index_controller] que foi renomeado;
29.3. Configuração
O servidor disponibilizará dois URLs de serviço:
- [/calculate-tax] para calcular o imposto de uma lista de contribuintes passada no corpo de um pedido POST. Corresponde, portanto, à URL [/] da versão 10 anterior;
- [/get-admindata] devolve a cadeia JSON dos dados de administração fiscal;
A configuração [config] associa cada uma destas URLs ao controlador que a processa:
29.4. O script principal [main]
O script principal [main] reestrutura o script [main] da versão anterior:
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 | |
- linhas 88–93: a função [calculate_tax] trata da URL [/calculate-tax];
- linhas 95–100: a função [get_admindata] trata da URL [/get-admindata];
- Estas duas funções não fazem nada por si só. Elas transferem imediatamente o controlo para o controlador principal [main_controller] nas linhas 37–86;
- linhas 37–86: o controlador principal [main_controller] nada mais é do que a função [index] da versão anterior, com uma pequena diferença: enquanto a função [index] tratava apenas de um único URL, aqui o [main_controller] trata de dois URLs. Por isso, deve fazer com que estes sejam processados por um dos dois controladores [calculate_tax_controller, get_admin_data_controller];
- Linhas 39–40: Recuperamos a ação solicitada [calculate_tax] ou [get_admindata]. Esta informação encontra-se no caminho da URL [request.path]. Dependendo do caso, [request.path] é [/get-admindata] ou [/calculate_tax]. A divisão na linha 40 irá produzir dois elementos:
- a string vazia para a parte que precede o /;
- o nome da ação solicitada para a parte que se segue ao /;
- linhas 62-63: assim que a ação da URL for recuperada, sabemos qual o controlador a utilizar para tratar a URL. Esta informação encontra-se na configuração [config];
29.5. Controladores
O [calculate_tax_controller] não é outro senão o [index_controller] da versão anterior.
O controlador [get_admindata_controller] é o seguinte:
- A URL [/get-admindata] deve devolver a cadeia JSON dos dados da administração fiscal;
- linha 6: estes dados foram recuperados pelo script principal [main] e colocados no dicionário [config] como um objeto [AdminData]. Devolvemos o dicionário deste objeto;
29.6. Testes no Postman
Iniciamos o servidor web, o SGBD e o servidor de e-mail [hMailServer]. Em seguida, utilizando um cliente Postman, calculamos o imposto para vários contribuintes:

Na consola do Postman, o diálogo cliente/servidor é o seguinte:
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…]}}
Agora vamos solicitar a URL [/get-admindata] com uma solicitação GET:

O diálogo cliente/servidor na consola do Postman é o seguinte:
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. O cliente web


A pasta [http-clients/06] é inicialmente criada através da cópia da pasta [http-clients/05]. O trabalho de modificação consiste essencialmente em:
- modificar a configuração [config_layers] para que inclua agora uma camada [business]. Anteriormente, tinha apenas uma camada [DAO];
- adicionar um novo método à camada [dao];
- escrever um script [main2] que irá basear-se na camada [business] do cliente para calcular os impostos dos contribuintes;
29.7.1. Configuração da Camada do Cliente
A configuração da camada ocorre em dois locais:
- na configuração [config], que deve incluir a pasta contendo a implementação da camada [business] nas dependências do cliente. Esta pasta já estava incluída nas dependências:
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",
]
Em seguida, o ficheiro [config_layers] deve ser modificado:
- linhas 4–6: instanciação da camada [de negócios];
- linhas 13-16: a camada [business] é devolvida no dicionário de camadas;
29.7.2. Implementação da camada [dao]

A camada [dao] irá implementar a seguinte interface [InterfaceImpôtsDaoWithHttpClient]:
- linha 5: a interface [InterfaceImpôtsDaoWithHttpClient] herda da classe abstrata [AbstractImpôtsDao], que gere o acesso ao sistema de ficheiros do cliente. Note-se que possui um método abstrato [get_admindata];
- linhas 7–10: o método [calculate_tax_in_bulk_mode] que definimos na versão anterior permite o cálculo do imposto para uma lista de contribuintes;
Esta interface é implementada pela seguinte classe [ImpôtsDaoWithHttpClient]:
- linha 13: a classe [TaxDaoWithHttpClient] implementa a interface [TaxDaoWithHttpClientInterface]. Por conseguinte, deriva da classe [AbstractTaxDao];
- linhas 65–66: o método [calculate_tax_in_bulk_mode] discutido na versão anterior;
- linhas 29–62: o método [get_admindata], que a classe pai [AbstractImpôtsDao] declarou como abstrato. Por conseguinte, é implementado na classe filha;
- linhas 33–35: é determinada a URL do serviço web que o método [get-admindata] deve consultar. Estas URLs de serviço são definidas na configuração [config] do cliente:
# 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ção)
- linhas 9–12: os dois URLs do servidor web;
- linhas 37–44: o URL do serviço é consultado de forma síncrona;
- linhas 46–42: se a configuração assim o exigir, a resposta do servidor é registada;
- linha 57: sabemos que o servidor enviou uma cadeia JSON de um dicionário;
- linhas 58–60: se o estado HTTP da resposta não for 200, é lançada uma exceção;
- linhas 61-62: o objeto [AdminData] que encapsula os dados de administração fiscal enviados pelo servidor é devolvido;
29.8. Os scripts [main, main2]
O script [main] é o da versão anterior. Utiliza o método [calculate_tax_in_bulk_mode] da camada [dao] e, por conseguinte, utiliza a camada [business] do servidor;
O script [main2] faz o mesmo que o script [main], mas utiliza a camada [business] do cliente:
- linhas 26-27: recuperar dados do servidor da autoridade fiscal;
- linhas 28-31: em seguida, o imposto dos contribuintes é calculado localmente;
29.9. Testes do cliente
Em cada um dos scripts [main, main2], registamos o início e o fim do script. Isto permite-nos calcular o tempo de execução do script. Vamos fazer algumas previsões:
- o script [main] da versão anterior:
- cria N threads que são executadas simultaneamente;
- cada thread processa um lote de contribuintes para os quais calcula o imposto através de um único pedido ao servidor;
- uma vez que os N threads são executados em simultâneo, o pedido N+1 é enviado antes de o pedido N ter recebido a sua resposta. Assim, os N pedidos têm um custo superior ao de um único pedido, mas provavelmente não muito superior. Existem também 11 (o número de contribuintes) cálculos de negócios no servidor;
- o script [main2] nesta versão:
- faz uma única solicitação ao servidor;
- realiza 11 cálculos comerciais localmente no cliente;
Os cálculos comerciais demorarão o mesmo tempo, quer sejam realizados no servidor ou no cliente. A diferença residirá, portanto, nas solicitações. Podemos, portanto, esperar que o tempo de execução de [main] seja ligeiramente mais longo do que o de [main2].
Iniciamos o servidor versão 11, o SGBD e o servidor de e-mail [hMailServer]. No lado do servidor, definimos o parâmetro [sleep_time] como zero para que ambos os testes sejam executados nas mesmas condições.
Execução 1 [main]
A execução de [main] produz os seguintes registos:
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
O tempo de execução foi de [051214-016079] nanossegundos (linha 17 – linha 1), ou seja, 35 milissegundos e 135 nanossegundos.
Podemos ver que, entre o primeiro pedido feito ao servidor e a última resposta recebida pelo cliente, a duração é a mesma [051214-016079] (linha 15 – linha 1), 35 milissegundos e 135 nanossegundos.
Execução 2 [main2]
A execução de [main2] produz os seguintes registos:
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
O tempo de execução foi de [349975-303520] nanossegundos (linha 3 - linha 1), ou seja, 46 milissegundos e 455 nanossegundos. De forma bastante inesperada, [main] é mais rápido do que [main2].
Vemos que a única solicitação de [main2] demorou [345084-303520] (linha 2 – linha 1), ou seja, 41 milissegundos e 564 nanossegundos. O cálculo do imposto demorou então [349975-345084] (linha 3 – linha 2), ou seja, 4 milissegundos e 91 nanossegundos. É a solicitação HTTP que é responsável pelo tempo de execução. Surpreendentemente, vemos aqui que a única solicitação de [main2] demorou mais tempo [41 milissegundos] do que as quatro solicitações simultâneas de [main] [35 milissegundos].
No lado do servidor, os registos são os seguintes:
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}}}
- linha 5: o primeiro pedido do cliente [main];
- linha 14: a última resposta ao cliente [main]. Passam-se 6 milissegundos e 647 nanossegundos entre as duas;
- linhas 15–16: a única solicitação do cliente [main2]. A resposta é instantânea;