Skip to content

28. 应用练习:第 10 版

28.1. 简介

在税务计算服务器的客户端示例中,如果需要处理 N 名纳税人,线程会依次发送 N 个请求。此处的思路是发送一个包含 N 名纳税人的单一请求。对于每名纳税人,必须发送 [已婚、子女、工资] 信息。这些信息可以作为参数发送:

  • 在 URL 中。这会导致 URL 过长且毫无意义;
  • 在 HTTP 请求正文中。我们知道,使用浏览器的用户无法看到该正文;

这两种情况下,均可使用 [GET][POST] 请求。我们将采用 POST 请求,并将参数嵌入 HTTP 请求正文中。

客户端/服务器架构并未改变:

Image

28.2. Web 服务器

Image

[http-servers/05] 文件夹最初是通过复制 [http-servers/02] 文件夹创建的。我们回到客户端与服务器之间的 JSON 交互。我们已经看到,从 JSON 切换到 XML 非常简单。

28.2.1. 配置

配置 [config, config_database, config_layers] 与之前版本保持一致。我们不再赘述。

28.2.2. 主脚本 [main]

[main] 脚本与我们复制的 [http-servers/02] 文件夹中的脚本完全相同。仅有一处不同:

1
2
3
4
5
#  Home URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():
    
  • 第 2 行:现在通过 POST 请求访问 / URL;

28.2.3. [index_controller]

[index_controller] 的演变如下:

#  import dependencies

import json

from flask_api import status
from werkzeug.local import LocalProxy


def execute(request: LocalProxy, config: dict) -> tuple:
    #  dependencies
    from ImpôtsError import ImpôtsError
    from TaxPayer import TaxPayer

    #  retrieve the body of the post - wait for a list of dictionaries
    msg_erreur = None
    list_dict_taxpayers = None
    #  the jSON body of POST
    request_text = request.data
    try:
        #  which we transform into a list of dictionaries
        list_dict_taxpayers = json.loads(request_text)
    except BaseException as erreur:
        #  we note the error
        msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
    #  do we have a non-empty list?
    if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
        #  we note the error
        msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
    #  do we have a list of dictionaries?
    if not msg_erreur:
        erreur = False
        i = 0
        while not erreur and i < len(list_dict_taxpayers):
            erreur = not isinstance(list_dict_taxpayers[i], dict)
            i += 1
        #  mistake?
        if erreur:
            msg_erreur = "le corps du POST doit être une liste de dictionnaires"
    #  mistake?
    if msg_erreur:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": [msg_erreur]}}
        return résultats, status.HTTP_400_BAD_REQUEST

    #  check TaxPayers one by one
    #  initially no errors
    list_erreurs = []
    for dict_taxpayer in list_dict_taxpayers:
        #  we create a TaxPayer from dict_taxpayer
        msg_erreur = None
        try:
            #  the following operation will eliminate cases where the parameters are not
            #  properties of the TaxPayer class as well as the cases where their values
            #  are incorrect
            TaxPayer().fromdict(dict_taxpayer)
        except BaseException as erreur:
            msg_erreur = f"{erreur}"
        #  certain keys must be present in the dictionary
        if not msg_erreur:
            #  the keys [married, children, salary] must be present in the dictionary
            keys = dict_taxpayer.keys()
            if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
        #  mistakes?
        if msg_erreur:
            #  we note the error in the TaxPayer itself
            dict_taxpayer['erreur'] = msg_erreur
            #  add the TaxPayer to the error list
            list_erreurs.append(dict_taxpayer)

    #  we've processed all the taxpayers - are there any mistakes?
    if list_erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": list_erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    #  no mistakes, we can work
    #  data recovery from tax authorities
    admindata = config["admindata"]
    métier = config["layers"]["métier"]
    try:
        #  process the TaxPayer one by one
        list_taxpayers = []
        for dict_taxpayer in list_dict_taxpayers:
            #  tAX CALCULATION
            taxpayer = TaxPayer().fromdict(
                {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                 'salaire': dict_taxpayer['salaire']})
            métier.calculate_tax(taxpayer, admindata)
            #  the result is stored as a dictionary
            list_taxpayers.append(taxpayer.asdict())
        #  we send the response to the client
        return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
    except ImpôtsError as erreur:
        #  an error response is sent to the client
        return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • 第 9 行:控制器接收:
      • 客户端的请求
      • 服务器配置 [config]
  • 第 14–18 行:我们提取 POST 请求体。封装在 HTTP 请求体中的参数可以采用多种编码方式。我们之前已经遇到过一种:[x-www-form-urlencoded]。这里,我们将使用另一种编码:JSON;
  • 第 18 行:[request.data] 获取 HTTP 请求的正文。此处我们获取的是文本,且知道该文本是 JSON 格式,表示一个字典列表 [married, children, salary]
  • 第 19–24 行:我们提取这个字典列表;
  • 第 22–24 行:如果 JSON 解析失败,我们会记录错误;
  • 第 26–28 行:如果发现获取到的对象不是列表或是一个空列表,则记录错误;
  • 第 29–38 行:若成功获取列表,则验证其确实为字典列表;
  • 第 40–43 行:若发生错误,则在此终止并向客户端发送错误响应;
  • 第 45–69 行:现在检查每个字典:
    • 它们必须包含键 [married, children, salary]
    • 它们必须允许我们构建一个有效的 [TaxPayer] 对象;
  • 第 65–69 行:如果在字典中检测到错误,则将其添加到该字典中,键名为 ‘error’;
  • 第 72–75 行:包含错误的字典已被收集到列表 [list_errors] 中。如果该列表不为空,则将其作为错误响应发送给客户端;
  • 第 77 行:此时,我们知道可以从客户端发送的请求正文中创建一个 [TaxPayer] 类型的对象列表;
  • 第 84–91 行:我们处理收到的字典列表;
  • 第 86 行:从字典中创建一个 [TaxPayer] 对象;
  • 第 89 行:为该 [TaxPayer] 计算税款;
  • 第 91 行:我们知道 [taxpayer] 已因税额计算而发生变化。将其转换为字典并添加到结果列表中;
  • 第 93 行:将该结果列表发送给客户端;

