Skip to content

15. Anwendungsübung – Version 4

Image

Hier greifen wir die im Abschnitt |Version 3| beschriebene Übung wieder auf und implementieren sie nun unter Verwendung von Klassen und Schnittstellen. Wir werden zwei Anwendungen schreiben:

Anwendung 1 sieht wie folgt aus:

Image

Ein Hauptskript [main] instanziiert eine [DAO]-Schicht und eine [Business]-Schicht:

  • Die [DAO]-Schicht ist für die Verwaltung der in Textdateien und später in einer Datenbank gespeicherten Daten zuständig;
  • die [Business]-Schicht ist für die Berechnung der Steuer zuständig;

In dieser Anwendung gibt es keine Benutzereingaben: Die Steuerzahlerdaten befinden sich in einer Textdatei, deren Name an das [main]-Modul übergeben wird.

In Anwendung 2 gibt der Benutzer die Steuerzahlerdaten über die Tastatur ein. Die Architektur entwickelt sich dann wie folgt:

Image

  • Die [DAO]-Schicht (Data Access Object) verwaltet den Zugriff auf externe Daten
  • Die [Business]-Schicht übernimmt die Geschäftslogik, in diesem Fall die Steuerberechnung. Sie verarbeitet die Daten nicht. Diese Daten können aus zwei Quellen stammen:
    • der [DAO]-Schicht für persistente Daten;
    • die [UI]-Schicht für vom Benutzer bereitgestellte Daten.
  • Die [UI]-Schicht (User Interface) verwaltet die Interaktionen mit dem Benutzer;
  • [main] fungiert als Koordinator;

Im Folgenden werden die Schichten [dao], [business] und [ui] jeweils mithilfe einer Klasse implementiert. Die Schichten [business] und [dao] sind für beide Anwendungen identisch. Aus diesem Grund wurden sie in einer einzigen Version der Anwendungsübung zusammengefasst.

15.1. Version 4 – Anwendung 1

Version 4 berechnet die Steuer für eine Liste von Steuerzahlern, die in einer Textdatei gespeichert ist. Sie weist folgende Architektur auf:

Image

15.1.1. Entitäten

Image

Entitäten sind Datenklassen. Ihre Aufgabe ist es, Daten zu kapseln und Getter/Setter bereitzustellen, mit denen die Gültigkeit der Daten überprüft werden kann. Entitäten werden zwischen den Schichten ausgetauscht. Eine einzelne Entität kann von der [ui]-Schicht zur [dao]-Schicht und umgekehrt wandern.

15.1.1.1. Die Klasse [ImpôtsError]

Wir werden eine benutzerdefinierte Ausnahmeklasse verwenden:

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


class ImpôtsError(MyException):
    pass

Sobald in den Schichten [business] und [DAO] ein Problem auftritt, wird diese Ausnahme ausgelöst. Sie leitet sich von der Klasse [MyException] ab. Sie wird daher wie folgt verwendet: [raise ImpôtsError(error_code, error_message)].

15.1.1.2. Die Klasse [AdminData]

Die Klasse [AdminData] kapselt die bei Steuerberechnungen verwendeten Konstanten:

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"
        ]
  • Zeile 5: Die Klasse [AdminData] erweitert die im Abschnitt |BaseEntity| beschriebene Klasse [BaseEntity]. Beachten Sie, dass Klassen, die die Klasse [BaseEntity] erweitern, Folgendes definieren müssen:
    • ein Klassenattribut [excluded_keys] (Zeile 7), das die Eigenschaften des Objekts auflistet, die bei der Konvertierung des Objekts in ein Wörterbuch ausgeschlossen werden;
    • eine statische Methode [get_allowed_keys] (Zeilen 10–26), die die Liste der Eigenschaften zurückgibt, die akzeptiert werden, wenn das Objekt mit einem Wörterbuch initialisiert wird;

Wir haben keine Setter verwendet, um die Daten zu validieren, die zur Initialisierung eines [AdminData]-Objekts verwendet werden. Der Grund dafür ist, dass dieses Objekt eindeutig ist und durch die Konfiguration definiert wird und daher wahrscheinlich keine Fehler enthält.

15.1.1.3. Die Klasse [TaxPayer]

Die Klasse [TaxPayer] modelliert einen Steuerzahler:

#  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")

