Skip to content

15. 应用练习 - 第4版

Image

在此,我们将重新审视|3版|章节中描述的练习,并使用类和接口来实现它。我们将编写两个应用程序:

应用程序 1 将如下所示:

Image

一个主脚本 [main] 将实例化 [DAO] 层和 [business] 层:

  • [DAO] 层将负责管理存储在文本文件中的数据,以及后续存储在数据库中的数据;
  • [业务]层将负责计算税款;

在此应用程序中,不会有用户输入:纳税人数据将来自一个文本文件,该文件的名称将传递给 [main] 模块

在应用程序 2 中,用户将通过键盘输入纳税人数据。此时,架构将演变为如下形式:

Image

  • [DAO](数据访问对象)层负责处理外部数据的访问
  • [业务]层负责处理业务逻辑,本例中即税费计算。该层不直接处理数据。这些数据可来自两个来源:
    • 持久化数据来自 [DAO] 层;
    • [UI] 层用于处理用户提供的数据。
  • [UI](用户界面)层负责处理与用户的交互;
  • [main] 充当协调者;

在下文中,[dao][business][ui] 层将分别通过一个类来实现。[business][dao] 层在两个应用程序中是相同的。这就是为什么它们被合并为应用程序练习的单一版本。

15.1. 版本 4 – 应用程序 1

版本 4 用于计算存储在文本文件中的一组纳税人的税款。其架构如下:

Image

15.1.1. 实体

Image

实体是数据类。其作用是封装数据,并提供用于验证数据有效性的getter/setter方法。实体在各层之间进行传递。单个实体可以在[ui]层与[dao]层之间双向传递。

15.1.1.1. [ImpôtsError] 类

我们将使用一个自定义异常类:

1
2
3
4
5
6
7
# -------------------------------
#  exceptional class
from MyException import MyException


class ImpôtsError(MyException):
    pass

一旦 [business][DAO] 层遇到问题,就会抛出此异常。它继承自 [MyException] 类。因此,其使用方式如下:[raise ImpôtsError(error_code, error_message)]

15.1.1.2. [AdminData] 类

[AdminData] 类封装了税务计算中使用的常量:

from BaseEntity import BaseEntity


#  tax administration data
class AdminData(BaseEntity):
    #  keys excluded from class state
    excluded_keys = []

    #  auroralized keys
    @staticmethod
    def get_allowed_keys() -> list:
        return [
            "limites",
            "coeffr",
            "coeffn",
            "plafond_qf_demi_part",
            "plafond_revenus_celibataire_pour_reduction",
            "plafond_revenus_couple_pour_reduction",
            "valeur_reduc_demi_part",
            "plafond_decote_celibataire",
            "plafond_decote_couple",
            "plafond_impot_couple_pour_decote",
            "plafond_impot_celibataire_pour_decote",
            "abattement_dixpourcent_max",
            "abattement_dixpourcent_min"
        ]
  • 第 5 行:[AdminData] 类继承了 |BaseEntity| 部分中描述的 [BaseEntity] 类。请注意,继承 [BaseEntity] 类的类必须定义:
    • 一个类属性 [excluded_keys](第 7 行),用于列出在将对象转换为字典时被排除的属性;
    • 一个静态方法 [get_allowed_keys](第 10–26 行),用于返回在用字典初始化对象时被接受的属性列表;

我们没有使用设置器来验证用于初始化 [AdminData] 对象的数据。这是因为该对象是唯一的且由配置定义,因此不太可能包含错误。

15.1.1.3. [TaxPayer] 类

[TaxPayer] 类将用于建模纳税人:

#  imports
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


