Skip to content

8. 应用练习 – [税费计算] 采用分层架构

在此,我们将重新审视第4.1节中描述的练习。我们将从第4.3节中描述的文本文件版本开始。为了使用对象处理这个示例,我们将采用三层架构:

  • [DAO](数据访问对象)层负责数据访问。在接下来的内容中,这些数据将首先从文本文件中获取,随后从MySQL数据库中获取;
  • [业务]层负责处理业务逻辑,在本例中即税费计算。它不处理数据。这些数据可来自两个来源:
    • 用于持久化数据的 [DAO] 层;
    • [控制台]层,用于处理用户提供的数据。
  • [控制台]层负责处理与用户的交互。

在接下来的内容中,[DAO]层和[业务]层将分别通过一个类来实现。[控制台]层将由主程序来实现。

我们将假设 [dao] 层的所有实现都提供了 getData() 方法,该方法返回一个包含三个元素(limits、coeffR、coeffN)的元组——这是计算税额所需的三个数据数组。在其他语言中,这被称为接口。接口定义了方法(此处为 getData),实现该接口的类必须具备这些方法。

[business] 层将由一个类实现,该类的构造函数将 [dao] 层的引用作为参数,以确保两层之间的通信。

8.1. [DAO] 层

我们将把应用程序所需的各个类合并到一个文件中,即 impots.py。随后,该文件中的对象将被导入到需要它们的脚本中。

我们首先从数据位于文本文件的情况开始,如第 4.3 节中的示例所示。

实现 [DAO] 层的 [ ImpotsFile] 类(impots.py)的代码如下:

#    -*- coding=utf-8 -*-

import math, sys

# --------------------------------------------------------------------------
#    exceptional owner class
class ImpotsError:
    pass

# --------------------------------------------------------------------------
class Utilitaires:
    """classe de fonctions utilitaires"""
    def cutNewLineChar(self,ligne):
        #  delete the end-of-line mark if it exists
        l=len(ligne)
        while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
            l-=1
        return(ligne[0:l])

# --------------------------------------------------------------------------
class ImpotsFile:
    def __init__(self,IMPOTS):
        #  IMPOTS: the name of the file containing data from the limit tables, coeffR, coeffN
        #  open the file
        data=open(IMPOTS,"r")
        #    a Utilities object
        u=Utilitaires()
        #  create the 3 tables - assume that the 3 rows of IMPOTS are syntactically correct
        #  -- line 1
        ligne=data.readline()
        if ligne== '':
            raise ImpotsError("La premiere ligne du fichier {0} est absente".format(IMPOTS))
        limites=u.cutNewLineChar(ligne).split(":")
        for i in range(len(limites)):
            limites[i]=int(limites[i])
        #  -- line 2
        ligne=data.readline()
        if ligne== '':
            raise ImpotsError("La deuxieme ligne du fichier {0} est absente".format(IMPOTS))
        coeffR=u.cutNewLineChar(ligne).split(":")
        for i in range(len(coeffR)):
            coeffR[i]=float(coeffR[i])
        #  -- line 3
        ligne=data.readline()
        if ligne== '':
            raise ImpotsError("La troisieme ligne du fichier {0} est absente".format(IMPOTS))
        coeffN=u.cutNewLineChar(ligne).split(":")
        for i in range(len(coeffN)):
            coeffN[i]=float(coeffN[i])
        #    end
        (self.limites,self.coeffR,self.coeffN)=(limites,coeffR,coeffN)

    def getData(self):
        return (self.limites, self.coeffR, self.coeffN)

注释:

  • 第 7-8 行:我们定义了一个从 Exception 类派生的 ImpotsError 类。该类并未对 Exception 类进行任何扩展,我们仅将其用作自定义异常类。该类日后可能进行扩展;
  • 第 11–18 行:一个包含实用方法的类。其中,cutNewLineChar 方法用于从字符串中移除所有换行符;
  • 第 25 行:打开文件可能会抛出 IOError 异常
  • 第 32、39、46 行:抛出自定义的 ImpotsError 异常
  • 该代码与第 4.3 节示例中的代码类似。

8.2. [业务]层

实现[业务]层的[ ImpotsMetier]类(impots.py)如下:


class ImpotsMetier:
 
    # constructeur
    # on récupère un pointeur sur la couche [dao]
    def __init__(self, dao):
        self.dao=dao
 
    # calcul de l'impôt
    # --------------------------------------------------------------------------
    def calculer(self,marie,enfants,salaire):
        # marié : oui, non
        # enfants : nombre d'enfants
        # salaire : salaire annuel
 
        # on demande à la couche [dao] les données nécessaires au calcul
        (limites, coeffR, coeffN)=self.dao.getData()
 
       # nombre de parts
        marie=marie.lower()
        if(marie=="oui"):
            nbParts=float(enfants)/2+2
        else:
            nbParts=float(enfants)/2+1
        # une 1/2 part de plus si au moins 3 enfants
        if enfants>=3:
            nbParts+=0.5
        # revenu imposable
        revenuImposable=0.72*salaire
        # quotient familial
        quotient=revenuImposable/nbParts
        # est mis à la fin du tableau limites pour arrêter la boucle qui suit
        limites[len(limites)-1]=quotient
        # calcul de l'impôt
        i=0
        while quotient>limites[i] :
            i=i+1
        # du fait qu'on a placé quotient à la fin du tableau limites, la boucle précédente
        # ne peut déborder du tableau limites
        # maintenant on peut calculer l'impôt
        return math.floor(revenuImposable*(float)(coeffR[i])-nbParts*(float)(coeffN[i]))
 

注:

  • 第 5-6 行:类构造函数将 [dao] 层的引用作为参数接收;
  • 第16行:我们使用[dao]层的getData方法来获取计算税款所需的数据;
  • 其余代码与第 4.3 节示例中的内容类似。

8.3. [console] 层

实现 [console] 层的脚本(impots-03)如下:

#    -*- coding=utf-8 -*-

#    import of Impots* class module
from impots import *

#    ------------------------------------------------ main
#    definition of constants
DATA="data.txt"
RESULTATS="resultats.txt"
IMPOTS="impots.txt"

#  the data required to calculate the tax has been placed in the IMPOTS file
#  one line per table in the form
#    val1:val2:val3,...

#    instantiation layer [metier]
try:
    metier=ImpotsMetier(ImpotsFile(IMPOTS))
except (IOError, ImpotsError) as infos:
    print ("Une erreur s'est produite : {0}".format(infos))
    sys.exit()

#    reading data
try:
    data=open(DATA,"r")
except:
    print "Impossible d'ouvrir en lecture le fichier des donnees [DATA]"
    sys.exit()

#    open results file
try:
    resultats=open(RESULTATS,"w")
except:  
    print "Impossible de creer le fichier des résultats [RESULTATS]"
    sys.exit()

#    utilities
u=Utilitaires()

#  the current line of the data file is used
ligne=data.readline()
while(ligne != ''):
    #  remove any end-of-line marker
    ligne=u.cutNewLineChar(ligne)
    #  we retrieve the 3 fields married:children:salary which form the line
    (marie,enfants,salaire)=ligne.split(",")
    enfants=int(enfants)
    salaire=int(salaire)
    #  tax calculation
    impot=metier.calculer(marie,enfants,salaire)
    #  enter the result
    resultats.write("{0}:{1}:{2}:{3}\n".format(marie,enfants,salaire,impot))
    #  a new line is read
    ligne=data.readline()
#    close files
data.close()
resultats.close()

注:

  • 第 4 行:我们从 impots.py 文件中导入所有对象,该文件包含类定义。完成此操作后,我们可以像这些对象与脚本位于同一文件中一样使用它们;
  • 第 17–21 行:我们分别实例化 [dao] 层和 [business] 层,并处理可能出现的异常;
  • 第 18 行:我们先实例化 [dao] 层,然后实例化 [business] 层。我们将 [business] 层的引用存储起来;
  • 第 19 行:我们处理可能发生的两种异常;
  • 其余代码与第 4.3 节示例中的内容类似。

8.4. 结果

与使用数组和文件的版本中已获得的结果相同。

数据文件 impots.txt

12620:13190:15640:24740:31810:39970:48360:55790:92970:127860:151250:172040:195000:0
0:0.05:0.1:0.15:0.2:0.25:0.3:0.35:0.4:0.45:0.5:0.55:0.6:0.65
0:631:1290.5:2072.5:3309.5:4900:6898.5:9316.5:12106:16754.5:23147.5:30710:39312:49062

数据文件 data.txt

oui,2,200000
non,2,200000
oui,3,200000
non,3,200000
oui,5,50000
non,0,3000000

包含结果的 results.txt 文件:

oui:2:200000:22504.0
non:2:200000:33388.0
oui:3:200000:16400.0
non:3:200000:22504.0
oui:5:50000:0.0
non:0:3000000:1354938.0