Anmerkungen:

  • Die Klasse [TaxPayer] kapselt einen Steuerzahler;
  • Zeile 7: Die Klasse [TaxPayer] leitet sich von der Klasse [BaseEntity] ab. Sie verfügt daher über einen Bezeichner [id];
  • Zeile 20: Es sind keine Eigenschaften vom Zustand eines [AdminData]-Objekts ausgeschlossen;
  • Zeilen 22–25: die Klassen-Eigenschaften. Diese werden in den Zeilen 9–17 erläutert;
  • Zeilen 27–58: Getter für die Klassenattribute;
  • Zeilen 60–161: die Setter für die Attribute der Klasse. Erinnern Sie sich daran, dass der Vorteil einer Klasse, die Daten kapseln, gegenüber einem einfachen Wörterbuch darin besteht, dass die Klasse die Gültigkeit ihrer Eigenschaften mithilfe ihrer Setter überprüfen kann;

15.1.2. Die [dao]-Schicht

Image

Wir werden die Implementierungen der Schichten in einem Ordner [services] zusammenfassen. Diese Klassen werden Schnittstellen implementieren, die im Ordner [interfaces] definiert sind.

Image

15.1.2.1. Die Schnittstelle [InterfaceImpôtsDao]

Die [dao]-Schicht wird die folgende [InterfaceImpôtsDao]-Schnittstelle implementieren (Datei 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

Die Schnittstelle definiert drei Methoden:

  • [get_admindata]: ist die Methode, die die Steuerklasse-Tabelle abruft. Beachten Sie, dass keine Informationen darüber gegeben werden, wie diese Daten zu beschaffen sind. Später werden sie zunächst in einer Textdatei und dann in einer Datenbank zu finden sein. Es liegt an den Klassen, die die Schnittstelle implementieren, sich an die jeweilige Datenspeichermethode anzupassen. Wir werden daher eine Klasse haben, um die Steuerklassen aus einer Textdatei abzurufen, und eine andere, um sie aus einer Datenbank abzurufen. Beide werden die Methode [get_admindata] implementieren;
  • [get_taxpayers_data]: ist die Methode, die Steuerzahlerdaten abruft. Auch hier geben wir nicht an, wo diese zu finden sind. Wir behandeln nur den Fall, in dem sie in einer Textdatei vorliegen;
  • [write_taxpayers_results]: ist die Methode, die die Ergebnisse der Steuerberechnung speichert. Wir geben nicht an, wo. Wir behandeln nur den Fall, in dem die Ergebnisse in einer Textdatei gespeichert werden. Der Parameter [taxpayers_results] ist die Liste der zu speichernden Ergebnisse;

15.1.2.2. Die Klasse [AbstractImpôtsDao]

Die [dao]-Schicht wird durch zwei Klassen implementiert:

  • Eine ruft die Daten (Steuerzahler, Ergebnisse, Steuerklassen) aus Textdateien ab;
  • die andere ruft Daten (Steuerzahler, Ergebnisse) aus Textdateien und Steuerklassen aus einer Datenbank ab;

Die beiden Klassen unterscheiden sich lediglich in der Art und Weise, wie sie mit Steuerklassen umgehen. Steuerzahlerdaten und Steuerberechnungsergebnisse werden auf die gleiche Weise verwaltet. Aus diesem Grund werden wir sie in einer übergeordneten Klasse [AbstractImpôtsDao] verwalten. Die spezifische Handhabung der Steuerklassen wird in zwei untergeordneten Klassen verwaltet:

  • Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] ruft die Steuerklassen aus einer Textdatei im JSON-Format ab;
  • Die Klasse [ImpôtsDaoWithAdminDataInDatabase] ruft die Steuerklassen aus einer Datenbank ab;

Die übergeordnete Klasse [AbstractImpôtsDao] sieht wie folgt aus:

#  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
  • Zeile 13: Die Klasse [AbstractImpôtsDao] implementiert die Schnittstelle [InterfaceImpôtsDao]. Daher enthält sie die drei Methoden dieser Schnittstelle:
    • [get_taxpayers_data]: Zeile 31;
    • [write_taxpayers_results]: Zeile 35;
    • [get_admindata]: Zeile 40. Diese Methode wird von der Klasse [AbstractImpôtsDao] nicht implementiert, daher wird sie als abstrakt deklariert (Zeile 39);
  • Zeile 16: Der Konstruktor erhält ein [config]-Wörterbuch, das die folgenden Informationen enthält:
    • [taxpayersFilename]: der Name der Textdatei, die die Steuerzahlerdaten enthält;
    • [resultsFilename]: der Name der Textdatei, in der die Ergebnisse gespeichert werden;
    • [errorsFilename]: der Name der Textdatei, in der die bei der Verarbeitung der Datei [taxpayersFilename] aufgetretenen Fehler aufgelistet sind;