#  a taxpayer
class TaxPayer(BaseEntity):
    #  models a taxpayer
    #  id: identifier
    #  married: yes / no
    #  children: number of children
    #  salary: annual salary
    #  tax: amount of tax payable
    #  surcôte: additional tax to pay
    #  discount: discount on tax payable
    #  reduction: reduction in tax payable
    #  rate: the taxpayer's tax rate

    #  keys excluded from class state
    excluded_keys = []

    #  auroralized keys
    @staticmethod
    def get_allowed_keys() -> list:
        return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']

    #  properties
    @property
    def marié(self) -> str:
        return self.__marié

    @property
    def enfants(self) -> int:
        return self.__enfants

    @property
    def salaire(self) -> int:
        return self.__salaire

    @property
    def impôt(self) -> int:
        return self.__impôt

    @property
    def surcôte(self) -> int:
        return self.__surcôte

    @property
    def décôte(self) -> int:
        return self.__décôte

    @property
    def réduction(self) -> int:
        return self.__réduction

    @property
    def taux(self) -> float:
        return self.__taux

    #  setters
    @marié.setter
    def marié(self, marié: str):
        ok = isinstance(marié, str)
        if ok:
            marié = marié.strip().lower()
            ok = marié == "oui" or marié == "non"
        if ok:
            self.__marié = marié
        else:
            raise ImpôtsError(31, f"l'attribut marié [{marié}] doit avoir l'une des valeurs oui / non")

    @enfants.setter
    def enfants(self, enfants):
        #  children must be an integer >=0
        try:
            enfants = int(enfants)
            erreur = enfants < 0
        except:
            erreur = True
        if not erreur:
            self.__enfants = enfants
        else:
            raise ImpôtsError(32, f"L'attribut enfants [{enfants}] doit être un entier >=0")

    @salaire.setter
    def salaire(self, salaire):
        #  salary must be an integer >=0
        try:
            salaire = int(salaire)
            erreur = salaire < 0
        except:
            erreur = True
        if not erreur:
            self.__salaire = salaire
        else:
            raise ImpôtsError(33, f"L'attribut salaire [{salaire}] doit être un entier >=0")

    @impôt.setter
    def impôt(self, impôt):
        #  tax must be an integer >=0
        try:
            impôt = int(impôt)
            erreur = impôt < 0
        except:
            erreur = True
        if not erreur:
            self.__impôt = impôt
        else:
            raise ImpôtsError(34, f"L'attribut impôt [{impôt}] doit être un nombre >=0")

    @décôte.setter
    def décôte(self, décôte):
        #  discount must be an integer >=0
        try:
            décôte = int(décôte)
            erreur = décôte < 0
        except:
            erreur = True
        if not erreur:
            self.__décôte = décôte
        else:
            raise ImpôtsError(35, f"L'attribut décôte [{décôte}] doit être un nombre >=0")

    @surcôte.setter
    def surcôte(self, surcôte):
        #  surcharge must be an integer >=0
        try:
            surcôte = int(surcôte)
            erreur = surcôte < 0
        except:
            erreur = True
        if not erreur:
            self.__surcôte = surcôte
        else:
            raise ImpôtsError(36, f"L'attribut surcôte [{surcôte}] doit être un nombre >=0")

    @réduction.setter
    def réduction(self, réduction):
        #  surcharge must be an integer >=0
        try:
            réduction = int(réduction)
            erreur = réduction < 0
        except:
            erreur = True
        if not erreur:
            self.__réduction = réduction
        else:
            raise ImpôtsError(37, f"L'attribut réduction [{réduction}] doit être un nombre >=0")

    @taux.setter
    def taux(self, taux):
        #  rate must be real >=0
        try:
            taux = float(taux)
            erreur = taux < 0
        except:
            erreur = True
        if not erreur:
            self.__taux = taux
        else:
            raise ImpôtsError(38, f"L'attribut taux [{taux}] doit être un nombre >=0")

  • [TaxPayer] 类封装了一个纳税人;
  • 第 7 行:[TaxPayer] 类继承自 [BaseEntity] 类。因此它具有一个标识符 [id]
  • 第 20 行:[AdminData] 对象的状态中不排除任何属性;
  • 第 22–25 行:类属性。这些在第 9–17 行中进行了说明;
  • 第 27–58 行:类属性的获取器;
  • 第 60–161 行:类属性的设置器。请注意,与简单的字典相比,类封装数据的优势在于,类可以通过其设置器验证属性的有效性;

15.1.2. [dao] 层

Image

我们将把该层的实现类归类到 [services] 文件夹中。这些类将实现 [interfaces] 文件夹中定义的接口。

Image

15.1.2.1. [InterfaceImpôtsDao] 接口

[dao] 层将实现以下 [InterfaceImpôtsDao] 接口(文件 InterfaceImpôtsDao.py):

#  imports
from abc import ABC, abstractmethod


#  interface IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    #  list of tax brackets
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    #  list of taxpayer data
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    #  entering tax calculation results
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