28.2.4. 服务器测试

我们将使用 Postman 客户端测试服务器:

  • 启动Web服务器、数据库管理系统(DBMS)以及邮件服务器[hMailServer]
  • 启动 Postman 客户端及其控制台(Ctrl-Alt-C);

Image

  • [1] 中:发送一个 [POST] 请求;
  • [2] 中:服务器的 URL;
  • [3] 中:HTTP 请求的正文;
  • [5] 中:我们指定该请求体应作为 JSON 字符串发送;
  • [4] 中:切换到 [raw] 模式以便能够复制并粘贴 JSON 字符串;
  • [6] 中:粘贴从不同版本的 [results.json] 文件中获取的 JSON 字符串。然后,对于每位纳税人,仅保留 [married, salary, children] 这三个属性;

Image

  • [7] 中,我们查看 Postman 客户端将发送给服务器的 HTTP 头部;
  • [8] 中,我们看到它将发送一个 [Content-Type] 头部,表明请求包含一个 JSON 编码的正文。这是由于之前在 [5] 中做出的选择所致;

Image

  • [9-12] 中:我们在请求中包含了服务器所需的凭据;

我们发送此请求。服务器的响应如下:

Image

  • [3] 中,我们收到了 JSON 数据;
  • [4] 中,纳税人的税款;

让我们查看在 Postman 控制台(Ctrl-Alt-C)中发生的客户端/服务器对话:

Postman客户端发送了以下文本:

POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000
  },
  {
    "marié": "non",
    "enfants": 2,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 5,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 30000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 200000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 200000
  }
]
  • 第 1 行:向服务器发送的 POST 请求;
  • 第 2 行:HTTP 身份验证头;
  • 第 3 行:客户端告知服务器其将发送一个 JSON 字符串,且该字符串长度为 824 字节(第 11 行);
  • 第 13–69 行:请求的 JSON 主体;

服务器返回了以下文本:


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: Tue, 28 Jul 2020 07:16:34 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}, {"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}, {"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}, {"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}, {"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}]}}
  • 第 1 行:请求成功;
  • 第2行:服务器响应正文是一个JSON字符串。其长度为1461字节(第3行);
  • 第 7 行:服务器的 JSON 响应;

现在让我们测试一些错误情况。

情况 1:我们发送任意内容


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3
 
abc
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT
 