Die Methode [get_taxpayers_data] lautet wie folgt:

    #  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()
  • Zeile 4: Die Steuerzahlerdaten (verheiratet, Kinder, Gehalt) werden in eine Liste von Objekten vom Typ [TaxPayer] aufgenommen;
  • Zeilen 8–9: Wir öffnen die Steuerzahler-Textdatei zum Lesen. Ihr Inhalt hat das folgende Format:
# 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

Im Vergleich zu früheren Versionen:

  • Jede Zeile in der Datei [taxpayersFilename] beginnt mit der Steueridentifikationsnummer, einer einzelnen Zahl;
  • Kommentare und Leerzeilen sind zulässig;
  • wir werden Fehler behandeln. Daher müssen die Zeilen 17, 19 und 21 als ungültig markiert werden. Fehler werden in einer separaten Datei protokolliert;

Fahren wir mit der Durchsicht des Codes fort:

  • Zeile 4: Die Daten aus der Textdatei werden in die Liste [taxPayersData] übertragen;
  • Zeilen 14–31: Die Steuerzahlerdatei wird Zeile für Zeile gelesen;
  • Zeile 14: Das Ende der Datei ist erreicht, wenn eine leere Zeile gelesen wird (nichts – nicht einmal das Zeilenendezeichen \r\n);
  • Zeile 20: Leere Zeilen und Kommentare werden ignoriert. Eine Zeile ist ein Kommentar, wenn nach dem Entfernen der Leerzeichen vor und nach dem Text das erste Zeichen das #-Zeichen ist;
  • Zeile 24: Eine gültige Zeile besteht aus vier durch Kommas getrennten Feldern. Diese werden abgerufen. Die Zuweisung von Daten zu einem vierelementigen Tupel schlägt fehl, wenn nicht genau vier Datenpunkte zugewiesen sind;
  • Zeile 25: Wenn eines der vier abgerufenen Felder [id, married, children, salary] ungültig ist, löst die Methode [BaseEntity.fromdict] eine [MyException]-Ausnahme aus;
  • Zeilen 25–26: Ein [TaxPayer]-Objekt wird der Liste [taxpayers_data] mit Steuerzahlern hinzugefügt;
  • Zeilen 27–29: Alle Fehler werden in einer Liste [errors] gesammelt. Diese Liste wurde in Zeile 6 erstellt;
  • Zeilen 33–36: Die Liste der aufgetretenen Fehler wird in der Textdatei [errorsFilename] gespeichert. Es gibt zwei Arten von Fehlern:
    • Eine Zeile wies nicht die richtige Anzahl der erwarteten Felder auf;
    • die Informationen in der Zeile waren falsch und es konnte kein [TaxPayer]-Objekt erstellt werden;
  • Zeilen 39–41: Jeder Fehler (BaseException) wird abgefangen und weitergeleitet, indem er in einen Typ [TaxPayerError] verpackt wird;
  • Zeilen 42–45: In allen Fällen, ob erfolgreich oder nicht, wird die Steuerzahler-Textdatei geschlossen, falls sie geöffnet war;

Die Methode [write_taxpayers_results] muss eine JSON-Datei im folgenden Format erzeugen:


[
  {
    "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
  },

]

Die Methode [write_taxpayers_results] lautet wie folgt:

    #  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()
  • Zeile 2: Die Methode erhält eine Liste von Steuerzahlern [taxpayers], die sie im JSON-Format in der Textdatei [self.taxpayers_results_filename] speichern muss;
  • Zeile 10: Erstellung der UTF-8-Ergebnisdatei;
  • Zeile 12: Hier führen wir die Funktion [map] ein, deren Syntax hier [map (function, list1)] lautet. Die Funktion [function] wird auf jedes Element von [list1] angewendet und erzeugt ein neues Element, das in die Liste [list2] aufgenommen wird. Schließlich gilt für jedes i:

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