该接口定义了三个方法:

  • [get_admindata]:用于检索税率表的方法。 请注意,这里并未提供获取该数据的具体方式。后续实现中,该数据将先从文本文件中读取,再从数据库中读取。具体采用哪种数据存储方式,将由实现该接口的类自行适配。因此,我们将分别创建一个从文本文件读取税率表的类,以及一个从数据库读取税率表的类。这两个类都将实现 [get_admindata] 方法;
  • [get_taxpayers_data]:用于检索纳税人数据的方法。同样,我们未指定数据存储位置。此处仅处理数据存储在文本文件中的情况;
  • [write_taxpayers_results]:用于持久化税款计算结果的方法。我们同样不指定存储位置,仅处理将结果持久化到文本文件的情况。参数 [taxpayers_results] 将是待持久化的结果列表;

15.1.2.2. [AbstractImpôtsDao] 类

[dao] 层将由两个类实现:

  • 一个类负责从文本文件中读取数据(纳税人、计算结果、税率表);
  • 另一个将从文本文件中读取数据(纳税人、计算结果),并从数据库中读取税率表;

这两个类仅在处理税率表的方式上有所不同。纳税人数据和税额计算结果将采用相同的管理方式。因此,我们将通过父类 [AbstractImpôtsDao] 来管理它们。税率表的具体处理将由两个子类负责:

  • [ImpôtsDaoWithAdminDataInJsonFile] 类将从 JSON 格式的文本文件中获取税率表;
  • [ImpôtsDaoWithAdminDataInDatabase] 类将从数据库中获取税率表;

父类 [AbstractImpôtsDao] 的定义如下:

#  imports
import codecs
import json
from abc import abstractmethod

from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDao import InterfaceImpôtsDao
from TaxPayer import TaxPayer


#  base class for the [dao] layer
class AbstractImpôtsDao(InterfaceImpôtsDao):
    #  taxpayers and their taxes will be stored in text files
    #  manufacturer
    def __init__(self, config: dict):
        #  config[taxpayersFilename]: name of the taxpayer text file
        #  config[resultsFilename]: the name of the jSON results file
        #  config[errorsFilename]: the name of the error file

        #  save parameters
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    #  interface IImpôtsDao
    # ------------------

    #  list of taxpayer data
    def get_taxpayers_data(self) -> dict:
        

    #  tax entry for taxpayers
    def write_taxpayers_results(self, taxpayers: list):
        

    #  reading tax brackets
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • 第 13 行:类 [AbstractImpôtsDao] 实现了接口 [InterfaceImpôtsDao]。因此,它包含该接口的三个方法:
    • [get_taxpayers_data]:第 31 行;
    • [write_taxpayers_results]:第 35 行;
    • [get_admindata]:第 40 行。该方法不会由 [AbstractImpôtsDao] 类实现,因此被声明为抽象方法(第 39 行);
  • 第 16 行:构造函数接收一个 [config] 字典,其中包含以下信息:
    • [taxpayersFilename]:包含纳税人数据的文本文件名称;
    • [resultsFilename]:用于存储处理结果的文本文件名;
    • [errorsFilename]:记录处理 [taxpayersFilename] 文件时遇到的错误的文本文件名;

[get_taxpayers_data] 方法如下:

    #  list of taxpayer data
    def get_taxpayers_data(self) -> dict:
        #  initializations
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            #  open data file
            datafile = open(self.taxpayers_filename, "r")
            #  the current line of the
            ligne = datafile.readline()
            #  line no
            numligne = 0
            while ligne != '':
                #  a + line
                numligne += 1
                #  remove the whites
                ligne = ligne.strip()
                #  ignore empty lines and comments
                if ligne != "" and ligne[0] != "#":
                    try:
                        #  we retrieve the 4 fields id,married,children,salary which form the taxpayer line
                        (id, marié, enfants, salaire) = ligne.split(",")
                        #  create a new TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        #  we note the error
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                #  a new taxpayer line reads
                ligne = datafile.readline()
            #  record errors if any
            if erreurs:
                text = f"Analyse du fichier {self.taxpayers_filename}\n\n" + "\n".join(erreurs)
                with codecs.open(self.errors_filename, "w", "utf-8") as fd:
                    fd.write(text)
            #  we return the result
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            #  throw a ImpôtsError exception
            raise ImpôtsError(11, f"{erreur}")
        finally:
            #  close the file
            if datafile:
                datafile.close()
  • 第 4 行:纳税人数据(已婚、子女、工资)将被放入一个 [TaxPayer] 类型的对象列表中;
  • 第8-9行:我们打开纳税人文本文件进行读取。其内容格式如下:
# données valides : id, marié, enfants, salaire
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# on peut avoir des lignes vides

# on crée des lignes erronées
# pas assez de valeurs
11,12
# trop de valeurs
12,oui,3,200000, x, y
# des valeurs erronées
x,x,x,x

与以前的版本相比:

  • [taxpayersFilename] 文件中的每一行都以纳税人 ID 开头,即一个数字;
  • 允许包含注释和空行;
  • 我们将处理错误。因此,第 17、19 和 21 行必须被标记为无效。错误将记录在单独的文件中;

让我们继续审查代码:

  • 第 4 行:将文本文件中的数据传输到 [taxPayersData] 列表中;
  • 第14–31行:逐行读取纳税人文件;
  • 第 14 行:当读取到空行(没有任何内容,甚至没有换行符 \r\n)时,即表示到达文件末尾;
  • 第 20 行:忽略空行和注释。如果移除文本前后空格后,第一字符是 # 字符,则该行被视为注释;
  • 第 24 行:有效行由四个以逗号分隔的字段组成。这些字段被提取出来。如果分配的数据点数不恰好为四个,则将数据赋值给四元元组的操作将失败;
  • 第 25 行:如果检索到的四个字段 [id, married, children, salary] 中任何一个无效,则 [BaseEntity.fromdict] 方法将引发 [MyException] 异常;
  • 第 25–26 行:将一个 [TaxPayer] 对象添加到纳税人列表 [taxpayers_data] 中;
  • 第 27–29 行:将所有错误收集到列表 [errors] 中。该列表在第 6 行创建;
  • 第 33–36 行:将遇到的错误列表保存到文本文件 [errorsFilename] 中。错误分为两类:
    • 行中预期字段的数量不正确;
    • 行中的信息不正确,导致无法构建 [TaxPayer] 对象;
  • 第 39–41 行:通过将任何错误(BaseException)封装为 [TaxPayerError] 类型来捕获并传递该错误;
  • 第 42–45 行:无论操作是否成功,若纳税人文本文件已被打开,则将其关闭;

[write_taxpayers_results] 方法必须生成格式如下所示的 JSON 文件:


[
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 2,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },
  {
    "id": 3,
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000,
    "impôt": 0,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 720,
    "réduction": 0
  },

]

[write_taxpayers_results] 方法如下:

    #  tax entry for taxpayers
    def write_taxpayers_results(self, taxpayers: list):
        #  writing results to a jSON file
        #  taxpayers: list of objects of type TaxPayer
        #  (id, married, children, salary, tax, surcharge, discount, reduction, rate)
        #  the [taxpayers] list is saved in text file [self.taxpayers_results_filename]
        file = None
        try:
            #  opening the results file
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            #  creation of the list to be serialized in jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            #  serialization jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            #  restart the error with another type
            raise ImpôtsError(12, f"{erreur}")
        finally:
            #  close the file if it has been opened
            if file:
                file.close()
  • 第 2 行:该方法接收一个纳税人列表 [taxpayers],并需将其以 JSON 格式保存到文本文件 [self.taxpayers_results_filename] 中;
  • 第 10 行:创建 UTF-8 格式的结果文件;
  • 第 12 行:这里我们引入了 [map] 函数,其语法为 [map (function, list1)]。该 [function] 会被应用于 [list1] 的每个元素,并生成一个新元素,该元素会被添加到列表 [list2] 中。最后,对于每个 i:

liste2[i]=fonction(liste1[i])

此处,[list1] 是列表 [taxPayers],即 [TaxPayer] 类型的对象列表。 函数 [function] 在此以所谓的 [lambda] 函数形式表示,它描述了对列表 [taxpayers] 中的元素 [taxpayer] 所施加的转换:每个 [taxpayer] 元素都被替换为其字典 [taxpayer.asdict()]。最后,生成的列表 [list2] 就是 [taxpayers] 列表中各元素的字典列表;

  • 第 12 行:[map] 函数返回的结果并非列表 [list2],而是一个类型为 [map] 的对象。要获得 [list2],必须使用表达式 [list(mapping)](第 14 行);
  • 第 14 行:将列表 [list2] 以 JSON 格式保存到文件 [self.taxpayers_results_filename] 中;
  • 第 15–17 行:捕获任何类型的异常,并将其包装在 [ImpôtsError] 中,然后重新抛出(第 17 行);
  • 第 19–21 行:无论操作是否成功,如果结果文件已被打开,则将其关闭;