{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
  • 第 13 行:发送了字符串 [abc],这并非有效的 JSON 字符串(第 3 行);
  • 第 15 行:服务器返回 400 错误代码;
  • 第 21 行:服务器的 JSON 响应;

情况 2:让我们发送一个有效的 JSON 字符串,但该字符串不是列表


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17
 
{"att1":"value1"}
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT
 
{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}

情况 3:让我们发送一个 JSON 字符串,该字符串是一个列表,且其元素并非全是字典


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7
 
[0,1,2]
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT
 
{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}

案例 4:让我们发送一个包含字典的列表,其中有一个字典的键不正确


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19
 
[{"att1":"value1"}]
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT
 
{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}

案例 5:让我们发送一个包含字典的列表,其中有一个字典缺少键:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18
 
[{"marié":"oui"}]
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT
 
{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}

案例 6:让我们发送一组字典列表,其中一个字典包含正确的键,但有些字典的值不正确:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46
 
[{"marié":"x", "enfants":"x", "salaire":"x"}]
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT
 
{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}

28.3. Web 客户端

Image

文件 [http-clients/05](版本 10)最初是通过复制文件 [http-clients/02](版本 7)获得的。随后对其进行了修改。

28.3.1. [dao] 层

[dao] 层由以下 [ImpôtsDaoWithHttpClient] 类实现:

#  imports

import requests
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    #  manufacturer
    def __init__(self, config: dict):
        

    #  unused method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        

    #  bulk tax calculation
    def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
        #  we let the exceptions rise

        #  transform taxpayers into a list of dictionaries
        #  we keep only the properties [married, children, salary]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        #  server connection
        config_server = self.__config_server
        if config_server['authBasic']:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
                                     auth=(config_server["user"]["login"],
                                           config_server["user"]["password"]))
        else:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")
        #  response status code HTTP
        status_code = response.status_code
        #  we put the response jSON in a dictionary
        résultat = response.json()
        #  error if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            #  we know that the errors have been associated with the [errors] key in the response
            raise ImpôtsError(93, résultat['réponse']['erreurs'])
        #  we know that the result has been associated with the [results] key in the response
        list_dict_taxpayers2 = résultat['réponse']['results']
        #  the initial list of taxpayers is updated with the results received
        for i in range(len(taxpayers)):
            #  taxpayers[i] update
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        #  here the [taxpayers] parameter has been updated with the server results
  • 第 1–26 行:代码与第 7 版及其他版本保持一致;
  • 第 27–70 行:引入了一个新方法 [calculate_tax_in_bulk_mode],其目的是为纳税人列表计算税款;
  • 第 28 行:[taxpayers] 即为该纳税人列表;
  • 第 31–39 行:我们使用 [map] 函数将 [TaxPayer] 对象列表转换为字典列表;
  • 第 34–38 行:所使用的 lambda 函数将 [TaxPayer] 类型的对象转换为仅包含 [married, children, salary] 这三个键的 [dict] 类型字典。为此,我们使用了 [BaseEntity.asdict] 方法中的 [included_keys] 参数。 请注意,要确定应包含在 [excluded_keys, included_keys] 参数中的属性确切名称,必须使用预定义字典 [taxpayer.__dict__]
  • 第 41–48 行:连接到服务器并获取其 HTTP 响应;
  • 第 44、48 行:
    • 我们使用静态方法 [requests.post] 向服务器发送 POST 请求;
    • 名为 [json] 的参数用于指示 POST 请求主体是一个 JSON 字符串。这将产生两个结果:
      • 赋值给命名参数 [json] 的对象(在本例中为字典列表)将被转换为 JSON 字符串;
      • 请求头
Content-Type: application/json

将被包含在 POST 请求的 HTTP 头部中;

  • 第 59 行:将服务器的 JSON 响应反序列化为 [result] 字典;
  • 第 61–63 行:处理服务器发回的任何错误;
  • 第 65 行:税费计算结果存储在一个字典列表中;
  • 第 67–69 行:使用这些结果更新方法在第 28 行最初接收的纳税人列表 [taxpayers]
  • 第 70 行:此处,初始纳税人列表已根据税费计算结果进行了更新;

28.3.2. 主脚本 [main]

主脚本 [main] 的演变如下:仅修改了由客户端创建的线程所执行的函数 [thread_function]。其余代码保持不变。

#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
    #  log thread start
    thread_name = threading.current_thread().name
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  log
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
  • 第 9–10 行:此前我们使用了一个循环,依次将每位纳税人传递给 [dao.calculate_tax] 方法,而在此处,我们仅调用一次 [dao.calculate_tax_in_bulk_mode] 方法,并将所有纳税人一次性传递给该方法;

28.3.3. 客户端执行

我们将比较以下版本的执行时间:

  • 7,其中每个纳税人都是一个 HTTP 请求的对象;
  • 10(即本版本),将纳税人分组为单个 HTTP 请求;

首先是第 6 版。为了比较这两个版本,我们将服务器的 [sleep_time] 属性设置为零,以避免线程被迫等待。客户端日志如下:


2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}

2020-07-28 14:20:45.913065, Thread-3 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]

因此,客户端计算11名纳税人税款的执行时间为 [913065-811347= 101718],即约102毫秒

现在我们用版本 10(服务器 sleep_time 设为零)进行同样的操作。此时客户端日志如下:


2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"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-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables

因此,客户端计算11名纳税人税款的执行时间为 [935958-871428= 64530 ns](第8行 – 第1行),即约65毫秒。因此,此新版10相较于版本7,性能提升了约57%。

28.3.4. 客户端 [dao] 层的测试

Image

版本 10 中客户端的 [TestHttpClientDao] 测试与版本 7 中的测试非常相似:

import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax_in_bulk_mode([taxpayer])
        #  check
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    

if __name__ == '__main__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]

    #  test methods are executed
    print("tests en cours...")
    unittest.main()
  • 第 14 行:我们不再调用 [dao.calculate_tax] 方法,而是调用 [dao.calculate_tax_in_bulk_mode] 方法,并向其传递一个纳税人列表(由方括号表示);

所有测试均通过。