Hier ist [list1] die Liste [taxPayers], eine Liste von Objekten vom Typ [TaxPayer]. Die Funktion [function] wird hier als sogenannte [lambda]-Funktion ausgedrückt, die die auf ein Element [taxpayer] der Liste [taxpayers] angewendete Transformation beschreibt: Jedes [taxpayer]-Element wird durch sein Wörterbuch [taxpayer.asdict()] ersetzt. Schließlich ist die resultierende Liste [list2] die Liste der Wörterbücher der Elemente in der Liste [taxpayers];

  • Zeile 12: Das von der Funktion [map] zurückgegebene Ergebnis ist nicht die Liste [list2], sondern ein Objekt vom Typ [map]. Um [list2] zu erhalten, müssen Sie den Ausdruck [list(mapping)] verwenden (Zeile 14);
  • Zeile 14: Die Liste [list2] wird im JSON-Format in der Datei [self.taxpayers_results_filename] gespeichert;
  • Zeilen 15–17: Jede Art von Ausnahme wird abgefangen und in ein [ImpôtsError] verpackt, bevor sie erneut ausgelöst wird (Zeile 17);
  • Zeilen 19–21: In jedem Fall, ob erfolgreich oder nicht, wird die Ergebnisdatei geschlossen, falls sie geöffnet war;

15.1.2.3. Klasse [ImpôtsDaoWithAdminDataInJsonFile]

Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] wird von der Klasse [AbstractImpôtsDao] abgeleitet und die Methode [getAdminData] implementieren, die ihre übergeordnete Klasse nicht implementiert hat. Sie wird Steuerverwaltungsdaten aus einer JSON-Datei abrufen:


{
    "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
}

Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] sieht wie folgt aus:

#  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
  • Zeile 11: Die Klasse [ImpôtsDaoWithAdminDataInJsonFile] erbt von der Klasse [AbstractImpôtsDao]. Als solche implementiert sie die Schnittstelle [InterfaceImpôtsDao];
  • Zeile 13: Der Konstruktor erhält als Parameter ein Wörterbuch, das die Informationen aus den Zeilen 14–17 enthält;
  • Zeile 20: Die übergeordnete Klasse wird initialisiert;
  • Zeile 24: Die JSON-Datei mit den Daten der Steuerbehörde wird geöffnet;
  • Zeile 25: Die UTF-8-Datei mit den Daten der Steuerbehörde wird geöffnet;
  • Zeile 27: Der Inhalt der Datei wird gelesen und in das Objekt [self.admindata] vom Typ [AdminData] gespeichert. Die Schlüssel in der JSON-Datei müssen mit den für ein [AdminData]-Objekt akzeptierten Eigenschaften übereinstimmen; andernfalls löst die Methode [fromdict] eine Ausnahme aus;
  • Zeilen 28–30: Ausnahmebehandlung. Alle möglicherweise auftretenden Ausnahmen werden in einen Typ [ImpôtsError] verpackt, bevor sie erneut ausgelöst werden;
  • Zeilen 32–34: Die Datei wird geschlossen, falls sie geöffnet wurde;
  • Zeilen 42–43: Implementierung der Methode [get_admindata] der Schnittstelle [InterfaceImpôtsDao];

15.1.3. Die [business]-Schicht

Image

15.1.3.1. Die Schnittstelle [InterfaceImpôtsMétier]

Die Schnittstelle für die [Geschäfts-]Ebene sieht wie folgt aus:

#  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
  • Die Schnittstelle [BusinessTaxInterface] definiert eine einzige Methode:
  • Zeile 12: Die Methode [calculate_tax] berechnet die Steuer für einen einzelnen Steuerzahler [taxpayer]. [admindata] ist das [AdminData]-Objekt, das die Daten der Steuerverwaltung kapselt;
  • Zeile 12: Die Methode [calculate_tax] gibt kein Ergebnis zurück. Die erhaltenen Daten (Steuer, Zuschlag, Rabatt, Ermäßigung, Steuersatz) werden in den Parameter [taxpayer] aufgenommen: Vor dem Aufruf sind diese Attribute leer; nach dem Aufruf sind sie initialisiert;

15.1.3.2. Die Klasse [BusinessTaxes]

Die Klasse [ImpôtsMétier] implementiert die Schnittstelle [InterfaceImpôtsMétier] wie folgt:

Image

Die Klassenmethoden stammen aus dem Modul [impôts_module_02] im Abschnitt |Das Modul [impots.v02.modules.impôts_module_02]|. Wir haben die Methodenparameter auf nur zwei beschränkt:

  • taxpayer(id, married, children, salary, tax, discount, surcharge, reduction, rate): das Objekt, das einen Steuerzahler und dessen Steuern repräsentiert;
  • admindata: das Objekt, das die Daten der Steuerverwaltung kapselt;