15.1.2.3. 类 [ImpôtsDaoWithAdminDataInJsonFile]

[ImpôtsDaoWithAdminDataInJsonFile] 类将继承自 [AbstractImpôtsDao] 类,并实现其父类未实现的 [getAdminData] 方法。该类将从 JSON 文件中检索税务管理数据:


{
    "limites": [9964, 27519, 73779, 156244, 0],
    "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
    "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
    "plafond_qf_demi_part": 1551,
    "plafond_revenus_celibataire_pour_reduction": 21037,
    "plafond_revenus_couple_pour_reduction": 42074,
    "valeur_reduc_demi_part": 3797,
    "plafond_decote_celibataire": 1196,
    "plafond_decote_couple": 1970,
    "plafond_impot_couple_pour_decote": 2627,
    "plafond_impot_celibataire_pour_decote": 1595,
    "abattement_dixpourcent_max": 12502,
    "abattement_dixpourcent_min": 437
}

[ImpôtsDaoWithAdminDataInJsonFile] 类的定义如下:

#  imports
import codecs
import json

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError


#  an implementation of the [dao] layer, where tax administration data is stored in a jSON file
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    #  manufacturer
    def __init__(self, config: dict):
        #  config[admindataFilename]: name of jSON file containing tax administration data
        #  config[taxpayersFilename]: name of the taxpayer text file
        #  config[resultsFilename]: the name of the jSON results file
        #  config[errorsFilename]: the name of the error file

        #  parent class initialization
        AbstractImpôtsDao.__init__(self, config)
        #  reading tax administration data
        file = None
        try:
            #  open the jSON tax data file in read mode
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            #  transfer the contents of file jSON to object [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            #  we relaunch the error as type [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            #  close the file if it has been opened
            if file:
                file.close()

    # -------------
    #  interface
    # -------------

    #  data recovery from tax authorities
    #  the method returns an object [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • 第 11 行:[ImpôtsDaoWithAdminDataInJsonFile] 类继承自 [AbstractImpôtsDao] 类。因此,它实现了 [InterfaceImpôtsDao] 接口;
  • 第 13 行:构造函数接收一个字典作为参数,该字典包含第 14–17 行中的信息;
  • 第 20 行:初始化父类;
  • 第 24 行:打开包含税务管理数据的 JSON 文件;
  • 第 25 行:打开包含税务机关数据的 UTF-8 文件;
  • 第 27 行:读取文件内容并将其放入类型为 [AdminData] [self.admindata] 对象中。JSON 文件中的键必须与 [AdminData] 对象支持的属性匹配;否则,[fromdict] 方法将抛出异常;
  • 第 28–30 行:异常处理。任何可能发生的异常在重新抛出前都会被包装为 [ImpôtsError] 类型;
  • 第 32–34 行:若文件已被打开,则将其关闭;
  • 第 42–43 行:实现 [InterfaceImpôtsDao] 接口的 [get_admindata] 方法;

15.1.3. [business] 层

Image

15.1.3.1. [InterfaceImpôtsMétier] 接口

[业务]层的接口如下:

#  imports
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


#  interface IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    #  tax calculation for 1 taxpayer
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • [BusinessTaxInterface] 接口定义了一个方法:
  • 第 12 行:[calculate_tax] 方法用于计算单个纳税人 [taxpayer] 的税款。[admindata] 是封装税务管理数据的 [AdminData] 对象;
  • 第 12 行:[calculate_tax] 方法不返回结果。获取的数据(税款、附加费、折扣、减免、税率)包含在 [taxpayer] 参数中:调用前,这些属性为空;调用后,它们已被初始化;

15.1.3.2. [BusinessTaxes] 类

[ImpôtsMétier] 类如下所示实现了 [InterfaceImpôtsMétier] 接口:

Image

该类的方法源自 [impôts_module_02] 模块,详见 |[impôts.v02.modules.impôts_module_02] 模块| 部分。我们已将方法参数限制为仅两个:

  • taxpayer(id, married, children, salary, tax, discount, surcharge, reduction, rate):表示纳税人及其税款的对象;
  • admindata:封装税务管理数据的对象;

我们将通过一个方法演示所做的更改;

    #  tax calculation - phase 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        #  taxpayer(id, married, children, salary, tax, discount, surcharge, reduction, rate)
        #  admindata: tax administration data

        #  tax calculation with children
        self.calculate_tax_2(taxpayer, admindata)
        #  results are in taxpayer
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        #  tax calculation without children
        if taxpayer.enfants != 0:
            #  tax calculation for the same taxpayer without children
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            #  the results are in taxpayer2
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            #  application of the family allowance ceiling
            if taxpayer.enfants < 3:
                #  PLAFOND_QF_DEMI_PART euros for the first 2 children
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                #  PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            #  if the taxpayer has no children, then impot2=impot1
            impot2 = impot1

        #  we take the highest tax with the corresponding rate and surcharge
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        #  partial results
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        #  calculation of any discount
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        #  calculation of any tax reduction
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        #  result
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • 第 3 行:[calculate_tax] 方法是 [InterfaceImpôtsMétier] 接口中的唯一方法。它接受两个参数:
    • [tapPayer]:正在为其计算税款的纳税人;
    • [admindata]:封装税务管理数据的对象;
    • 计算结果封装在 [taxpayer] 参数中(第 40–50 行)。因此,该对象的内容在调用该方法前后并不相同;

15.1.4. 针对 [dao] 和 [business] 层的测试

Image

  • [TestDaoMétier] 是用于测试 [dao] [business] 层的 UnitTest 类;
  • [config] 是测试配置文件;

[config] 的配置如下:

def configure():
    import os

    #  step 1 ------
    #  path python configuration

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    #  absolute application dependencies
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config = {
        #  absolute paths for application files
        "admindataFilename": f"{script_dir}/../data/input/admindata.json"
    }

    #  instantiation of application layers
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    #  put the layer instances in the config
    config["dao"] = dao
    config["métier"] = métier

    #  return the config
    return config
  • 第 4–23 行:我们为测试配置 Python 路径;
  • 第 32–41 行:实例化 [dao] [business] 层。将它们的引用存储在 [config] 字典中;
  • 第 44 行:返回此字典;

[TestDaoMétier] 测试类如下:

import unittest


def get_config() -> dict:
    #  application configuration
    import config
    #  we return the configuration
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    #  executed before each test_ method
    def setUp(self) -> None:
        #  retrieve the test configuration
        config = get_config()
        #  memorize some information
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

    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})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  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)

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

        #  { 'married': 'yes', 'children': 2, 'salary': 50000,
        #  tax': 1384, 'surcôte': 0, 'décôte': 384, 'réduction': 347, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 1384, delta=1)
        self.assertAlmostEqual(taxpayer.décôte, 384, delta=1)
        self.assertAlmostEqual(taxpayer.réduction, 347, delta=1)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

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

        #  { 'married': 'yes', 'children': 3, 'salary': 50000,
        #  tax': 0, 'surcôte': 0, 'décôte': 720, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertEqual(taxpayer.impôt, 0)
        self.assertAlmostEqual(taxpayer.décôte, 720, delta=1)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

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

        #  { 'married': 'no', 'children': 2, 'salary': 100000,
        #  tax': 19884, 'surcôte': 4480, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 19884, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 4480, delta=1)

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

        #  { 'married': 'no', 'children': 3, 'salary': 100000,
        #  tax': 16782, 'surcôte': 7176, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 16782, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7176, delta=1)

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

        #  { 'married': 'yes', 'children': 3, 'salary': 100000,
        #  tax': 9200, 'surcôte': 2180, 'décôte': 0, 'réduction': 0, 'taux': 0.3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 9200, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.3, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 2180, delta=1)

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

        #  { 'married': 'yes', 'children': 5, 'salary': 100000,
        #  tax': 4230, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 4230, 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)

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

        #  { 'married': 'no', 'children': 0, 'salary': 100000,
        #  tax': 22986, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 22986, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

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

        #  { 'married': 'yes', 'children': 2, 'salary': 30000,
        #  tax': 0, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertEqual(taxpayer.impôt, 0)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.0, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

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

        #  { 'married': 'no', 'children': 0, 'salary': 200000,
        #  tax': 64210, 'surcôte': 7498, 'décôte': 0, 'réduction': 0, 'taux': 0.45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 64210, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.45, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7498, delta=1)

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

        #  { 'married': 'yes', 'children': 3, 'salary': 200000,
        #  tax': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':
    unittest.main()