Wir veranschaulichen die vorgenommenen Änderungen anhand einer Methode;

    #  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)
  • Zeile 3: Die Methode [calculate_tax] ist die einzige Methode in der Schnittstelle [InterfaceImpôtsMétier]. Sie nimmt zwei Parameter entgegen:
    • [tapPayer]: der Steuerzahler, für den die Steuer berechnet wird;
    • [admindata]: das Objekt, das die Daten der Steuerverwaltung enthält;
    • Die Ergebnisse der Berechnung werden im Parameter [taxpayer] gekapselt (Zeilen 40–50). Der Inhalt dieses Objekts ist daher vor und nach dem Aufruf der Methode nicht identisch;

15.1.4. Tests für die Schichten [dao] und [business]

Image

  • [TestDaoMétier] ist die UnitTest-Klasse zum Testen der [dao]- und [business]-Schichten;
  • [config] ist die Testkonfigurationsdatei;

Die [config]-Konfiguration lautet wie folgt:

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
  • Zeilen 4–23: Wir konfigurieren den Python-Pfad für die Tests;
  • Zeilen 32–41: Instanziieren der Schichten [dao] und [business]. Speichern ihrer Referenzen im Wörterbuch [config];
  • Zeile 44: Dieses Wörterbuch zurückgeben;

Die Testklasse [TestDaoMétier] sieht wie folgt aus:

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()

Kommentare

  • Zeile 11: Die Testklasse erweitert die Klasse [unittest.TestCase];
  • Zeilen 13–19: In einem UnitTest wird die Methode [setUp] vor jeder der [test_]-Methoden ausgeführt;
  • Zeile 16: Die Konfiguration aus dem zuvor besprochenen [config]-Skript wird abgerufen;
  • Zeile 18: Eine Referenz auf die [business]-Schicht wird gespeichert;
  • Zeile 19: Wir fordern das [AdminData]-Objekt – das Daten der Steuerverwaltung kapselt – von der [DAO]-Schicht an und speichern es;
  • Zeilen 21–173: 11 Tests, deren Ergebnisse auf der offiziellen Steuer-Website 2019 |https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm| verifiziert wurden;
  • Zeilen 21–33: Alle Tests wurden anhand derselben Vorlage erstellt;
  • Zeile 22: Import der Klasse [TaxPayer];
  • Zeile 24: Der zu testende Steuerzahler;
  • Zeile 25: erwartete Ergebnisse;
  • Zeile 26: Erstellung des [TaxPayer]-Objekts des Steuerzahlers;
  • Zeile 27: Berechnung der Steuer. Das Ergebnis befindet sich in [taxpayer];
  • Zeilen 29–33: Überprüfung der erzielten Ergebnisse;
  • Zeile 29: Wir überprüfen den Steuerbetrag auf den nächsten Euro. Tests haben tatsächlich gezeigt, dass die vom Algorithmus in diesem Dokument erzielten Ergebnisse um bis zu 1 Euro von den offiziellen Zahlen abweichen können;

Die Durchführung der Tests liefert folgende Ergebnisse:

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. Hauptskript

Image

Das Hauptskript wird durch das folgende [config]-Skript konfiguriert:

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

Sie ähnelt derjenigen, die zum Testen der [Business]- und [DAO]-Schichten verwendet wird.

Das Hauptskript [main.py] sieht wie folgt aus:

#  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é...")

Anmerkungen

  • Zeilen 2–4: Wir rufen die Anwendungskonfiguration ab. Wir wissen auch, dass der Python-Pfad der Anwendung erstellt wurde;
  • Zeilen 9–11: Wir rufen Referenzen auf die [Business]- und [DAO]-Schichten ab;
  • Zeile 15: Wir rufen Daten von der Steuerverwaltung ab;
  • Zeile 17: Wir rufen die Liste der Steuerzahler ab, für die Steuern berechnet werden müssen;
  • Zeilen 19–20: Ist diese Liste leer, wird eine Ausnahme ausgelöst;
  • Zeilen 22–25: Berechnung der Steuer für die verschiedenen [taxpayer]-Objekte unter Verwendung der [business]-Schicht;
  • Zeile 27: [taxpayers] ist nun eine Liste von [TaxPayer]-Objekten, denen Werte für die Attribute (tax, discount, surcharge, reduction, rate) zugewiesen wurden. Diese Liste wird in eine JSON-Datei geschrieben;
  • Zeilen 28–30: Erfassen Sie mögliche Fehler;
  • Zeilen 31–33: werden in allen Fällen ausgeführt;

Die Ausführung des Skripts liefert die gleichen Ergebnisse wie in früheren Versionen. Die Steuerzahler-Fehlerdatei war eine neue Funktion in dieser Version. Nach Ausführung des [main]-Skripts sieht dessen Inhalt wie folgt aus:


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]