注释

  • 第 11 行:测试类继承自 [unittest.TestCase] 类;
  • 第 13–19 行:在 UnitTest 中,[setUp] 方法会在每个 [test_] 方法之前执行;
  • 第 16 行:从前面提到的 [config] 脚本中获取配置;
  • 第 18 行:存储对 [business] 层的引用;
  • 第 19 行:从 [DAO] 层请求封装税务管理数据的 [AdminData] 对象并将其存储;
  • 第 21–173 行:11 个测试,其结果已在 2019 年官方税务网站 |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm| 上验证;
  • 第 21–33 行:所有测试均基于相同的模板构建;
  • 第 22 行:导入 [TaxPayer] 类;
  • 第 24 行:待测试的纳税人;
  • 第25行:预期结果;
  • 第 26 行:创建纳税人的 [TaxPayer] 对象;
  • 第 27 行:计算其税款。结果存储在 [taxpayer] 中;
  • 第29–33行:验证所得结果;
  • 第29行:我们将税额四舍五入至最接近的欧元。测试确实表明,本文档中该算法得出的结果可能与官方数据相差最多1欧元;

运行测试得出以下结果:

Image

Testing started at 16:08 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\tests



Ran 11 tests in 0.055s

OK

Process finished with exit code 0

15.1.5. 主脚本

Image

主脚本由以下 [config] 脚本配置:

def configure():
    import os

    #  step 1 ------
    #  path python configuration

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    #  application dependencies
    absolute_dependencies = [
        #  local dependencies
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config = {
        #  absolute paths for application files
        "taxpayersFilename": f"{script_dir}/../../data/input/taxpayersdata.txt",
        "resultsFilename": f"{script_dir}/../../data/output/résultats.json",
        "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
        "errorsFilename": f"{script_dir}/../../data/output/errors.txt"
    }

    #  instantiation of application layers
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    #  put the layer instances in the config
    config["dao"] = dao
    config["métier"] = métier

    #  return the config
    return config

这与用于测试 [business][dao] 层的配置类似。

主脚本 [main.py] 如下:

#  configure the application
import config

config = config.configure()

#  imports
from ImpôtsError import ImpôtsError

#  retrieve application layers (already instantiated)
dao = config["dao"]
métier = config["métier"]

try:
    #  tax bracket recovery
    admindata = dao.get_admindata()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    for taxpayer in taxpayers:
        #  taxpayer is both an input and output parameter
        #  taxpayer will be modified
        métier.calculate_tax(taxpayer, admindata)
    #  writing results to a text file
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  completed
    print("Travail terminé...")

注释

  • 第 2–4 行:我们获取应用程序配置。我们还知道应用程序的 Python 路径已构建完成;
  • 第 9–11 行:我们获取 [business] [DAO] 层的引用;
  • 第 15 行:我们从税务管理局获取数据;
  • 第 17 行:我们获取需要计算税款的纳税人列表;
  • 第 19–20 行:如果该列表为空,则抛出异常;
  • 第 22–25 行:使用 [business] 层为各个 [taxpayer] 对象计算税款;
  • 第 27 行:[taxpayers] 现已成为一个 [TaxPayer] 对象列表,其中属性(tax、discount、surcharge、reduction、rate)已赋值。该列表被写入 JSON 文件;
  • 第 28–30 行:捕获任何潜在错误;
  • 第 31–33 行:在所有情况下均执行;

运行该脚本得出的结果与之前版本相同。纳税人错误文件是本版本的新功能。运行 [main] 脚本后,其内容如下:


Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\main\01/../../data/input/taxpayersdata.txt
 
Ligne 17, not enough values to unpack (expected 4, got 2)
Ligne 19, too many values to unpack (expected 4)
Ligne 21, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]

出错的行如下:

# données valides : id, marié, enfants, salaire
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# on peut avoir des lignes vides

# on crée des lignes erronées
# pas assez de valeurs
11,12
# trop de valeurs
12,oui,3,200000, x, y
# des valeurs erronées
x,x,x,x

15.2. 版本 4 – 应用程序 2

在此版本中,用户通过键盘输入纳税人列表。应用程序架构如下:

Image

新增了一个模块:[ui](用户界面)层,该层将与用户进行交互。该层将包含一个接口,并由一个类来实现。

Image

15.2.1. [InterfaceImpôtsUi] 接口

#  imports
from abc import ABC, abstractmethod


#  interface InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    #  execution of the class implementing the interface
    @abstractmethod
    def run(self):
        pass

[InterfaceImpôtsUi] 接口将仅包含一个方法,即第 8 至 10 行中的方法。此处将通过控制台应用程序实现该接口,但也可以通过图形用户界面来实现。在两种实现中,传递给 [run] 方法的参数将不相同。为解决此问题,通常的做法是:

  • 不向 [run] 方法传递任何参数(或传递最少的参数);
  • 将参数传递给实现该接口的类的构造函数。这些参数在不同实现中可能有所不同。这些参数将作为类属性进行存储;
  • 确保 [run] 方法使用这些类属性(self.x);

此方法允许通过各实现类的构造函数参数来定义一个非常通用的接口。该方法已在模块化版本 #1 中使用。

15.2.2. [ImpôtsConsole] 类

[ImpôtsConsole] 类通过以下方式实现了 [InterfaceImpôtsUi] 接口:

#  imports
import re

from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer


#  layer [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    #  manufacturer
    def __init__(self, config: dict):
        #  save parameters
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        #  interactive dialogue with the user
        fini = False
        while not fini:
            #  is the taxpayer married?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            #  check validity of input
            while marié != "oui" and marié != "non" and marié != "*":
                #  error msg
                print("Tapez oui ou non ou *")
                #  question again
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            #  finished?
            if marié == "*":
                #  dialogue over
                return
            #  number of children
            enfants = input("Nombre d'enfants : ").strip()
            #  check validity of input
            if not re.match(r"^\d+$", enfants):
                #  error msg
                print("Tapez un nombre entier positif ou nul")
                #  here we go again
                enfants = input("Nombre d'enfants : ").strip()
            #  annual salary
            salaire = input("Salaire annuel : ").strip()
            #  check validity of input
            if not re.match(r"^\d+$", salaire):
                #  error msg
                print("Tapez un nombre entier positif ou nul")
                #  here we go again
                salaire = input("Salaire annuel : ").strip()
            #  tAX CALCULATION
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            #  display
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            #  next taxpayer
  • 第 9 行:[TaxConsole] 类实现了 [TaxUiInterface] 接口;
  • 第 11 行:类构造函数接收一个参数,即包含应用程序配置的 [config] 字典;
    • 第 13 行:从税务机关获取数据以计算税款;
    • 第 14 行:存储对 [business] 层的引用;
  • 第 16 行:实现接口的 [run] 方法;
  • 第 19–53 行:用户交互。这包括
    • 向纳税人询问三项信息(婚姻状况、子女情况、工资);
    • 计算其应缴税款;
    • 显示结果;
    • 当用户对第一个问题回答 * 时,对话结束;
  • 第20–27行:程序询问纳税人是否已婚,并验证回答的有效性;
  • 第29–31行:若用户对该问题回答“*”,则对话结束;
  • 第32–39行:询问纳税人有几个孩子,并验证回答的有效性;
  • 第40–47行:询问纳税人的年薪,并验证回答的有效性;
  • 第48–50行:利用这些信息,[业务]层计算纳税人的应纳税额;
  • 第52行:显示税额;

15.2.3. 主脚本

主脚本 [main] 由以下 [config] 文件配置:

def configure():
    import os

    #  step 1 ------
    #  path python configuration

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    #  application dependencies
    absolute_dependencies = [
        #  local dependencies
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config = {
        #  absolute paths for application files
        "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
    }

    #  instantiation of application layers
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    #  dao layer
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    #  business layer
    métier = ImpôtsMétier()
    #  put the layer instances in the config
    config["dao"] = dao
    config["métier"] = métier
    #  layer ui
    ui = ImpôtsConsole(config)
    config["ui"] = ui

    #  return the config
    return config

主脚本如下(main.py):

#  configure the application
import config

config = config.configure()

#  imports
from ImpôtsError import ImpôtsError

#  retrieve application layers (already instantiated)
ui = config["ui"]

#  code
try:
    #  execution of the [ui] layer
    ui.run()
except ImpôtsError as erreur:
    #  the error message is displayed
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  executed in all cases
    print("Travail terminé...")
  • 第 1-4 行:获取应用程序配置;
  • 第 10 行:获取 [ui] 层的引用;
  • 第12-21行:代码结构与前一个应用程序相同:代码包裹在try/catch块中以捕获任何潜在的异常;
  • 第 15 行:我们请求 [ui] 层执行操作:随后开始用户交互;
  • 第 16–18 行:捕获任何潜在的异常;

以下是一个执行示例:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/main/02/main.py
Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : oui
Nombre d'enfants : 3
Salaire annuel : 200000
Impôt du contribuable = {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
 
 
Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : *
Travail terminé...
 
Process finished with exit code 0