Die fehlerhaften Zeilen lauteten wie folgt:

# 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. Version 4 – Anwendung 2

In dieser Version gibt der Benutzer die Liste der Steuerzahler über die Tastatur ein. Die Anwendungsarchitektur sieht wie folgt aus:

Image

Es kommt ein neues Modul hinzu: die [ui]-Schicht (User Interface), die mit dem Benutzer interagiert. Diese Schicht verfügt über eine Schnittstelle und wird durch eine Klasse implementiert.

Image

15.2.1. Die Schnittstelle [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

Die Schnittstelle [InterfaceImpôtsUi] wird nur eine Methode haben, nämlich die in den Zeilen 8–10. Die Schnittstelle wird hier mit einer Konsolenanwendung implementiert, könnte aber auch mit einer grafischen Benutzeroberfläche implementiert werden. Die an die Methode [run] übergebenen Parameter wären in beiden Implementierungen nicht identisch. Um dieses Problem zu umgehen, wird üblicherweise wie folgt vorgegangen:

  • keine Parameter an die Methode [run] zu übergeben (oder nur die Mindestanzahl an Parametern zu übergeben);
  • Parameter an den Konstruktor der Klasse übergeben, die die Schnittstelle implementiert. Diese können sich von einer Implementierung zur anderen unterscheiden. Diese Parameter werden als Klassenattribute gespeichert;
  • sicherstellen, dass die [run]-Methode diese Klassenattribute verwendet (self.x);

Diese Methode ermöglicht eine sehr allgemeine Schnittstelle, die durch die Parameter der Konstruktoren jeder Implementierungsklasse festgelegt wird. Diese Methode wurde bereits für die modulare Version Nr. 1 verwendet.

15.2.2. Die Klasse [ImpôtsConsole]

Die Klasse [ImpôtsConsole] implementiert die Schnittstelle [InterfaceImpôtsUi] wie folgt:

#  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
  • Zeile 9: Die Klasse [TaxConsole] implementiert die Schnittstelle [TaxUiInterface];
  • Zeile 11: Der Klassenkonstruktor erhält einen Parameter, das [config]-Wörterbuch, das die Anwendungskonfiguration enthält;
    • Zeile 13: Es werden Daten von der Steuerbehörde abgerufen, um die Steuer zu berechnen;
    • Zeile 14: Eine Referenz auf die [business]-Schicht wird gespeichert;
  • Zeile 16: Implementierung der Methode [run] der Schnittstelle;
  • Zeilen 19–53: Benutzerinteraktion. Dabei
    • die Abfrage von drei Informationen beim Steuerzahler (Familienstand, Kinder, Gehalt);
    • Berechnung seiner Steuer;
    • Anzeige des Ergebnisses;
    • der Dialog endet, wenn der Benutzer auf die erste Frage mit * antwortet;
  • Zeilen 20–27: Das Programm fragt, ob der Steuerzahler verheiratet ist, und überprüft die Gültigkeit der Antwort;
  • Zeilen 29–31: Wenn der Benutzer die Frage mit „*“ beantwortet hat, endet der Dialog;
  • Zeilen 32–39: Der Steuerzahler wird gefragt, wie viele Kinder er hat, und die Gültigkeit der Antwort wird überprüft;
  • Zeilen 40–47: Das Jahreseinkommen des Steuerzahlers wird abgefragt und die Gültigkeit der Antwort überprüft;
  • Zeilen 48–50: Anhand dieser Informationen berechnet die [Business]-Schicht die Steuer des Steuerzahlers;
  • Zeile 52: Der Steuerbetrag wird angezeigt;

15.2.3. Das Hauptskript

Das Hauptskript [main] wird durch die folgende [config]-Datei konfiguriert:

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

Das Hauptskript lautet wie folgt (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é...")
  • Zeilen 1–4: Abrufen der Anwendungskonfiguration;
  • Zeile 10: Ruft eine Referenz auf die [ui]-Ebene ab;
  • Zeilen 12–21: Die Codestruktur entspricht der der vorherigen Anwendung: Der Code ist in einen try/catch-Block eingeschlossen, um mögliche Ausnahmen abzufangen;
  • Zeile 15: Wir weisen die [ui]-Schicht an, die Ausführung zu starten: Die Benutzerinteraktion beginnt dann;
  • Zeilen 16–18: Abfangen möglicher Ausnahmen;

Hier ist ein Beispiel für die Ausführung:


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